"use client"; // Adapted from Magic UI (MIT). See THIRD_PARTY_NOTICES.md. import { type ComponentProps, useEffect, useRef } from "react"; export interface NumberTickerProps extends Omit< ComponentProps<"span">, "children" > { value: number; startValue?: number; duration?: number; decimalPlaces?: number; locale?: string; } export function NumberTicker({ value, startValue = 0, duration = 800, decimalPlaces = 0, locale = "en-US", className = "", ...props }: NumberTickerProps) { const visual = useRef(null); const current = useRef(startValue); const target = Number.isFinite(value) ? value : 0; const digits = Number.isFinite(decimalPlaces) ? Math.max(0, Math.min(20, Math.floor(decimalPlaces))) : 0; const format = new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits, }); const formatted = format.format(target); useEffect(() => { const media = matchMedia("(prefers-reduced-motion: reduce)"); let frame = 0; const formatter = new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits, }); const from = Number.isFinite(current.current) ? current.current : 0; const finish = () => { cancelAnimationFrame(frame); current.current = target; if (visual.current) visual.current.textContent = formatter.format(target); }; const start = performance.now(); const tick = (now: number) => { const progress = Math.min(1, (now - start) / duration); current.current = from + (target - from) * (1 - (1 - progress) ** 3); if (visual.current) visual.current.textContent = formatter.format(current.current); if (progress < 1) frame = requestAnimationFrame(tick); }; if (media.matches || !Number.isFinite(duration) || duration <= 0) finish(); else frame = requestAnimationFrame(tick); media.addEventListener("change", finish); return () => { cancelAnimationFrame(frame); media.removeEventListener("change", finish); }; }, [target, duration, digits, locale]); return ( {formatted} ); }