diff --git a/src/components/ui/animated-number.tsx b/src/components/ui/animated-number.tsx new file mode 100644 index 0000000..1edc32b --- /dev/null +++ b/src/components/ui/animated-number.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useEffect, useState, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; + +interface AnimatedNumberProps { + value: number; + duration?: number; + delay?: number; + suffix?: string; + prefix?: string; + className?: string; +} + +export function AnimatedNumber({ + value, + duration = 2000, + delay = 0, + suffix = '', + prefix = '', + className = '', +}: AnimatedNumberProps) { + const [count, setCount] = useState(0); + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + const hasAnimated = useRef(false); + + useEffect(() => { + if (!isInView || hasAnimated.current) return; + + hasAnimated.current = true; + + const startTime = Date.now() + delay; + const endTime = startTime + duration; + + const animate = () => { + const now = Date.now(); + + if (now < startTime) { + requestAnimationFrame(animate); + return; + } + + if (now >= endTime) { + setCount(value); + return; + } + + const progress = (now - startTime) / duration; + const easeOutQuart = 1 - Math.pow(1 - progress, 4); + const currentValue = Math.floor(easeOutQuart * value); + + setCount(currentValue); + requestAnimationFrame(animate); + }; + + requestAnimationFrame(animate); + }, [isInView, value, duration, delay]); + + return ( + + {prefix}{count}{suffix} + + ); +} + +interface StatCardProps { + value: number; + label: string; + suffix?: string; + prefix?: string; + index?: number; +} + +export function StatCard({ value, label, suffix = '', prefix = '', index = 0 }: StatCardProps) { + return ( + +
+ +
+
+ {label} +
+
+ ); +}