/ components / Spinner / useShimmerAnimation.ts
useShimmerAnimation.ts
 1  import { useMemo } from 'react'
 2  import { stringWidth } from '../../ink/stringWidth.js'
 3  import { type DOMElement, useAnimationFrame } from '../../ink.js'
 4  import type { SpinnerMode } from './types.js'
 5  
 6  export function useShimmerAnimation(
 7    mode: SpinnerMode,
 8    message: string,
 9    isStalled: boolean,
10  ): [ref: (element: DOMElement | null) => void, glimmerIndex: number] {
11    const glimmerSpeed = mode === 'requesting' ? 50 : 200
12    // Pass null when stalled to unsubscribe from the clock — otherwise the
13    // setInterval keeps firing at 20fps even when the shimmer isn't visible.
14    // Notably, if the caller never attaches `ref` (e.g. conditional JSX),
15    // useTerminalViewport stays at its initial isVisible:true and the
16    // viewport-pause never kicks in, so this is the only stop mechanism.
17    const [ref, time] = useAnimationFrame(isStalled ? null : glimmerSpeed)
18    const messageWidth = useMemo(() => stringWidth(message), [message])
19  
20    if (isStalled) {
21      return [ref, -100]
22    }
23  
24    const cyclePosition = Math.floor(time / glimmerSpeed)
25    const cycleLength = messageWidth + 20
26  
27    if (mode === 'requesting') {
28      return [ref, (cyclePosition % cycleLength) - 10]
29    }
30    return [ref, messageWidth + 10 - (cyclePosition % cycleLength)]
31  }