source dump of claude code
at main 57 lines 1.9 kB view raw
1import { useContext, useEffect, useState } from 'react' 2import { ClockContext } from '../components/ClockContext.js' 3import type { DOMElement } from '../dom.js' 4import { useTerminalViewport } from './use-terminal-viewport.js' 5 6/** 7 * Hook for synchronized animations that pause when offscreen. 8 * 9 * Returns a ref to attach to the animated element and the current animation time. 10 * All instances share the same clock, so animations stay in sync. 11 * The clock only runs when at least one keepAlive subscriber exists. 12 * 13 * Pass `null` to pause — unsubscribes from the clock so no ticks fire. 14 * Time freezes at the last value and resumes from the current clock time 15 * when a number is passed again. 16 * 17 * @param intervalMs - How often to update, or null to pause 18 * @returns [ref, time] - Ref to attach to element, elapsed time in ms 19 * 20 * @example 21 * function Spinner() { 22 * const [ref, time] = useAnimationFrame(120) 23 * const frame = Math.floor(time / 120) % FRAMES.length 24 * return <Box ref={ref}>{FRAMES[frame]}</Box> 25 * } 26 * 27 * The clock automatically slows when the terminal is blurred, 28 * so consumers don't need to handle focus state. 29 */ 30export function useAnimationFrame( 31 intervalMs: number | null = 16, 32): [ref: (element: DOMElement | null) => void, time: number] { 33 const clock = useContext(ClockContext) 34 const [viewportRef, { isVisible }] = useTerminalViewport() 35 const [time, setTime] = useState(() => clock?.now() ?? 0) 36 37 const active = isVisible && intervalMs !== null 38 39 useEffect(() => { 40 if (!clock || !active) return 41 42 let lastUpdate = clock.now() 43 44 const onChange = (): void => { 45 const now = clock.now() 46 if (now - lastUpdate >= intervalMs!) { 47 lastUpdate = now 48 setTime(now) 49 } 50 } 51 52 // keepAlive: true — visible animations drive the clock 53 return clock.subscribe(onChange, true) 54 }, [clock, intervalMs, active]) 55 56 return [viewportRef, time] 57}