( target: number, active: boolean, durationMs = 1100, )
| 117 | // fresh data lands). Reduced motion or no rAF resolves immediately to the |
| 118 | // target. |
| 119 | export const useCountUp = ( |
| 120 | target: number, |
| 121 | active: boolean, |
| 122 | durationMs = 1100, |
| 123 | ): number => { |
| 124 | const reducedMotion = usePrefersReducedMotion(); |
| 125 | const [value, setValue] = useState(0); |
| 126 | const valueRef = useRef(0); |
| 127 | const frameRef = useRef<number | null>(null); |
| 128 | |
| 129 | useEffect(() => { |
| 130 | if (!active) { |
| 131 | return undefined; |
| 132 | } |
| 133 | if ( |
| 134 | reducedMotion || |
| 135 | typeof window === 'undefined' || |
| 136 | typeof window.requestAnimationFrame !== 'function' |
| 137 | ) { |
| 138 | valueRef.current = target; |
| 139 | setValue(target); |
| 140 | return undefined; |
| 141 | } |
| 142 | |
| 143 | const from = valueRef.current; |
| 144 | const delta = target - from; |
| 145 | if (delta === 0) { |
| 146 | return undefined; |
| 147 | } |
| 148 | |
| 149 | const start = performance.now(); |
| 150 | const step = (now: number) => { |
| 151 | const progress = Math.min(1, (now - start) / durationMs); |
| 152 | const eased = 1 - (1 - progress) ** 3; |
| 153 | const next = Math.round(from + delta * eased); |
| 154 | valueRef.current = next; |
| 155 | setValue(next); |
| 156 | if (progress < 1) { |
| 157 | frameRef.current = window.requestAnimationFrame(step); |
| 158 | } |
| 159 | }; |
| 160 | frameRef.current = window.requestAnimationFrame(step); |
| 161 | |
| 162 | return () => { |
| 163 | if (frameRef.current) { |
| 164 | window.cancelAnimationFrame(frameRef.current); |
| 165 | } |
| 166 | }; |
| 167 | }, [active, target, durationMs, reducedMotion]); |
| 168 | |
| 169 | return value; |
| 170 | }; |
no test coverage detected