51 lines
1.2 KiB
TypeScript
51 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useInView } from "framer-motion";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface AnimatedNumberProps {
|
|
value: number;
|
|
suffix?: string;
|
|
prefix?: string;
|
|
duration?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function AnimatedNumber({
|
|
value,
|
|
suffix = "",
|
|
prefix = "",
|
|
duration = 1800,
|
|
className,
|
|
}: AnimatedNumberProps) {
|
|
const [count, setCount] = useState(0);
|
|
const ref = useRef<HTMLSpanElement>(null);
|
|
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
|
|
|
useEffect(() => {
|
|
if (!isInView) return;
|
|
|
|
let rafId: number;
|
|
let startTime: number | null = null;
|
|
|
|
const step = (ts: number) => {
|
|
if (!startTime) startTime = ts;
|
|
const progress = Math.min((ts - startTime) / duration, 1);
|
|
// Ease-out cubic
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
setCount(Math.round(eased * value));
|
|
if (progress < 1) rafId = requestAnimationFrame(step);
|
|
};
|
|
|
|
rafId = requestAnimationFrame(step);
|
|
return () => cancelAnimationFrame(rafId);
|
|
}, [isInView, value, duration]);
|
|
|
|
return (
|
|
<span ref={ref} className={cn(className)}>
|
|
{prefix}{count}{suffix}
|
|
</span>
|
|
);
|
|
}
|