74 lines
1.7 KiB
TypeScript
74 lines
1.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
interface AnimatedCounterProps {
|
|
end: number;
|
|
duration?: number;
|
|
prefix?: string;
|
|
suffix?: string;
|
|
decimals?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export default function AnimatedCounter({
|
|
end,
|
|
duration = 2000,
|
|
prefix = '',
|
|
suffix = '',
|
|
decimals = 0,
|
|
className = '',
|
|
}: AnimatedCounterProps) {
|
|
const [count, setCount] = useState(0);
|
|
const countRef = useRef(0);
|
|
const startTimeRef = useRef<number | null>(null);
|
|
const frameRef = useRef<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Reset when end value changes
|
|
countRef.current = 0;
|
|
startTimeRef.current = null;
|
|
setCount(0);
|
|
|
|
const animate = (timestamp: number) => {
|
|
if (startTimeRef.current === null) {
|
|
startTimeRef.current = timestamp;
|
|
}
|
|
|
|
const progress = timestamp - startTimeRef.current;
|
|
const percentage = Math.min(progress / duration, 1);
|
|
|
|
// Easing function for smoother animation
|
|
const easeOutQuart = 1 - Math.pow(1 - percentage, 4);
|
|
|
|
const currentCount = Math.min(easeOutQuart * end, end);
|
|
countRef.current = currentCount;
|
|
setCount(currentCount);
|
|
|
|
if (percentage < 1) {
|
|
frameRef.current = requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
frameRef.current = requestAnimationFrame(animate);
|
|
|
|
return () => {
|
|
if (frameRef.current !== null) {
|
|
cancelAnimationFrame(frameRef.current);
|
|
}
|
|
};
|
|
}, [end, duration]);
|
|
|
|
const formatNumber = (num: number) => {
|
|
return num.toFixed(decimals);
|
|
};
|
|
|
|
return (
|
|
<span className={className}>
|
|
{prefix}
|
|
{formatNumber(count)}
|
|
{suffix}
|
|
</span>
|
|
);
|
|
}
|