{ "$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\n\ntype TranscriptViewerContextValue = UseTranscriptViewerResult & {\n audioProps: Omit, \"children\" | \"src\">\n}\n\nconst TranscriptViewerContext =\n createContext(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 \n {children}\n \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, \"children\"> &\n Pick<\n Parameters[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: ,\n }),\n [audioRef, audioSrc]\n )\n\n const contextValue = useMemo(\n () => ({\n ...viewerState,\n audioProps,\n }),\n [viewerState, audioProps]\n )\n\n return (\n \n \n {children}\n \n \n )\n}\n\ntype TranscriptViewerWordStatus = \"spoken\" | \"unspoken\" | \"current\"\ninterface TranscriptViewerWordProps\n extends Omit, \"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 \n {children ?? word.text}\n \n )\n}\n\ninterface TranscriptViewerWordsProps extends HTMLAttributes {\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 \n {segmentsWithStatus.map(({ segment, status }) => {\n if (segment.kind === \"gap\") {\n const content = renderGap\n ? renderGap({ segment, status })\n : segment.text\n return (\n \n {content}\n \n )\n }\n\n if (renderWord) {\n return (\n \n {renderWord({ word: segment, status })}\n \n )\n }\n\n return (\n \n )\n })}\n \n )\n}\n\nfunction TranscriptViewerAudio({\n ...props\n}: ComponentPropsWithoutRef<\"audio\">) {\n const { audioProps } = useTranscriptViewerContext()\n return (\n \n )\n}\n\ntype RenderChildren = (state: { isPlaying: boolean }) => ReactNode\n\ntype TranscriptViewerPlayPauseButtonProps = Omit<\n ComponentPropsWithoutRef,\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) => {\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 \n {content ?? }\n \n )\n}\n\ntype TranscriptViewerScrubBarProps = Omit<\n ComponentPropsWithoutRef,\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 \n
\n \n \n \n \n {showTimeLabels && (\n \n \n \n
\n )}\n \n \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" }