veza/apps/web/src/hooks/useAnimatedCounter.ts

54 lines
1.3 KiB
TypeScript
Raw Normal View History

import { useState, useEffect, useRef } from 'react';
interface UseAnimatedCounterOptions {
/** Final value to count to */
end: number;
/** Duration of the animation in ms */
duration?: number;
/** Whether the animation should start (default: true) */
enabled?: boolean;
/** Number of decimal places */
decimals?: number;
}
export function useAnimatedCounter({
end,
duration = 1000,
enabled = true,
decimals = 0,
}: UseAnimatedCounterOptions): number {
const [count, setCount] = useState(0);
const frameRef = useRef<number>();
const startTimeRef = useRef<number>();
useEffect(() => {
if (!enabled || end === 0) {
setCount(end);
return;
}
const animate = (timestamp: number) => {
if (!startTimeRef.current) startTimeRef.current = timestamp;
const progress = Math.min((timestamp - startTimeRef.current) / duration, 1);
// Ease-out cubic
const eased = 1 - Math.pow(1 - progress, 3);
const current = eased * end;
setCount(Number(current.toFixed(decimals)));
if (progress < 1) {
frameRef.current = requestAnimationFrame(animate);
}
};
frameRef.current = requestAnimationFrame(animate);
return () => {
if (frameRef.current) cancelAnimationFrame(frameRef.current);
};
}, [end, duration, enabled, decimals]);
return count;
}