{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-04",
  "title": "Thinking visor",
  "description": "A thinking status with a small drawn helmet whose visor shows a voice meter of five bouncing bars; when the work is done the meter settles and the visor holds 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/mesh-tile.json",
    "https://ui.ndekocode.dev/r/motion-presets.json",
    "https://ui.ndekocode.dev/r/segments.json",
    "https://ui.ndekocode.dev/r/speech-envelope.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-04.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useRef } from 'react';\n\nimport { noiseAt } from '@/registry/ndeko/lib/speech-envelope';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport { DoneLayer, useDrawnStroke } from './_parts/done-mark';\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/**\n * The visor holds five bars, each a dot at rest, placed in hundredths of the mark: the drawing is\n * 24 units wide, the bars 1.5 units, centred 2.4 units apart on the visor's middle line.\n */\nconst BAR_LEFTS = ['26.875%', '36.875%', '46.875%', '56.875%', '66.875%'];\n\nconst BAR_TOP = '47.917%';\n\nconst BAR_SIZE = '6.25%';\n\n/** The tallest each bar kicks, from its resting dot: the visor narrows at its ends, so the end bars stay shorter. */\nconst BAR_REACH = [2.1, 2.7, 2.8, 2.6, 2.0];\n\n/** Each bar keeps its own rhythm, so the meter never moves as one block. */\nconst BAR_CYCLES_MS = [880, 1060, 940, 1140, 1000];\n\nconst BAR_DELAYS_MS = [0, 140, 60, 200, 100];\n\n/** Under reduced motion the visor rests on one reading of its meter. */\nconst STILL_SCALES = [1.5, 2.3, 1.8, 2.5, 1.3];\n\n/** A meter kicks up fast and falls back slowly, three times a cycle; the numbers place them in the cycle. */\nconst BEATS = [\n  { kick: 0.08, fall: 0.3 },\n  { kick: 0.4, fall: 0.62 },\n  { kick: 0.72, fall: 0.92 },\n];\n\n/** What is left of a kick once the bar has fallen back. */\nconst FALLEN_SHARE = 0.25;\n\nconst METER_SEED = 7;\n\nconst KICK_EASING = 'cubic-bezier(0.23, 1, 0.32, 1)';\n\nconst FALL_EASING = 'cubic-bezier(0.45, 0, 0.55, 1)';\n\nconst BARS = BAR_LEFTS.map((_, index) => index);\n\nconst REST = 'scaleY(1)';\n\nfunction scaleOf(scale: number) {\n  return `scaleY(${scale})`;\n}\n\nfunction meterKeyframesOf(index: number): Keyframe[] {\n  const reach = BAR_REACH[index] ?? 2;\n  const keyframes: Keyframe[] = [\n    { transform: REST, offset: 0, easing: KICK_EASING },\n  ];\n  for (const [beatIndex, beat] of BEATS.entries()) {\n    const noise = noiseAt(index * BEATS.length + beatIndex, METER_SEED);\n    const strength = 0.45 + 0.55 * noise;\n    const kicked = 1 + (reach - 1) * strength;\n    const fallen = 1 + (kicked - 1) * FALLEN_SHARE;\n    keyframes.push({\n      transform: scaleOf(kicked),\n      offset: beat.kick,\n      easing: FALL_EASING,\n    });\n    keyframes.push({\n      transform: scaleOf(fallen),\n      offset: beat.fall,\n      easing: KICK_EASING,\n    });\n  }\n  keyframes.push({ transform: REST, offset: 1 });\n  return keyframes;\n}\n\n/** The five bars of the visor bounce like a level meter, each on its own rhythm. */\nfunction meterLoopsOf(root: HTMLElement): Loop[] {\n  const bars = Array.from(root.querySelectorAll('[data-bar]'));\n  return bars.map((element, index) => ({\n    element,\n    keyframes: meterKeyframesOf(index),\n    timing: {\n      duration: BAR_CYCLES_MS[index] ?? 1000,\n      delay: BAR_DELAYS_MS[index] ?? 0,\n      iterations: Infinity,\n    },\n  }));\n}\n\nfunction barTransformOf(index: number, showsStill: boolean) {\n  if (!showsStill) {\n    return REST;\n  }\n  const scale = STILL_SCALES[index] ?? 1;\n  return scaleOf(scale);\n}\n\nfunction barStyleOf(index: number, showsStill: boolean) {\n  return {\n    left: BAR_LEFTS[index],\n    top: BAR_TOP,\n    width: BAR_SIZE,\n    height: BAR_SIZE,\n    transform: barTransformOf(index, showsStill),\n  };\n}\n\nfunction barsLayerOf(isDone: boolean) {\n  if (isDone) {\n    return 'opacity-0 duration-150';\n  }\n  return 'opacity-100 duration-200';\n}\n\nconst HELMET = (\n  <svg\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    strokeLinecap='round'\n    strokeLinejoin='round'\n    className='text-foreground absolute inset-0 size-full'\n  >\n    <path d='M3.5 19.5V12a8.5 8.5 0 0 1 17 0v7.5' strokeWidth={1.75} />\n    <path d='M2.75 19.5h18.5' strokeWidth={1.75} />\n    <rect\n      x={5.5}\n      y={8.75}\n      width={13}\n      height={7}\n      rx={3.5}\n      strokeWidth={1.5}\n      className='fill-foreground/[0.06]'\n    />\n  </svg>\n);\n\nfunction VisorCheck() {\n  const check = useDrawnStroke(0.28, 0.12);\n  return (\n    <DoneLayer>\n      <motion.path\n        d='M9.6 12.5 11.3 14.2 14.6 10.6'\n        strokeWidth={1.5}\n        {...check}\n      />\n    </DoneLayer>\n  );\n}\n\ntype VisorMarkProps = {\n  size: ThinkingSize;\n  isDone: boolean;\n  canLoop: boolean;\n  isPlaying: boolean;\n  isStill: boolean;\n};\n\nfunction VisorMark({\n  size,\n  isDone,\n  canLoop,\n  isPlaying,\n  isStill,\n}: VisorMarkProps) {\n  const barsRef = useRef<HTMLSpanElement>(null);\n  const showsStill = isStill && !isDone;\n\n  useLoops(barsRef, { canLoop, isPlaying, create: meterLoopsOf });\n\n  return (\n    <span\n      aria-hidden='true'\n      className={cn('relative shrink-0', MARK_BOX[size])}\n    >\n      {HELMET}\n      <span\n        ref={barsRef}\n        className={cn(\n          'absolute inset-0 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 absolute rounded-full'\n          />\n        ))}\n      </span>\n      <AnimatePresence initial={false}>\n        {isDone && <VisorCheck key='done' />}\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/**\n * A thinking status: a small helmet drawn in the text colour whose visor shows a voice meter, five\n * bars that bounce each on its own rhythm, beside a label a soft light passes over. When the work\n * is done the meter settles and the visor holds a check.\n */\nexport function ThinkingVisor({\n  label = DEFAULT_LABEL,\n  size = 'md',\n  isDone = false,\n  className,\n}: ThinkingIndicatorProps) {\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      className={className}\n    >\n      <VisorMark\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: 'Reviewer', series: 4 };\n\nconst STEPS = [\n  'Thinking',\n  'Listening to the Goma call',\n  'Noting the action items',\n];\n\nfunction renderVisor({ label, isDone }: IndicatorState) {\n  return <ThinkingVisor label={label} isDone={isDone} size='lg' />;\n}\n\nexport default function Loading04() {\n  return (\n    <ThinkingDemo\n      agent={AGENT}\n      steps={STEPS}\n      doneLabel='Notes ready'\n      renderIndicator={renderVisor}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/loading/loading-04.tsx"
    },
    {
      "path": "registry/ndeko/components/loading/_parts/done-mark.tsx",
      "content": "'use client';\n\nimport { motion, type Transition } from 'motion/react';\nimport type { ReactNode } 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';\n\nconst DRAWN = { pathLength: 1 };\n\nconst NOT_DRAWN = { pathLength: 0 };\n\nconst SHOWN = { opacity: 1 };\n\nconst HIDDEN = { opacity: 0 };\n\nconst LEAVING = { opacity: 0, transition: { duration: 0.1 } };\n\nconst FADE_IN: Transition = { duration: 0.2, ease: EASE_OUT_STRONG };\n\nfunction layerStartOf(shouldReduceMotion: boolean) {\n  if (shouldReduceMotion) {\n    return HIDDEN;\n  }\n  return SHOWN;\n}\n\ntype DoneLayerProps = {\n  /** Inside a 24 by 24 box, drawn in the text colour of success. */\n  children: ReactNode;\n  className?: string;\n};\n\n/**\n * The strokes a mark lands on when the work is done, over the whole mark. It leaves faster than it\n * came, and under reduced motion it fades in already drawn.\n */\nexport function DoneLayer({ children, className }: DoneLayerProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  return (\n    <motion.svg\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      initial={layerStartOf(shouldReduceMotion)}\n      animate={SHOWN}\n      exit={LEAVING}\n      transition={FADE_IN}\n      className={cn(\n        'text-success-foreground absolute inset-0 size-full',\n        className\n      )}\n    >\n      {children}\n    </motion.svg>\n  );\n}\n\n/** A stroke that draws itself from its start, after `delay` seconds; already drawn under reduced motion. */\nexport function useDrawnStroke(durationS: number, delay = 0) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const transition: Transition = {\n    duration: durationS,\n    delay,\n    ease: EASE_OUT_STRONG,\n  };\n  if (shouldReduceMotion) {\n    return { initial: DRAWN, animate: DRAWN, transition };\n  }\n  return { initial: NOT_DRAWN, animate: DRAWN, transition };\n}\n",
      "type": "registry:component",
      "target": "components/loading/_parts/done-mark.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"
}