SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
1.6 KB · 44 lines tsx
Raw Blame History
1'use client';23import { useEffect, useRef, useState } from 'react';4import { fmt } from '@/lib/format';56/**7 * Tweens between values ONLY when `value` changes after mount (a real update arrived). No idle motion.8 * The first render prints the SSR value verbatim so there is no hydration flicker.9 */10export function AnimatedNumber({ value, digits = 1, duration = 600, className = '', style, ...rest }: { value: number | null | undefined; digits?: number; duration?: number; className?: string; style?: React.CSSProperties } & React.AriaAttributes) {11  const [display, setDisplay] = useState<number | null | undefined>(value);12  const fromRef = useRef<number | null | undefined>(value);13  const rafRef = useRef<number | null>(null);1415  useEffect(() => {16    const from = fromRef.current;17    if (value == null || from == null || value === from || typeof window === 'undefined' || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {18      fromRef.current = value;19      setDisplay(value);20      return;21    }22    const start = performance.now();23    const f = from;24    const step = (t: number) => {25      const p = Math.min(1, (t - start) / duration);26      const e = 1 - (1 - p) ** 3; // ease-out cubic27      setDisplay(f + (value - f) * e);28      if (p < 1) rafRef.current = requestAnimationFrame(step);29      else fromRef.current = value;30    };31    rafRef.current = requestAnimationFrame(step);32    return () => {33      if (rafRef.current) cancelAnimationFrame(rafRef.current);34      fromRef.current = value;35    };36  }, [value, duration]);3738  return (39    <span className={`num ${className}`} style={style} {...rest}>40      {fmt(display, digits)}41    </span>42  );43}44