mirror of
https://github.com/Since-3/since3-website.git
synced 2026-10-12 19:51:16 +00:00
49 lines
1.3 KiB
TypeScript
49 lines
1.3 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { animate } from "motion/react"
|
|
|
|
/**
|
|
* Number that rolls smoothly to its new value. React renders only the first
|
|
* value; every later update is written by the animation, so the text never
|
|
* flashes to the final value before rolling.
|
|
* `format` must be a stable function (defined at module level).
|
|
*/
|
|
export function AnimatedNumber({
|
|
value,
|
|
format,
|
|
className,
|
|
}: {
|
|
value: number
|
|
format: (value: number) => string
|
|
className?: string
|
|
}) {
|
|
const ref = useRef<HTMLSpanElement>(null)
|
|
const displayed = useRef(value)
|
|
const [initial] = useState(value)
|
|
|
|
useEffect(() => {
|
|
const el = ref.current
|
|
if (!el || displayed.current === value) return
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
displayed.current = value
|
|
el.textContent = format(value)
|
|
return
|
|
}
|
|
const controls = animate(displayed.current, value, {
|
|
duration: 0.7,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
onUpdate: (latest) => {
|
|
displayed.current = latest
|
|
el.textContent = format(latest)
|
|
},
|
|
})
|
|
return () => controls.stop()
|
|
}, [value, format])
|
|
|
|
return (
|
|
<span ref={ref} className={className}>
|
|
{format(initial)}
|
|
</span>
|
|
)
|
|
}
|