import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from 'motion/react'; import * as React from 'react'; const DIGITS = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; const DIGIT_RE = /^\d$/; const DURATION = 0.22; const EASE: [ number, number, number, number ] = [ 0.2, 0, 0.2, 1 ]; const STAGGER_MS = 20; interface DigitColumnProps { char: string; delay: number; } /** * A single digit slot — a clipped column of 0-9 that scrolls to the * correct position when `char` changes. Direction (up/down) falls out * naturally from the relative position of old vs new digit values. */ function DigitColumn( { char, delay }: DigitColumnProps ) { const reduced = useReducedMotion(); const digit = parseInt( char, 10 ); const mv = useMotionValue( -digit ); const y = useTransform( mv, v => `${ v }em` ); React.useEffect( () => { if ( reduced ) { mv.set( -digit ); return; } const controls = animate( mv, -digit, { duration: DURATION, ease: EASE, delay: delay / 1000, } ); return controls.stop; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ char ] ); return ( { DIGITS.map( d => ( { d } ) ) } ); } interface Props { value: string; /** * Kept for API compatibility — the column approach determines scroll * direction automatically from digit values, so this prop is unused. */ direction?: 'up' | 'down'; className?: string; } /** * Slot-machine digit animation for refreshing numeric strings. * * Each digit scrolls independently to its new value (right-to-left * stagger, 20 ms per position). Non-digit characters (commas, dots, * k, M, %) render statically. When the formatted string changes length * (e.g. "999" → "1,000") the entire value cross-fades instead. * * Respects `prefers-reduced-motion` — falls back to an instant update. */ export function SlotNumber( { value, className }: Props ) { const reduced = useReducedMotion(); const prevRef = React.useRef( value ); const prevValue = prevRef.current; React.useEffect( () => { prevRef.current = value; } ); if ( reduced ) { return { value }; } const chars = value.split( '' ); const prevChars = prevValue.split( '' ); // Cross-fade the whole string when length changes — digit column // positions would misalign if we tried to animate char-by-char. if ( chars.length !== prevChars.length ) { return ( { value } ); } return ( { chars.map( ( char, i ) => { if ( ! DIGIT_RE.test( char ) ) { return { char }; } // Stagger from right (units) to left (most significant) const distFromRight = chars.length - 1 - i; return ( ); } ) } ); }