Files
agency/.pnpm-store/v3/files/8d/209ae9ab56b39693ed7b9157ecbc20e18f5ca541faaa585d2276e41a8ea5e9a24a8840fd5c8f3bf0c4a9e36a9dac64d59cf71db385f8e2563ed03439f0b274
hanzo-agent 5af6960b87 run project
Run: sess_f37640604ae254cb4d11dc6217f7b721
2026-09-30 00:04:56 +00:00

55 lines
1.8 KiB
Plaintext

import { useEffect, useRef, useState } from 'react'
export interface UseCountUpOptions {
/** Animation duration in ms. Default 1200. Set 0 to disable. */
duration?: number
/** Easing function. Default cubic ease-out. */
ease?: (t: number) => number
/** Re-trigger on `target` changes. Default true. */
retrigger?: boolean
}
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3)
/**
* Smoothly animates from 0 (or the previous value) up to `target`. Honors
* `prefers-reduced-motion` and a `duration: 0` opt-out.
*/
export function useCountUp(target: number | null, opts: UseCountUpOptions = {}): number {
const {
duration = 1200,
ease = easeOutCubic,
retrigger = true,
} = opts
const [value, setValue] = useState(0)
const raf = useRef<number | null>(null)
const prev = useRef(0)
useEffect(() => {
if (target === null || target === undefined) return
if (duration <= 0) { setValue(target); prev.current = target; return }
const reduce = typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
if (reduce) { setValue(target); prev.current = target; return }
const from = retrigger ? prev.current : value
const to = target
if (from === to) return
const start = performance.now()
const step = (now: number) => {
const t = Math.min(1, (now - start) / duration)
const v = from + (to - from) * ease(t)
setValue(Math.round(v))
if (t < 1) raf.current = requestAnimationFrame(step)
else { prev.current = to; raf.current = null }
}
raf.current = requestAnimationFrame(step)
return () => { if (raf.current) cancelAnimationFrame(raf.current) }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, duration])
return value
}