90 lines
2.8 KiB
TypeScript
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);
|
|
};
|