{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-ticker",
  "title": "Caption Ticker",
  "description": "An animated caption ticker for React and shadcn, stepping short captions in under a scene one line at a time.",
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-text-swap.json",
    "https://eased.dev/r/eased-demo-core.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/caption-ticker/caption-ticker.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { TextSwap } from \"../_primitives/text-swap/text-swap\";\nimport { useDemoTimeline } from \"../_primitives/demo-core/demo-core\";\n\n/** A line, or a line and how long it stays up on its own clock, in ms. */\nexport type Caption = string | { text: string; duration?: number };\n\nexport type CaptionTickerProps = {\n  /** Short lines, one at a time. Keep each to a few words: a caption never wraps. */\n  captions: readonly Caption[];\n  /**\n   * Show this line, for a ticker driven by a scene's timeline. `null` shows none. Leave it out\n   * and the ticker steps through the lines on its own, each for its `duration`.\n   */\n  index?: number | null;\n  /** On its own clock, start again after the last line. Default true. */\n  loop?: boolean;\n  /** Hold the current line on its own clock, e.g. from the scene's Pause control (rule 85). Default false. */\n  paused?: boolean;\n  /** How long a line without a `duration` stays up, in ms. Default 2400. */\n  duration?: number;\n  /** Called as its own clock moves to a line, so what the caption describes can follow it. */\n  onIndexChange?: (index: number) => void;\n  className?: string;\n};\n\n/**\n * Captions that step in under a scene, one short line at a time, with Text Swap's motion: the\n * old line lifts and blurs out, the new one rises in, and the box springs to its width. It\n * runs on its own clock (a timing per line, paused off-screen) or follows `index` from a\n * scene's timeline. Screen readers get every line at once as a list, so a loop never repeats\n * itself at them. Under reduced motion lines cross-fade, and the ticker's own clock rests on\n * the last line.\n */\nexport function CaptionTicker({ captions, index, loop = true, paused = false, duration = 2400, onIndexChange, className = \"\" }: CaptionTickerProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const lines = useMemo(\n    () => captions.map((c) => (typeof c === \"string\" ? { text: c, duration } : { text: c.text, duration: c.duration ?? duration })),\n    [captions, duration],\n  );\n  const steps = useMemo(() => lines.map((l, i) => ({ id: String(i), duration: l.duration })), [lines]);\n  const own = useDemoTimeline(steps, {\n    loop,\n    autoplay: index === undefined && !paused,\n    delay: 0,\n    observe: root,\n    onStep: (_, i) => onIndexChange?.(i),\n  });\n  const { play, pause } = own;\n  // Only a change of `paused` acts; the first render is autoplay's to decide.\n  const wasPaused = useRef(paused);\n  useEffect(() => {\n    if (wasPaused.current === paused) return;\n    wasPaused.current = paused;\n    if (index !== undefined) return;\n    if (paused) pause();\n    else play();\n  }, [index, paused, play, pause]);\n  const shown = index === undefined ? own.index : index;\n  const text = shown === null || shown < 0 ? \"\" : (lines[shown]?.text ?? \"\");\n\n  return (\n    <div ref={root} className={`flex min-h-control-sm min-w-0 items-center justify-center text-ui text-text-secondary ${className}`}>\n      {/* No overflow clip: the new line shows whole while the box springs to its width. */}\n      <p aria-hidden className=\"max-w-full min-w-0\">\n        <TextSwap>{text}</TextSwap>\n      </p>\n      <ol className=\"sr-only\">\n        {lines.map((l, i) => (\n          <li key={i}>{l.text}</li>\n        ))}\n      </ol>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/caption-ticker/caption-ticker.tsx"
    }
  ],
  "meta": {
    "details": "Short captions that step in under a scene with Text Swap's motion: the old line lifts and blurs out, the new one rises in, and the width springs to fit. Each line has its own timing, or a scene's timeline drives it; screen readers get every line at once.",
    "industries": [
      "saas",
      "devtools",
      "ai"
    ],
    "releaseAt": "2026-10-26"
  },
  "docs": "Import CaptionTicker from components/eased/caption-ticker/caption-ticker. Give each caption a duration to run it on its own clock, or pass index from a useDemoTimeline step.",
  "type": "registry:ui"
}