bayarea-cc/src/hooks/use-countup.ts

90 lines
2.8 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
interface UseCountUpOptions {
end: number;
duration?: number;
decimals?: number;
startOnInView?: boolean;
}
export const useCountUp = ({ end, duration = 2000, decimals = 0, startOnInView = true }: UseCountUpOptions) => {
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const [hasStarted, setHasStarted] = useState(false);
const elementRef = useRef<HTMLElement>(null);
// Intersection Observer for triggering animation when element comes into view
useEffect(() => {
if (!startOnInView) {
setHasStarted(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasStarted) {
setIsVisible(true);
setHasStarted(true);
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => observer.disconnect();
}, [startOnInView, hasStarted]);
// Counter animation
useEffect(() => {
if (!hasStarted) return;
let startTime: number;
let animationFrame: number;
const animate = (currentTime: number) => {
if (!startTime) startTime = currentTime;
const progress = Math.min((currentTime - startTime) / duration, 1);
// Easing function for smooth animation
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
const currentCount = end * easeOutCubic;
setCount(currentCount);
if (progress < 1) {
animationFrame = requestAnimationFrame(animate);
}
};
animationFrame = requestAnimationFrame(animate);
return () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
};
}, [end, duration, hasStarted]);
const formattedCount = count.toFixed(decimals);
return { count: formattedCount, elementRef };
};
// Utility function to parse numbers from strings like "150+", "99.9%", "<2min"
export const parseNumberFromString = (value: string): number => {
const numericMatch = value.match(/(\d+\.?\d*)/);
return numericMatch ? parseFloat(numericMatch[1]) : 0;
};
// Utility function to format the final value with original suffix/prefix
export const formatWithOriginalString = (originalValue: string, animatedNumber: string): string => {
const numericMatch = originalValue.match(/(\d+\.?\d*)/);
if (!numericMatch) return originalValue;
const originalNumber = numericMatch[1];
return originalValue.replace(originalNumber, animatedNumber);
};