"use client"; /** * Animated number that counts up when it enters the viewport (once). * - Motion `animate()` drives a raw value; we format with prefix/suffix/decimals. * - prefers-reduced-motion: jumps straight to the final value. * - Real number text stays in the DOM for SEO / screen readers. */ import { useEffect, useRef, useState } from "react"; import { animate, useInView, useReducedMotion } from "motion/react"; import { EASE_OUT } from "./motion"; export default function CountUp({ to, prefix = "", suffix = "", decimals = 0, duration = 1.8, }: { to: number; prefix?: string; suffix?: string; decimals?: number; duration?: number; }) { const ref = useRef(null); const inView = useInView(ref, { once: true, margin: "0px 0px -15% 0px" }); const reduce = useReducedMotion(); const [val, setVal] = useState(0); useEffect(() => { if (!inView) return; if (reduce) { setVal(to); return; } const controls = animate(0, to, { duration, ease: EASE_OUT, onUpdate: (v) => setVal(v), }); return () => controls.stop(); }, [inView, to, duration, reduce]); const display = val.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); return ( {prefix} {display} {suffix} ); }