"use client"; import { motion, useInView } from "motion/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export interface NumberTickerProps { value: number; /** Digits to pad to (left). */ pad?: number; /** Per-digit roll duration in seconds. */ duration?: number; /** Stagger between digits. */ stagger?: number; /** Render only after the element enters the viewport. */ startOnView?: boolean; prefix?: string; suffix?: string; className?: string; digitClassName?: string; /** Insert locale group separators (commas). Server-component safe. */ locale?: boolean; /** Custom formatter. Client-only — server components must use `locale` instead. */ format?: (value: number) => string; } const DIGIT_HEIGHT_EM = 1.1; export function NumberTicker({ value, pad, duration = 0.9, stagger = 0.04, startOnView = true, prefix, suffix, className, digitClassName, locale, format, }: NumberTickerProps) { const containerRef = useRef(null); const inView = useInView(containerRef, { once: true, amount: 0.6 }); const [armed, setArmed] = useState(!startOnView); useEffect(() => { if (startOnView && inView) setArmed(true); }, [startOnView, inView]); const text = useMemo(() => { const rounded = Math.round(value); const formatted = format ? format(rounded) : locale ? rounded.toLocaleString() : rounded.toString(); return pad ? formatted.padStart(pad, "0") : formatted; }, [value, pad, format, locale]); return ( {prefix ? {prefix} : null} {text.split("").map((char, i) => { const isDigit = /\d/.test(char); if (!isDigit) { return ( {char} ); } const digit = Number(char); return ( ); })} {suffix ? {suffix} : null} ); } function Digit({ digit, delay, duration, className, }: { digit: number; delay: number; duration: number; className?: string; }) { return ( {Array.from({ length: 10 }, (_, n) => ( {n} ))} ); }