{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-02",
  "title": "Thinking wave",
  "description": "A thinking status as five rounded bars that swell one after the other, a small wave that breathes, with a compact variant without the label; when the work is done the bars settle into a check.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "https://ui.ndekocode.dev/r/drawn-check.json",
    "https://ui.ndekocode.dev/r/mesh-tile.json",
    "https://ui.ndekocode.dev/r/motion-presets.json",
    "https://ui.ndekocode.dev/r/segments.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-agent-run.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-kept-height.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/words.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/loading/loading-02.tsx",
      "content": "'use client';\n\nimport { AnimatePresence } from 'motion/react';\nimport { useRef } from 'react';\n\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { DrawnCheck } from '@/registry/ndeko/ui/drawn-check';\n\nimport type { Loop } from './_parts/loops';\nimport {\n  ThinkingDemo,\n  type IndicatorState,\n  type ThinkingDemoAgent,\n} from './_parts/thinking-demo';\nimport {\n  DEFAULT_LABEL,\n  MARK_BOX,\n  ThinkingStatus,\n  type ThinkingIndicatorProps,\n  type ThinkingSize,\n} from './_parts/thinking-status';\nimport { useLoopGate, useLoops } from './_parts/use-loops';\n\n/** How tall each bar grows at the top of its swell, from its resting pill: the middle one most. */\nconst PEAKS = [2.3, 2.9, 3.1, 2.8, 2.2];\n\n/** Under reduced motion the wave rests on one moment of its swell. */\nconst STILL_SCALES = [1.5, 2.6, 3.1, 2.1, 1.3];\n\nconst SWELL_MS = 1150;\n\nconst BAR_STEP_MS = 120;\n\nconst RISE_EASING = 'cubic-bezier(0.33, 0, 0.2, 1)';\n\nconst FALL_EASING = 'cubic-bezier(0.45, 0, 0.55, 1)';\n\nconst BARS = PEAKS.map((_, index) => index);\n\nconst REST = 'scaleY(1)';\n\nfunction swellKeyframesOf(peak: number): Keyframe[] {\n  return [\n    { transform: REST, offset: 0, easing: RISE_EASING },\n    { transform: `scaleY(${peak})`, offset: 0.3, easing: FALL_EASING },\n    { transform: REST, offset: 0.68 },\n    { transform: REST, offset: 1 },\n  ];\n}\n\n/** The bars rise and fall one after the other, from the first to the last, then breathe out together. */\nfunction swellLoopsOf(root: HTMLElement): Loop[] {\n  const bars = Array.from(root.querySelectorAll('[data-bar]'));\n  return bars.map((element, index) => ({\n    element,\n    keyframes: swellKeyframesOf(PEAKS[index] ?? 1),\n    timing: {\n      duration: SWELL_MS,\n      delay: index * BAR_STEP_MS,\n      iterations: Infinity,\n    },\n  }));\n}\n\nfunction barStyleOf(index: number, showsStill: boolean) {\n  if (!showsStill) {\n    return { transform: REST };\n  }\n  const scale = STILL_SCALES[index] ?? 1;\n  return { transform: `scaleY(${scale})` };\n}\n\nfunction barsLayerOf(isDone: boolean) {\n  if (isDone) {\n    return 'opacity-0 duration-150';\n  }\n  return 'opacity-100 duration-200';\n}\n\ntype WaveMarkProps = {\n  size: ThinkingSize;\n  isDone: boolean;\n  canLoop: boolean;\n  isPlaying: boolean;\n  isStill: boolean;\n};\n\nfunction WaveMark({\n  size,\n  isDone,\n  canLoop,\n  isPlaying,\n  isStill,\n}: WaveMarkProps) {\n  const barsRef = useRef<HTMLSpanElement>(null);\n  const showsStill = isStill && !isDone;\n\n  useLoops(barsRef, { canLoop, isPlaying, create: swellLoopsOf });\n\n  return (\n    <span\n      aria-hidden='true'\n      className={cn(\n        'relative grid shrink-0 place-items-center',\n        MARK_BOX[size]\n      )}\n    >\n      <span\n        ref={barsRef}\n        className={cn(\n          'absolute inset-0 flex items-center justify-between transition-opacity ease-(--ease-out-strong)',\n          barsLayerOf(isDone)\n        )}\n      >\n        {BARS.map((index) => (\n          <span\n            key={index}\n            data-bar=''\n            style={barStyleOf(index, showsStill)}\n            className='bg-primary h-[32%] w-[13%] rounded-full'\n          />\n        ))}\n      </span>\n      <AnimatePresence initial={false}>\n        {isDone && (\n          <DrawnCheck key='done' tone='success' className='size-full' />\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n\ntype ThinkingWaveProps = ThinkingIndicatorProps & {\n  /** Only the wave, for a header or a button; the label is still read by screen readers. */\n  isCompact?: boolean;\n};\n\n/**\n * A thinking status: five rounded bars swell one after the other, a small wave that breathes,\n * beside a label a soft light passes over. When the work is done the bars settle and a check\n * draws itself in their place.\n */\nexport function ThinkingWave({\n  label = DEFAULT_LABEL,\n  size = 'md',\n  isDone = false,\n  isCompact = false,\n  className,\n}: ThinkingWaveProps) {\n  const { ref, canLoop, isPlaying, isStill } = useLoopGate(!isDone);\n\n  return (\n    <ThinkingStatus\n      ref={ref}\n      label={label}\n      size={size}\n      isDone={isDone}\n      canLoop={canLoop}\n      isPlaying={isPlaying}\n      showsLabel={!isCompact}\n      className={className}\n    >\n      <WaveMark\n        size={size}\n        isDone={isDone}\n        canLoop={canLoop}\n        isPlaying={isPlaying}\n        isStill={isStill}\n      />\n    </ThinkingStatus>\n  );\n}\n\nconst AGENT: ThinkingDemoAgent = { name: 'Writer', series: 2 };\n\nconst STEPS = ['Thinking', 'Reading the thread', 'Drafting a reply'];\n\nfunction renderWave({ label, isDone }: IndicatorState) {\n  return <ThinkingWave label={label} isDone={isDone} size='lg' />;\n}\n\n/** The compact wave after the agent's name, as a list of conversations would show it. */\nfunction renderCompactWave({ label, isDone }: IndicatorState) {\n  return (\n    <span aria-hidden='true' className='flex'>\n      <ThinkingWave label={label} isDone={isDone} size='sm' isCompact />\n    </span>\n  );\n}\n\nexport default function Loading02() {\n  return (\n    <ThinkingDemo\n      agent={AGENT}\n      steps={STEPS}\n      doneLabel='Reply ready'\n      renderIndicator={renderWave}\n      renderBesideName={renderCompactWave}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/loading/loading-02.tsx"
    },
    {
      "path": "registry/ndeko/components/loading/_parts/loops.ts",
      "content": "export type Loop = {\n  element: Element;\n  keyframes: Keyframe[];\n  timing: KeyframeAnimationOptions;\n};\n\nconst LAND_MS = 220;\n\nconst LAND_EASING = 'cubic-bezier(0.23, 1, 0.32, 1)';\n\n/**\n * Each loop runs in the browser's own animation engine on transform and opacity only, so the\n * compositor plays it and the page does no work per frame.\n */\nexport function startLoops(loops: readonly Loop[]) {\n  return loops.map((loop) => loop.element.animate(loop.keyframes, loop.timing));\n}\n\n/**\n * A cancelled animation is left alone: playing or even pausing it would bring it back and hold its\n * element in a pose its mark has already left.\n */\nfunction isCancelled(animation: Animation) {\n  return animation.playState === 'idle';\n}\n\nexport function playAll(animations: readonly Animation[]) {\n  for (const animation of animations) {\n    if (!isCancelled(animation)) {\n      animation.play();\n    }\n  }\n}\n\nexport function pauseAll(animations: readonly Animation[]) {\n  for (const animation of animations) {\n    if (!isCancelled(animation)) {\n      animation.pause();\n    }\n  }\n}\n\nexport function cancelAll(animations: readonly Animation[]) {\n  for (const animation of animations) {\n    animation.cancel();\n  }\n}\n\nfunction targetOf(animation: Animation) {\n  const effect = animation.effect;\n  if (effect instanceof KeyframeEffect) {\n    return effect.target;\n  }\n  return null;\n}\n\nfunction poseOf(element: Element): Keyframe {\n  const styles = getComputedStyle(element);\n  return { transform: styles.transform, opacity: styles.opacity };\n}\n\n/** From the pose its loop left it in, an element glides back to its own resting style, then lets go. */\nfunction land(animation: Animation) {\n  const target = targetOf(animation);\n  if (!target?.isConnected) {\n    animation.cancel();\n    return;\n  }\n  const shown = poseOf(target);\n  animation.cancel();\n  const rest = poseOf(target);\n  target.animate([shown, rest], { duration: LAND_MS, easing: LAND_EASING });\n}\n\nexport function landAll(animations: readonly Animation[]) {\n  for (const animation of animations) {\n    land(animation);\n  }\n}\n",
      "type": "registry:component",
      "target": "components/loading/_parts/loops.ts"
    },
    {
      "path": "registry/ndeko/components/loading/_parts/shimmer-label.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion, type Transition } from 'motion/react';\nimport { Fragment, useRef } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { EASE_OUT_STRONG } from '@/registry/ndeko/lib/motion-presets';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { leadingSpaceOf, wordsOf } from '@/registry/ndeko/lib/words';\n\nimport type { Loop } from './loops';\nimport { useLoops } from './use-loops';\n\nconst LETTER_STEP_MS = 70;\n\nconst GLINT_RISE_MS = 140;\n\nconst GLINT_FALL_MS = 260;\n\n/** How far the light lifts a letter towards the text colour: a soft shimmer, short of a flash. */\nconst GLINT_PEAK = 0.85;\n\nconst PASS_REST_MS = 900;\n\nconst SINE_EASING = 'cubic-bezier(0.37, 0, 0.63, 1)';\n\nconst ENTER: Transition = { duration: 0.2, ease: EASE_OUT_STRONG };\n\nconst EXIT: Transition = { duration: 0.12, ease: EASE_OUT_STRONG };\n\nconst ARRIVING = { opacity: 0, y: 4, filter: 'blur(2px)' };\n\nconst SHOWN = { opacity: 1, y: 0, filter: 'blur(0px)', transition: ENTER };\n\nconst LEAVING = { opacity: 0, y: -4, filter: 'blur(2px)', transition: EXIT };\n\nconst FADING_IN = { opacity: 0 };\n\nconst FADED_IN = { opacity: 1, transition: ENTER };\n\nconst FADING_OUT = { opacity: 0, transition: EXIT };\n\nfunction presenceOf(shouldReduceMotion: boolean) {\n  if (shouldReduceMotion) {\n    return { initial: FADING_IN, animate: FADED_IN, exit: FADING_OUT };\n  }\n  return { initial: ARRIVING, animate: SHOWN, exit: LEAVING };\n}\n\nfunction periodOf(letterCount: number) {\n  const sweep = letterCount * LETTER_STEP_MS;\n  const glint = GLINT_RISE_MS + GLINT_FALL_MS;\n  return sweep + glint + PASS_REST_MS;\n}\n\nfunction glintKeyframesOf(periodMs: number): Keyframe[] {\n  const litAt = GLINT_RISE_MS / periodMs;\n  const goneAt = (GLINT_RISE_MS + GLINT_FALL_MS) / periodMs;\n  return [\n    { opacity: 0, offset: 0, easing: SINE_EASING },\n    { opacity: GLINT_PEAK, offset: litAt, easing: SINE_EASING },\n    { opacity: 0, offset: goneAt },\n    { opacity: 0, offset: 1 },\n  ];\n}\n\n/** A soft light passes over the letters one after the other: each one brightens towards the text colour and fades back. */\nfunction glintLoopsOf(root: HTMLElement): Loop[] {\n  const glints = Array.from(root.querySelectorAll('[data-glint]'));\n  const periodMs = periodOf(glints.length);\n  const keyframes = glintKeyframesOf(periodMs);\n  return glints.map((element, index) => ({\n    element,\n    keyframes,\n    timing: {\n      duration: periodMs,\n      delay: index * LETTER_STEP_MS,\n      iterations: Infinity,\n    },\n  }));\n}\n\ntype WordProps = {\n  word: string;\n};\n\n/** A word never breaks inside itself; each letter carries its own light over it. */\nfunction Word({ word }: WordProps) {\n  const letters = Array.from(word);\n  return (\n    <span className='whitespace-nowrap'>\n      {letters.map((letter, index) => (\n        <span key={index} className='relative inline-block'>\n          {letter}\n          <span\n            data-glint=''\n            className='text-foreground pointer-events-none absolute inset-0 opacity-0 select-none'\n          >\n            {letter}\n          </span>\n        </span>\n      ))}\n    </span>\n  );\n}\n\ntype ShimmerTextProps = {\n  text: string;\n  canLoop: boolean;\n  isPlaying: boolean;\n};\n\nfunction ShimmerText({ text, canLoop, isPlaying }: ShimmerTextProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const words = wordsOf(text);\n\n  useLoops(ref, { canLoop, isPlaying, create: glintLoopsOf });\n\n  return (\n    <span ref={ref} className='block'>\n      {words.map((word, index) => (\n        <Fragment key={index}>\n          {leadingSpaceOf(index)}\n          <Word word={word} />\n        </Fragment>\n      ))}\n    </span>\n  );\n}\n\nfunction toneOf(isDone: boolean) {\n  if (isDone) {\n    return 'text-foreground';\n  }\n  return 'text-muted-foreground';\n}\n\ntype ShimmerLabelProps = {\n  text: string;\n  isDone: boolean;\n  canLoop: boolean;\n  isPlaying: boolean;\n  className?: string;\n};\n\n/**\n * The words of a status, muted while the work runs, with a light that passes over them letter by\n * letter (opacity only), and in the text colour once it is done. A new label rises in as the old\n * one leaves. The words are drawn for the eye: the status that holds them says them once.\n */\nexport function ShimmerLabel({\n  text,\n  isDone,\n  canLoop,\n  isPlaying,\n  className,\n}: ShimmerLabelProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const presence = presenceOf(shouldReduceMotion);\n\n  return (\n    <span\n      aria-hidden='true'\n      className={cn(\n        'relative block min-w-0 transition-colors duration-200',\n        toneOf(isDone),\n        className\n      )}\n    >\n      <AnimatePresence mode='popLayout' initial={false}>\n        <motion.span key={text} className='block' {...presence}>\n          <ShimmerText text={text} canLoop={canLoop} isPlaying={isPlaying} />\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/loading/_parts/shimmer-label.tsx"
    },
    {
      "path": "registry/ndeko/components/loading/_parts/thinking-demo.tsx",
      "content": "'use client';\n\nimport { useState, type ReactNode } from 'react';\n\nimport {\n  useAutoplay,\n  type AutoplayStep,\n} from '@/registry/ndeko/hooks/use-autoplay';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { seriesColorOf, type SeriesSlot } from '@/registry/ndeko/lib/agent-run';\nimport { hiddenIf } from '@/registry/ndeko/lib/demonstration';\nimport { MeshTile } from '@/registry/ndeko/ui/mesh-tile';\nimport { Segments, type Segment } from '@/registry/ndeko/ui/segments';\n\ntype Phase = 'thinking' | 'done';\n\nconst PHASES: Segment<Phase>[] = [\n  { value: 'thinking', label: 'Thinking' },\n  { value: 'done', label: 'Done' },\n];\n\nconst LABEL_HOLD_MS = 1800;\n\nconst DONE_HOLD_MS = 2400;\n\nexport type ThinkingDemoAgent = {\n  name: string;\n  series: SeriesSlot;\n};\n\nexport type IndicatorState = {\n  label: string;\n  isDone: boolean;\n};\n\ntype ThinkingDemoProps = {\n  agent: ThinkingDemoAgent;\n  /** The labels the work goes through; the first one shows when it starts. */\n  steps: readonly string[];\n  doneLabel: string;\n  renderIndicator: (state: IndicatorState) => ReactNode;\n  /** A second mark after the agent's name, for a variant shown in a header. */\n  renderBesideName?: (state: IndicatorState) => ReactNode;\n};\n\nfunction phaseOf(isDone: boolean): Phase {\n  if (isDone) {\n    return 'done';\n  }\n  return 'thinking';\n}\n\nfunction labelOf(\n  steps: readonly string[],\n  stepIndex: number,\n  isDone: boolean,\n  doneLabel: string\n) {\n  if (isDone) {\n    return doneLabel;\n  }\n  return steps[stepIndex] ?? '';\n}\n\n/**\n * An agent's turn in a conversation: its name, then its status. The demonstration walks the work\n * through its labels, lands it on done, holds, and starts again; the switch under the card lets a\n * person do the same.\n */\nexport function ThinkingDemo({\n  agent,\n  steps,\n  doneLabel,\n  renderIndicator,\n  renderBesideName,\n}: ThinkingDemoProps) {\n  const [stepIndex, setStepIndex] = useState(0);\n  const [isDone, setIsDone] = useState(false);\n  const label = labelOf(steps, stepIndex, isDone, doneLabel);\n  const state: IndicatorState = { label, isDone };\n\n  function startOver() {\n    setStepIndex(0);\n    setIsDone(false);\n  }\n\n  function choose(phase: Phase) {\n    if (phase === 'done') {\n      setIsDone(true);\n      return;\n    }\n    startOver();\n  }\n\n  const nextLabels: AutoplayStep[] = steps.slice(1).map((_, index) => ({\n    after: LABEL_HOLD_MS,\n    run: () => setStepIndex(index + 1),\n  }));\n  const autoplaySteps: AutoplayStep[] = [\n    ...nextLabels,\n    { after: LABEL_HOLD_MS, run: () => setIsDone(true) },\n  ];\n  const { ref, isAutoplaying } = useAutoplay(autoplaySteps, {\n    loopAfter: DONE_HOLD_MS,\n    onReset: startOver,\n  });\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex w-full max-w-[22rem] flex-col items-center gap-3'\n    >\n      <div className='bg-card hairline-ring ring-border flex w-full flex-col gap-2 rounded-xl p-4 shadow-[0_1px_2px_rgb(0_0_0/0.04)]'>\n        <div className='flex min-w-0 items-center gap-2'>\n          <MeshTile seed={agent.name} color={seriesColorOf(agent.series)} />\n          <p className='min-w-0 truncate text-[13px] font-medium'>\n            {agent.name}\n          </p>\n          {renderBesideName?.(state)}\n        </div>\n        <div aria-hidden={hiddenIf(isAutoplaying)} className='min-w-0 ps-7'>\n          {renderIndicator(state)}\n        </div>\n      </div>\n      <Segments\n        value={phaseOf(isDone)}\n        segments={PHASES}\n        onValueChange={choose}\n        label='Indicator state'\n        className='w-full max-w-[13rem]'\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/loading/_parts/thinking-demo.tsx"
    },
    {
      "path": "registry/ndeko/components/loading/_parts/thinking-status.tsx",
      "content": "'use client';\n\nimport type { ReactNode, Ref } from 'react';\n\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport { ShimmerLabel } from './shimmer-label';\n\nexport type ThinkingSize = 'sm' | 'md' | 'lg';\n\nexport type ThinkingIndicatorProps = {\n  /** What the agent is doing, beside the mark and read by screen readers: Thinking, Reading the brief. */\n  label?: string;\n  /** `sm` sits in a line of small text, `md` in a message, `lg` alone. */\n  size?: ThinkingSize;\n  /** The work is over: the mark lands on its done state and the label stops shimmering. */\n  isDone?: boolean;\n  className?: string;\n};\n\nexport const DEFAULT_LABEL = 'Thinking';\n\n/** The mark is as tall as one line of its label, so a label that wraps keeps the mark on its first line. */\nexport const MARK_BOX: Record<ThinkingSize, string> = {\n  sm: 'size-4',\n  md: 'size-5',\n  lg: 'size-6',\n};\n\nconst LABEL_TYPE: Record<ThinkingSize, string> = {\n  sm: 'text-xs leading-4',\n  md: 'text-[13px] leading-5',\n  lg: 'text-sm leading-6',\n};\n\nconst ROW_GAP: Record<ThinkingSize, string> = {\n  sm: 'gap-1.5',\n  md: 'gap-2',\n  lg: 'gap-2.5',\n};\n\ntype ThinkingStatusProps = {\n  ref: Ref<HTMLSpanElement>;\n  label: string;\n  size: ThinkingSize;\n  isDone: boolean;\n  canLoop: boolean;\n  isPlaying: boolean;\n  /** False for a mark shown alone: the label is still read by screen readers. */\n  showsLabel?: boolean;\n  className?: string;\n  /** The mark, hidden from screen readers. */\n  children: ReactNode;\n};\n\n/** A mark and its label in one status, so a change of label or the end of the work is said once. */\nexport function ThinkingStatus({\n  ref,\n  label,\n  size,\n  isDone,\n  canLoop,\n  isPlaying,\n  showsLabel = true,\n  className,\n  children,\n}: ThinkingStatusProps) {\n  return (\n    <span\n      ref={ref}\n      role='status'\n      className={cn(\n        'inline-flex max-w-full min-w-0 items-start align-top',\n        ROW_GAP[size],\n        className\n      )}\n    >\n      {children}\n      <span className='sr-only'>{label}</span>\n      {showsLabel && (\n        <ShimmerLabel\n          text={label}\n          isDone={isDone}\n          canLoop={canLoop}\n          isPlaying={isPlaying}\n          className={LABEL_TYPE[size]}\n        />\n      )}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/loading/_parts/thinking-status.tsx"
    },
    {
      "path": "registry/ndeko/components/loading/_parts/use-loops.ts",
      "content": "'use client';\n\nimport { useInView } from 'motion/react';\nimport { useEffect, useEffectEvent, useRef, type RefObject } from 'react';\n\nimport { useDocumentVisible } from '@/registry/ndeko/hooks/use-agent-run';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\n\nimport {\n  cancelAll,\n  landAll,\n  pauseAll,\n  playAll,\n  startLoops,\n  type Loop,\n} from './loops';\n\n/**\n * Whether an indicator may loop and whether it plays right now. It loops while it runs and the\n * reduced motion setting is off; it plays only while it is on screen in the tab that is shown.\n */\nexport function useLoopGate<Root extends HTMLElement = HTMLSpanElement>(\n  isRunning: boolean\n) {\n  const ref = useRef<Root>(null);\n  const isInView = useInView(ref);\n  const isTabVisible = useDocumentVisible();\n  const isStill = usePrefersReducedMotion();\n  const canLoop = isRunning && !isStill;\n  const isOnScreen = isInView && isTabVisible;\n  const isPlaying = canLoop && isOnScreen;\n\n  return { ref, canLoop, isPlaying, isStill };\n}\n\ntype LoopsOptions<Root> = {\n  canLoop: boolean;\n  isPlaying: boolean;\n  create: (root: Root) => Loop[];\n};\n\n/**\n * The loops of a mark, made when it starts running and paused whenever it is not seen, so they ask\n * for no frame off screen or in a hidden tab. When it stops running each element glides back to\n * its resting style and the loops are gone, so a finished mark costs nothing.\n */\nexport function useLoops<Root extends HTMLElement>(\n  rootRef: RefObject<Root | null>,\n  { canLoop, isPlaying, create }: LoopsOptions<Root>\n) {\n  const running = useRef<Animation[] | null>(null);\n  const isStill = usePrefersReducedMotion();\n\n  const loopsOf = useEffectEvent((root: Root) => {\n    const loops = create(root);\n    return startLoops(loops);\n  });\n\n  /** Stopped by the reduced motion setting, a mark goes straight to rest instead of gliding there. */\n  const finish = useEffectEvent((animations: Animation[], root: Root) => {\n    const canGlide = root.isConnected && !isStill;\n    if (canGlide) {\n      landAll(animations);\n      return;\n    }\n    cancelAll(animations);\n  });\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root || !canLoop) {\n      return;\n    }\n    const animations = loopsOf(root);\n    pauseAll(animations);\n    running.current = animations;\n    return () => {\n      running.current = null;\n      finish(animations, root);\n    };\n  }, [rootRef, canLoop]);\n\n  useEffect(() => {\n    const animations = running.current;\n    if (!animations || !isPlaying) {\n      return;\n    }\n    playAll(animations);\n    return () => pauseAll(animations);\n  }, [canLoop, isPlaying]);\n}\n",
      "type": "registry:component",
      "target": "components/loading/_parts/use-loops.ts"
    }
  ],
  "categories": [
    "components",
    "loading"
  ],
  "type": "registry:component"
}