Rocky_Mountain_Vending/components/animated-number.tsx
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

94 lines
2.2 KiB
TypeScript

'use client'
import { useEffect, useRef, useState } from 'react'
interface AnimatedNumberProps {
value: number
duration?: number
className?: string
}
export function AnimatedNumber({ value, duration = 2000, className = '' }: AnimatedNumberProps) {
const [displayValue, setDisplayValue] = useState(0)
const [isVisible, setIsVisible] = useState(false)
const elementRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !isVisible) {
setIsVisible(true)
}
})
},
{ threshold: 0.1 }
)
if (elementRef.current) {
observer.observe(elementRef.current)
}
return () => {
if (elementRef.current) {
observer.unobserve(elementRef.current)
}
}
}, [isVisible])
useEffect(() => {
if (!isVisible || value === 0) {
setDisplayValue(0)
return
}
const startTime = Date.now()
const startValue = 0
const endValue = value
const animate = () => {
const now = Date.now()
const elapsed = now - startTime
const progress = Math.min(elapsed / duration, 1)
// Easing function for bounce effect (ease-out-bounce)
const easeOutBounce = (t: number): number => {
if (t < 1 / 2.75) {
return 7.5625 * t * t
} else if (t < 2 / 2.75) {
return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75
} else if (t < 2.5 / 2.75) {
return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375
} else {
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375
}
}
const easedProgress = easeOutBounce(progress)
const currentValue = Math.floor(startValue + (endValue - startValue) * easedProgress)
setDisplayValue(currentValue)
if (progress < 1) {
requestAnimationFrame(animate)
} else {
setDisplayValue(endValue)
}
}
const frameId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(frameId)
}, [isVisible, value, duration])
return (
<div ref={elementRef} className={className}>
{displayValue}
</div>
)
}