discourse/frontend/components/count-up-animation.tsx

92 lines
2.2 KiB
TypeScript
Raw Permalink Normal View History

2025-03-25 03:52:30 -04:00
"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>
)
}