32 lines
1.2 KiB
TypeScript
32 lines
1.2 KiB
TypeScript
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)]
|
|
}
|