import { useInView, useMotionValue, useSpring } from 'framer-motion'; import { useEffect, useRef } from 'react'; export default function CountUp({ to, from = 0, direction = 'up', delay = 0, duration = 2, className = '', postFix = '', startWhen = true, separator = '', onStart, onEnd, }) { const ref = useRef(null); const motionValue = useMotionValue(direction === 'down' ? to : from); const damping = 20 + 40 * (1 / duration); const stiffness = 100 * (1 / duration); const springValue = useSpring(motionValue, { damping, stiffness, }); const isInView = useInView(ref, { once: true, margin: '0px' }); useEffect(() => { if (ref.current) { ref.current.textContent = String(direction === 'down' ? to : from); } }, [from, to, direction]); useEffect(() => { if (isInView && startWhen) { if (typeof onStart === 'function') { onStart(); } const timeoutId = setTimeout(() => { motionValue.set(direction === 'down' ? from : to); }, delay * 1000); const durationTimeoutId = setTimeout( () => { if (typeof onEnd === 'function') { onEnd(); } }, delay * 1000 + duration * 1000, ); return () => { clearTimeout(timeoutId); clearTimeout(durationTimeoutId); }; } }, [isInView, startWhen, motionValue, direction, from, to, delay, onStart, onEnd, duration]); useEffect(() => { const unsubscribe = springValue.on('change', (latest) => { if (ref.current) { const options = { useGrouping: !!separator, minimumFractionDigits: 0, maximumFractionDigits: 0, }; const formattedNumber = Intl.NumberFormat('en-US', options).format(latest.toFixed(0)); ref.current.textContent = (separator ? formattedNumber.replace(/,/g, separator) : formattedNumber) + postFix; } }); return () => unsubscribe(); }, [springValue, separator]); return ; }