| import { useMemo } from 'react' | |
| import { stringWidth } from '../../ink/stringWidth.js' | |
| import { type DOMElement, useAnimationFrame } from '../../ink.js' | |
| import type { SpinnerMode } from './types.js' | |
| export function useShimmerAnimation( | |
| mode: SpinnerMode, | |
| message: string, | |
| isStalled: boolean, | |
| ): [ref: (element: DOMElement | null) => void, glimmerIndex: number] { | |
| const glimmerSpeed = mode === 'requesting' ? 50 : 200 | |
| // Pass null when stalled to unsubscribe from the clock — otherwise the | |
| // setInterval keeps firing at 20fps even when the shimmer isn't visible. | |
| // Notably, if the caller never attaches `ref` (e.g. conditional JSX), | |
| // useTerminalViewport stays at its initial isVisible:true and the | |
| // viewport-pause never kicks in, so this is the only stop mechanism. | |
| const [ref, time] = useAnimationFrame(isStalled ? null : glimmerSpeed) | |
| const messageWidth = useMemo(() => stringWidth(message), [message]) | |
| if (isStalled) { | |
| return [ref, -100] | |
| } | |
| const cyclePosition = Math.floor(time / glimmerSpeed) | |
| const cycleLength = messageWidth + 20 | |
| if (mode === 'requesting') { | |
| return [ref, (cyclePosition % cycleLength) - 10] | |
| } | |
| return [ref, messageWidth + 10 - (cyclePosition % cycleLength)] | |
| } | |