agency-web/app/components/CountUp.tsx

57 lines
1.4 KiB
TypeScript

"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<HTMLSpanElement>(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 (
<span ref={ref}>
{prefix}
{display}
{suffix}
</span>
);
}