import React, { useEffect, useRef } from 'react'; import { gsap } from 'gsap'; /** * AnimatedNumber - A reusable component that animates numbers from 0 to their target value using GSAP * @param {number|string} value - The target value to animate to * @param {number} duration - Animation duration in seconds (default: 2) * @param {string} prefix - Optional prefix (e.g., "$") * @param {string} suffix - Optional suffix (e.g., "%", "K", "M") * @param {number} decimals - Number of decimal places (default: 0) * @param {boolean} useLocaleString - Whether to format with commas (default: false) * @param {string} className - Additional CSS classes */ const AnimatedNumber = ({ value, duration = 2, prefix = '', suffix = '', decimals = 0, useLocaleString = false, className = '', delay = 0 }) => { const numberRef = useRef(null); const animationRef = useRef(null); useEffect(() => { if (!numberRef.current) return; // Extract numeric value if it's a string like "$4.2M" let numericValue = value; let extractedPrefix = prefix; let extractedSuffix = suffix; if (typeof value === 'string') { // Check for currency symbols if (value.startsWith('$')) { extractedPrefix = '$'; value = value.substring(1); } // Check for suffixes like M, K, B const lastChar = value.slice(-1); if (['K', 'M', 'B'].includes(lastChar.toUpperCase())) { extractedSuffix = lastChar.toUpperCase(); value = value.slice(0, -1); } // Parse the numeric part numericValue = parseFloat(value.replace(/,/g, '')); } // Handle non-numeric values if (isNaN(numericValue)) { numberRef.current.textContent = value; return; } // Convert suffix multipliers to actual numbers let multiplier = 1; if (extractedSuffix === 'K') multiplier = 1000; if (extractedSuffix === 'M') multiplier = 1000000; if (extractedSuffix === 'B') multiplier = 1000000000; const targetValue = numericValue * multiplier; // Animate from 0 to target value const obj = { val: 0 }; animationRef.current = gsap.to(obj, { val: numericValue, duration: duration, delay: delay, ease: 'expo.out', onUpdate: () => { if (numberRef.current) { let displayValue = obj.val.toFixed(decimals); if (useLocaleString) { displayValue = parseFloat(displayValue).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } numberRef.current.textContent = `${extractedPrefix}${displayValue}${extractedSuffix}`; } }, onComplete: () => { // Ensure final value is exact if (numberRef.current) { let displayValue = numericValue.toFixed(decimals); if (useLocaleString) { displayValue = parseFloat(displayValue).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } numberRef.current.textContent = `${extractedPrefix}${displayValue}${extractedSuffix}`; } } }); return () => { if (animationRef.current) { animationRef.current.kill(); } }; }, [value, duration, prefix, suffix, decimals, useLocaleString, delay]); return 0; }; export default AnimatedNumber;