{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transcript-viewer",
  "devDependencies": [
    "@elevenlabs/elevenlabs-js"
  ],
  "registryDependencies": [
    "button",
    "https://ui.elevenlabs.io/r/scrub-bar.json"
  ],
  "files": [
    {
      "path": "components/ui/transcript-viewer.tsx",
      "content": "\"use client\"\n\nimport {\n  createContext,\n  useContext,\n  useMemo,\n  type ComponentPropsWithoutRef,\n  type ComponentPropsWithRef,\n  type HTMLAttributes,\n  type ReactNode,\n} from \"react\"\nimport type { CharacterAlignmentResponseModel } from \"@elevenlabs/elevenlabs-js/api/types/CharacterAlignmentResponseModel\"\nimport { Pause, Play } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  useTranscriptViewer,\n  type SegmentComposer,\n  type TranscriptSegment,\n  type TranscriptWord as TranscriptWordType,\n  type UseTranscriptViewerResult,\n} from \"@/registry/elevenlabs-ui/hooks/use-transcript-viewer\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  ScrubBarContainer,\n  ScrubBarProgress,\n  ScrubBarThumb,\n  ScrubBarTimeLabel,\n  ScrubBarTrack,\n} from \"@/components/ui/scrub-bar\"\n\ntype TranscriptGap = Extract<TranscriptSegment, { kind: \"gap\" }>\n\ntype TranscriptViewerContextValue = UseTranscriptViewerResult & {\n  audioProps: Omit<ComponentPropsWithRef<\"audio\">, \"children\" | \"src\">\n}\n\nconst TranscriptViewerContext =\n  createContext<TranscriptViewerContextValue | null>(null)\n\nfunction useTranscriptViewerContext() {\n  const context = useContext(TranscriptViewerContext)\n  if (!context) {\n    throw new Error(\n      \"useTranscriptViewerContext must be used within a TranscriptViewer\"\n    )\n  }\n  return context\n}\n\ntype TranscriptViewerProviderProps = {\n  value: TranscriptViewerContextValue\n  children: ReactNode\n}\n\nfunction TranscriptViewerProvider({\n  value,\n  children,\n}: TranscriptViewerProviderProps) {\n  return (\n    <TranscriptViewerContext.Provider value={value}>\n      {children}\n    </TranscriptViewerContext.Provider>\n  )\n}\n\ntype AudioType =\n  | \"audio/mpeg\"\n  | \"audio/wav\"\n  | \"audio/ogg\"\n  | \"audio/mp3\"\n  | \"audio/m4a\"\n  | \"audio/aac\"\n  | \"audio/webm\"\n\ntype TranscriptViewerContainerProps = {\n  audioSrc: string\n  audioType: AudioType\n  alignment: CharacterAlignmentResponseModel\n  segmentComposer?: SegmentComposer\n  hideAudioTags?: boolean\n  children?: ReactNode\n} & Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> &\n  Pick<\n    Parameters<typeof useTranscriptViewer>[0],\n    \"onPlay\" | \"onPause\" | \"onTimeUpdate\" | \"onEnded\" | \"onDurationChange\"\n  >\n\nfunction TranscriptViewerContainer({\n  audioSrc,\n  audioType = \"audio/mpeg\",\n  alignment,\n  segmentComposer,\n  hideAudioTags = true,\n  children,\n  className,\n  onPlay,\n  onPause,\n  onTimeUpdate,\n  onEnded,\n  onDurationChange,\n  ...props\n}: TranscriptViewerContainerProps) {\n  const viewerState = useTranscriptViewer({\n    alignment,\n    hideAudioTags,\n    segmentComposer,\n    onPlay,\n    onPause,\n    onTimeUpdate,\n    onEnded,\n    onDurationChange,\n  })\n\n  const { audioRef } = viewerState\n\n  const audioProps = useMemo(\n    () => ({\n      ref: audioRef,\n      controls: false,\n      preload: \"metadata\" as const,\n      src: audioSrc,\n      children: <source src={audioSrc} type={audioType} />,\n    }),\n    [audioRef, audioSrc]\n  )\n\n  const contextValue = useMemo(\n    () => ({\n      ...viewerState,\n      audioProps,\n    }),\n    [viewerState, audioProps]\n  )\n\n  return (\n    <TranscriptViewerProvider value={contextValue}>\n      <div\n        data-slot=\"transcript-viewer-root\"\n        className={cn(\"space-y-4 p-4\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </TranscriptViewerProvider>\n  )\n}\n\ntype TranscriptViewerWordStatus = \"spoken\" | \"unspoken\" | \"current\"\ninterface TranscriptViewerWordProps\n  extends Omit<HTMLAttributes<HTMLSpanElement>, \"children\"> {\n  word: TranscriptWordType\n  status: TranscriptViewerWordStatus\n  children?: ReactNode\n}\n\nfunction TranscriptViewerWord({\n  word,\n  status,\n  className,\n  children,\n  ...props\n}: TranscriptViewerWordProps) {\n  return (\n    <span\n      data-slot=\"transcript-word\"\n      data-kind=\"word\"\n      data-status={status}\n      className={cn(\n        \"rounded-sm px-0.5 transition-colors\",\n        status === \"spoken\" && \"text-foreground\",\n        status === \"unspoken\" && \"text-muted-foreground\",\n        status === \"current\" && \"bg-primary text-primary-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? word.text}\n    </span>\n  )\n}\n\ninterface TranscriptViewerWordsProps extends HTMLAttributes<HTMLDivElement> {\n  renderWord?: (props: {\n    word: TranscriptWordType\n    status: TranscriptViewerWordStatus\n  }) => ReactNode\n  renderGap?: (props: {\n    segment: TranscriptGap\n    status: TranscriptViewerWordStatus\n  }) => ReactNode\n  wordClassNames?: string\n  gapClassNames?: string\n}\n\nfunction TranscriptViewerWords({\n  className,\n  renderWord,\n  renderGap,\n  wordClassNames,\n  gapClassNames,\n  ...props\n}: TranscriptViewerWordsProps) {\n  const {\n    spokenSegments,\n    unspokenSegments,\n    currentWord,\n    segments,\n    duration,\n    currentTime,\n  } = useTranscriptViewerContext()\n\n  const nearEnd = useMemo(() => {\n    if (!duration) return false\n    return currentTime >= duration - 0.01\n  }, [currentTime, duration])\n\n  const segmentsWithStatus = useMemo(() => {\n    if (nearEnd) {\n      return segments.map((segment) => ({ segment, status: \"spoken\" as const }))\n    }\n\n    const entries: Array<{\n      segment: TranscriptSegment\n      status: TranscriptViewerWordStatus\n    }> = []\n\n    for (const segment of spokenSegments) {\n      entries.push({ segment, status: \"spoken\" })\n    }\n\n    if (currentWord) {\n      entries.push({ segment: currentWord, status: \"current\" })\n    }\n\n    for (const segment of unspokenSegments) {\n      entries.push({ segment, status: \"unspoken\" })\n    }\n\n    return entries\n  }, [spokenSegments, unspokenSegments, currentWord, nearEnd, segments])\n\n  return (\n    <div\n      data-slot=\"transcript-words\"\n      className={cn(\"text-xl leading-relaxed\", className)}\n      {...props}\n    >\n      {segmentsWithStatus.map(({ segment, status }) => {\n        if (segment.kind === \"gap\") {\n          const content = renderGap\n            ? renderGap({ segment, status })\n            : segment.text\n          return (\n            <span\n              key={`gap-${segment.segmentIndex}`}\n              data-kind=\"gap\"\n              data-status={status}\n              className={cn(gapClassNames)}\n            >\n              {content}\n            </span>\n          )\n        }\n\n        if (renderWord) {\n          return (\n            <span\n              key={`word-${segment.segmentIndex}`}\n              data-kind=\"word\"\n              data-status={status}\n              className={cn(wordClassNames)}\n            >\n              {renderWord({ word: segment, status })}\n            </span>\n          )\n        }\n\n        return (\n          <TranscriptViewerWord\n            key={`word-${segment.segmentIndex}`}\n            word={segment}\n            status={status}\n            className={wordClassNames}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\nfunction TranscriptViewerAudio({\n  ...props\n}: ComponentPropsWithoutRef<\"audio\">) {\n  const { audioProps } = useTranscriptViewerContext()\n  return (\n    <audio\n      data-slot=\"transcript-audio\"\n      {...audioProps}\n      {...props}\n      ref={audioProps.ref}\n    />\n  )\n}\n\ntype RenderChildren = (state: { isPlaying: boolean }) => ReactNode\n\ntype TranscriptViewerPlayPauseButtonProps = Omit<\n  ComponentPropsWithoutRef<typeof Button>,\n  \"children\"\n> & {\n  children?: ReactNode | RenderChildren\n}\n\nfunction TranscriptViewerPlayPauseButton({\n  className,\n  children,\n  onClick,\n  ...props\n}: TranscriptViewerPlayPauseButtonProps) {\n  const { isPlaying, play, pause } = useTranscriptViewerContext()\n  const Icon = isPlaying ? Pause : Play\n\n  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n    if (isPlaying) pause()\n    else play()\n    onClick?.(event)\n  }\n\n  const content =\n    typeof children === \"function\"\n      ? (children as RenderChildren)({ isPlaying })\n      : children\n\n  return (\n    <Button\n      data-slot=\"transcript-play-pause-button\"\n      type=\"button\"\n      variant=\"outline\"\n      size=\"icon\"\n      aria-label={isPlaying ? \"Pause audio\" : \"Play audio\"}\n      data-playing={isPlaying}\n      className={cn(\"cursor-pointer\", className)}\n      onClick={handleClick}\n      {...props}\n    >\n      {content ?? <Icon className=\"size-5\" />}\n    </Button>\n  )\n}\n\ntype TranscriptViewerScrubBarProps = Omit<\n  ComponentPropsWithoutRef<typeof ScrubBarContainer>,\n  \"duration\" | \"value\" | \"onScrub\" | \"onScrubStart\" | \"onScrubEnd\"\n> & {\n  showTimeLabels?: boolean\n  labelsClassName?: string\n  trackClassName?: string\n  progressClassName?: string\n  thumbClassName?: string\n}\n\n/**\n * A context-aware implementation of the scrub bar specific to the transcript viewer.\n */\nfunction TranscriptViewerScrubBar({\n  className,\n  showTimeLabels = true,\n  labelsClassName,\n  trackClassName,\n  progressClassName,\n  thumbClassName,\n  ...props\n}: TranscriptViewerScrubBarProps) {\n  const { duration, currentTime, seekToTime, startScrubbing, endScrubbing } =\n    useTranscriptViewerContext()\n  return (\n    <ScrubBarContainer\n      data-slot=\"transcript-scrub-bar\"\n      duration={duration}\n      value={currentTime}\n      onScrubStart={startScrubbing}\n      onScrubEnd={endScrubbing}\n      onScrub={seekToTime}\n      className={className}\n      {...props}\n    >\n      <div className=\"flex flex-1 flex-col gap-1\">\n        <ScrubBarTrack className={trackClassName}>\n          <ScrubBarProgress className={progressClassName} />\n          <ScrubBarThumb className={thumbClassName} />\n        </ScrubBarTrack>\n        {showTimeLabels && (\n          <div\n            className={cn(\n              \"text-muted-foreground flex items-center justify-between text-xs\",\n              labelsClassName\n            )}\n          >\n            <ScrubBarTimeLabel time={currentTime} />\n            <ScrubBarTimeLabel time={duration - currentTime} />\n          </div>\n        )}\n      </div>\n    </ScrubBarContainer>\n  )\n}\n\nexport {\n  TranscriptViewerContainer,\n  TranscriptViewerWords,\n  TranscriptViewerWord,\n  TranscriptViewerAudio,\n  TranscriptViewerPlayPauseButton,\n  TranscriptViewerScrubBar,\n  TranscriptViewerProvider,\n  useTranscriptViewerContext,\n}\nexport type { CharacterAlignmentResponseModel }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}