92 lines
2.2 KiB
TypeScript
92 lines
2.2 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useRef } from "react"
|
|
import { useInView } from "react-intersection-observer"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface CountUpAnimationProps {
|
|
end: number
|
|
start?: number
|
|
duration?: number
|
|
delay?: number
|
|
prefix?: string
|
|
suffix?: string
|
|
decimals?: number
|
|
className?: string
|
|
}
|
|
|
|
export function CountUpAnimation({
|
|
end,
|
|
start = 0,
|
|
duration = 2000,
|
|
delay = 0,
|
|
prefix = "",
|
|
suffix = "",
|
|
decimals = 0,
|
|
className,
|
|
}: CountUpAnimationProps) {
|
|
const [count, setCount] = useState(start)
|
|
const countRef = useRef<number>(start)
|
|
const [ref, inView] = useInView({
|
|
triggerOnce: true,
|
|
threshold: 0.1,
|
|
})
|
|
|
|
useEffect(() => {
|
|
let startTime: number | null = null
|
|
let animationFrame: number | null = null
|
|
|
|
// Only start animation when in view and after delay
|
|
if (!inView) return
|
|
|
|
const timeout = setTimeout(() => {
|
|
const animate = (timestamp: number) => {
|
|
if (!startTime) startTime = timestamp
|
|
const progress = Math.min((timestamp - startTime) / duration, 1)
|
|
|
|
// Use easeOutExpo for a nice deceleration effect
|
|
const easeOutExpo = 1 - Math.pow(2, -10 * progress)
|
|
const currentCount = start + (end - start) * easeOutExpo
|
|
|
|
countRef.current = currentCount
|
|
setCount(currentCount)
|
|
|
|
if (progress < 1) {
|
|
animationFrame = requestAnimationFrame(animate)
|
|
} else {
|
|
// Ensure we end exactly at the target number
|
|
countRef.current = end
|
|
setCount(end)
|
|
}
|
|
}
|
|
|
|
animationFrame = requestAnimationFrame(animate)
|
|
}, delay)
|
|
|
|
return () => {
|
|
if (timeout) clearTimeout(timeout)
|
|
if (animationFrame) cancelAnimationFrame(animationFrame)
|
|
}
|
|
}, [inView, start, end, duration, delay])
|
|
|
|
// Format the number with commas and decimals
|
|
const formattedCount = () => {
|
|
const value = Math.round(count * Math.pow(10, decimals)) / Math.pow(10, decimals)
|
|
return (
|
|
prefix +
|
|
value.toLocaleString(undefined, {
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals,
|
|
}) +
|
|
suffix
|
|
)
|
|
}
|
|
|
|
return (
|
|
<span ref={ref} className={cn("tabular-nums", className)}>
|
|
{formattedCount()}
|
|
</span>
|
|
)
|
|
}
|
|
|