'use client'; import * as React from 'react'; import { type SpringOptions, type UseInViewOptions, useInView, useMotionValue, useSpring, } from 'motion/react'; type CountingNumberProps = React.ComponentProps<'span'> & { number: number; fromNumber?: number; padStart?: boolean; inView?: boolean; inViewMargin?: UseInViewOptions['margin']; inViewOnce?: boolean; decimalSeparator?: string; transition?: SpringOptions; decimalPlaces?: number; }; function CountingNumber({ ref, number, fromNumber = 0, padStart = false, inView = false, inViewMargin = '0px', inViewOnce = true, decimalSeparator = '.', transition = { stiffness: 90, damping: 50 }, decimalPlaces = 0, className, ...props }: CountingNumberProps) { const localRef = React.useRef(null); React.useImperativeHandle(ref, () => localRef.current as HTMLSpanElement); const numberStr = number.toString(); const decimals = typeof decimalPlaces === 'number' ? decimalPlaces : numberStr.includes('.') ? (numberStr.split('.')[1]?.length ?? 0) : 0; const motionVal = useMotionValue(fromNumber); const springVal = useSpring(motionVal, transition); const inViewResult = useInView(localRef, { once: inViewOnce, margin: inViewMargin, }); const isInView = !inView || inViewResult; React.useEffect(() => { if (isInView) motionVal.set(number); }, [isInView, number, motionVal]); React.useEffect(() => { const unsubscribe = springVal.on('change', (latest) => { if (localRef.current) { let formatted = decimals > 0 ? latest.toFixed(decimals) : Math.round(latest).toString(); if (decimals > 0) { formatted = formatted.replace('.', decimalSeparator); } if (padStart) { const finalIntLength = Math.floor(Math.abs(number)).toString().length; const [intPart, fracPart] = formatted.split(decimalSeparator); const paddedInt = intPart?.padStart(finalIntLength, '0') ?? ''; formatted = fracPart ? `${paddedInt}${decimalSeparator}${fracPart}` : paddedInt; } localRef.current.textContent = formatted; } }); return () => unsubscribe(); }, [springVal, decimals, padStart, number, decimalSeparator]); const finalIntLength = Math.floor(Math.abs(number)).toString().length; const initialText = padStart ? '0'.padStart(finalIntLength, '0') + (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '') : '0' + (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : ''); return ( {initialText} ); } export { CountingNumber, type CountingNumberProps };