import React, { createContext, useEffect, useState } from 'react' import { FRAME_INTERVAL_MS } from '../core/constants.js' import { useTerminalFocus } from '../hooks/use-terminal-focus.js' export type Clock = { subscribe: (onChange: () => void, keepAlive: boolean) => () => void now: () => number setTickInterval: (ms: number) => void } export function createClock(tickIntervalMs: number): Clock { const subscribers = new Map<() => void, boolean>() let interval: ReturnType | null = null let currentTickIntervalMs = tickIntervalMs let startTime = 0 // Snapshot of the current tick's time, ensuring all subscribers in the same // tick see the same value (keeps animations synchronized) let tickTime = 0 function tick(): void { tickTime = Date.now() - startTime for (const onChange of subscribers.keys()) { onChange() } } function updateInterval(): void { const anyKeepAlive = [...subscribers.values()].some(Boolean) if (anyKeepAlive) { if (interval) { clearInterval(interval) interval = null } if (startTime === 0) { startTime = Date.now() } interval = setInterval(tick, currentTickIntervalMs) } else if (interval) { clearInterval(interval) interval = null } } return { subscribe(onChange, keepAlive) { subscribers.set(onChange, keepAlive) updateInterval() return () => { subscribers.delete(onChange) updateInterval() } }, now() { if (startTime === 0) { startTime = Date.now() } // When the clock interval is running, return the synchronized tickTime // so all subscribers in the same tick see the same value. // When paused (no keepAlive subscribers), return real-time to avoid // returning a stale tickTime from the last tick before the pause. if (interval && tickTime) { return tickTime } return Date.now() - startTime }, setTickInterval(ms) { if (ms === currentTickIntervalMs) return currentTickIntervalMs = ms updateInterval() }, } } export const ClockContext = createContext(null) const BLURRED_TICK_INTERVAL_MS = FRAME_INTERVAL_MS * 2 // Own component so App.tsx doesn't re-render when the clock is created. // The clock value is stable (created once via useState), so the provider // never causes consumer re-renders on its own. export function ClockProvider({ children, }: { children: React.ReactNode }): React.ReactNode { const [clock] = useState(() => createClock(FRAME_INTERVAL_MS)) const focused = useTerminalFocus() useEffect(() => { clock.setTickInterval( focused ? FRAME_INTERVAL_MS : BLURRED_TICK_INTERVAL_MS, ) }, [clock, focused]) return {children} }