Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useEffect, useRef, useState } from "react"; | |
| interface AnimatedCounterProps { | |
| value: number; | |
| suffix?: string; | |
| duration?: number; | |
| className?: string; | |
| } | |
| export function AnimatedCounter({ | |
| value, | |
| suffix = "", | |
| duration = 1200, | |
| className, | |
| }: AnimatedCounterProps) { | |
| const [display, setDisplay] = useState(0); | |
| const ref = useRef<HTMLSpanElement>(null); | |
| const hasAnimated = useRef(false); | |
| useEffect(() => { | |
| if (hasAnimated.current) return; | |
| hasAnimated.current = true; | |
| const start = performance.now(); | |
| function tick(now: number) { | |
| const elapsed = now - start; | |
| const progress = Math.min(elapsed / duration, 1); | |
| // ease-out expo | |
| const ease = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress); | |
| setDisplay(Math.round(ease * value)); | |
| if (progress < 1) requestAnimationFrame(tick); | |
| } | |
| requestAnimationFrame(tick); | |
| }, [value, duration]); | |
| return ( | |
| <span ref={ref} className={className}> | |
| {display} | |
| {suffix} | |
| </span> | |
| ); | |
| } | |