{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transcript-viewer",
  "title": "Transcript Viewer",
  "description": "A recording as text, where every line is also the way back to the moment it was said.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/transcript-viewer/transcript-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type TranscriptCue = {\n  id: string\n  /** Seconds from the start of the recording. */\n  start: number\n  end?: number\n  speaker?: string\n  text: string\n}\n\nexport type TranscriptViewerProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onSelect\"\n> & {\n  cues: readonly TranscriptCue[]\n  /** Where the recording is now, in seconds. */\n  time?: number\n  onSeek?: (cue: TranscriptCue) => void\n  /** Follows the recording as it plays. On by default. */\n  follow?: boolean\n  label?: string\n}\n\nfunction clock(seconds: number) {\n  const whole = Math.max(0, Math.floor(seconds))\n  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, \"0\")}`\n}\n\n/** The cue covering a moment, or the last one that started before it. */\nfunction cueAt(cues: readonly TranscriptCue[], time: number) {\n  let current: TranscriptCue | undefined\n\n  for (const cue of cues) {\n    if (cue.start > time) break\n    if (cue.end !== undefined && cue.end < time) {\n      current = cue\n      continue\n    }\n    current = cue\n  }\n\n  return current\n}\n\n/**\n * A recording as text, in the order it was said, with each line a way back to\n * the moment it was said at. Reading a transcript and scrubbing a recording\n * are the same act here rather than two.\n */\nexport function TranscriptViewer({\n  cues,\n  time,\n  onSeek,\n  follow = true,\n  label = \"Transcript\",\n  className,\n  ...rootProps\n}: TranscriptViewerProps) {\n  const active = time === undefined ? undefined : cueAt(cues, time)\n  const listRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    if (!follow || !active) return\n\n    const row = listRef.current?.querySelector<HTMLElement>(\n      `[data-cue=\"${CSS.escape(active.id)}\"]`\n    )\n\n    // Nearest, so following never yanks the reader further than it must.\n    row?.scrollIntoView({ block: \"nearest\" })\n  }, [active, follow])\n\n  return (\n    <div\n      data-slot=\"transcript-viewer\"\n      className={cn(\n        \"border-border bg-muted/40 overflow-hidden rounded-[calc(var(--radius)+0.15rem)] border\",\n        className\n      )}\n      {...rootProps}\n    >\n      <div\n        ref={listRef}\n        aria-label={label}\n        className=\"max-h-80 overflow-y-auto p-1\"\n      >\n        {cues.map((cue) => {\n          const here = active?.id === cue.id\n\n          return (\n            <button\n              key={cue.id}\n              type=\"button\"\n              data-cue={cue.id}\n              aria-current={here ? \"true\" : undefined}\n              onClick={() => onSeek?.(cue)}\n              className={cn(\n                \"focus-visible:ring-ring grid w-full grid-cols-[3.25rem_1fr] gap-3 rounded-lg px-2 py-2 text-left focus-visible:ring-2 focus-visible:outline-none\",\n                here ? \"bg-background\" : \"hover:bg-background/60\"\n              )}\n            >\n              <span\n                className={cn(\n                  \"font-mono text-xs tabular-nums\",\n                  here ? \"text-primary\" : \"text-muted-foreground\"\n                )}\n              >\n                {clock(cue.start)}\n              </span>\n\n              <span className=\"min-w-0\">\n                {cue.speaker ? (\n                  <span className=\"text-muted-foreground mr-2 text-xs font-semibold\">\n                    {cue.speaker}\n                  </span>\n                ) : null}\n                <span\n                  className={cn(\n                    \"text-sm\",\n                    here ? \"text-foreground\" : \"text-muted-foreground\"\n                  )}\n                >\n                  {cue.text}\n                </span>\n              </span>\n            </button>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "agent",
    "audio",
    "transcript",
    "viewer"
  ],
  "type": "registry:ui"
}