{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-scribe",
  "dependencies": [
    "@elevenlabs/client"
  ],
  "files": [
    {
      "path": "hooks/use-scribe.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { RealtimeEvents, Scribe } from \"@elevenlabs/client\"\nimport type {\n  AudioFormat,\n  AudioOptions,\n  CommitStrategy,\n  CommittedTranscriptMessage,\n  CommittedTranscriptWithTimestampsMessage,\n  MicrophoneOptions,\n  PartialTranscriptMessage,\n  RealtimeConnection,\n  ScribeAuthErrorMessage,\n  ScribeChunkSizeExceededErrorMessage,\n  ScribeCommitThrottledErrorMessage,\n  ScribeErrorMessage,\n  ScribeInputErrorMessage,\n  ScribeInsufficientAudioActivityErrorMessage,\n  ScribeQueueOverflowErrorMessage,\n  ScribeQuotaExceededErrorMessage,\n  ScribeRateLimitedErrorMessage,\n  ScribeResourceExhaustedErrorMessage,\n  ScribeSessionTimeLimitExceededErrorMessage,\n  ScribeTranscriberErrorMessage,\n  ScribeUnacceptedTermsErrorMessage,\n} from \"@elevenlabs/client\"\n\n// ============= Types =============\n\nexport type ScribeStatus =\n  | \"disconnected\"\n  | \"connecting\"\n  | \"connected\"\n  | \"transcribing\"\n  | \"error\"\n\nexport interface TranscriptSegment {\n  id: string\n  text: string\n  timestamp: number\n  isFinal: boolean\n}\n\nexport interface ScribeCallbacks {\n  onSessionStarted?: () => void\n  onPartialTranscript?: (data: { text: string }) => void\n  onCommittedTranscript?: (data: { text: string }) => void\n  onCommittedTranscriptWithTimestamps?: (data: {\n    text: string\n    timestamps?: { start: number; end: number }[]\n  }) => void\n  /** Called for any error (also called when specific error callbacks fire) */\n  onError?: (error: Error | Event) => void\n  onAuthError?: (data: { error: string }) => void\n  onQuotaExceededError?: (data: { error: string }) => void\n  onCommitThrottledError?: (data: { error: string }) => void\n  onTranscriberError?: (data: { error: string }) => void\n  onUnacceptedTermsError?: (data: { error: string }) => void\n  onRateLimitedError?: (data: { error: string }) => void\n  onInputError?: (data: { error: string }) => void\n  onQueueOverflowError?: (data: { error: string }) => void\n  onResourceExhaustedError?: (data: { error: string }) => void\n  onSessionTimeLimitExceededError?: (data: { error: string }) => void\n  onChunkSizeExceededError?: (data: { error: string }) => void\n  onInsufficientAudioActivityError?: (data: { error: string }) => void\n\n  onConnect?: () => void\n  onDisconnect?: () => void\n}\n\nexport interface ScribeHookOptions extends ScribeCallbacks {\n  // Connection options\n  token?: string\n  modelId?: string\n  baseUri?: string\n\n  // VAD options\n  commitStrategy?: CommitStrategy\n  vadSilenceThresholdSecs?: number\n  vadThreshold?: number\n  minSpeechDurationMs?: number\n  minSilenceDurationMs?: number\n  languageCode?: string\n\n  // Microphone options (for automatic microphone mode)\n  microphone?: {\n    deviceId?: string\n    echoCancellation?: boolean\n    noiseSuppression?: boolean\n    autoGainControl?: boolean\n    channelCount?: number\n  }\n\n  // Manual audio options\n  audioFormat?: AudioFormat\n  sampleRate?: number\n\n  // Auto-connect on mount\n  autoConnect?: boolean\n\n  // Include timestamps\n  includeTimestamps?: boolean\n}\n\nexport interface UseScribeReturn {\n  // State\n  status: ScribeStatus\n  isConnected: boolean\n  isTranscribing: boolean\n  partialTranscript: string\n  committedTranscripts: TranscriptSegment[]\n  error: string | null\n\n  // Connection methods\n  connect: (options?: Partial<ScribeHookOptions>) => Promise<void>\n  disconnect: () => void\n\n  // Audio methods (for manual mode)\n  sendAudio: (\n    audioBase64: string,\n    options?: { commit?: boolean; sampleRate?: number; previousText?: string }\n  ) => void\n  commit: () => void\n\n  // Utility methods\n  clearTranscripts: () => void\n  getConnection: () => RealtimeConnection | null\n}\n\n// ============= Hook Implementation =============\n\nexport function useScribe(options: ScribeHookOptions = {}): UseScribeReturn {\n  const {\n    // Callbacks\n    onSessionStarted,\n    onPartialTranscript,\n    onCommittedTranscript,\n    onCommittedTranscriptWithTimestamps,\n    onError,\n    onAuthError,\n    onQuotaExceededError,\n    onCommitThrottledError,\n    onTranscriberError,\n    onUnacceptedTermsError,\n    onRateLimitedError,\n    onInputError,\n    onQueueOverflowError,\n    onResourceExhaustedError,\n    onSessionTimeLimitExceededError,\n    onChunkSizeExceededError,\n    onInsufficientAudioActivityError,\n    onConnect,\n    onDisconnect,\n\n    // Connection options\n    token: defaultToken,\n    modelId: defaultModelId,\n    baseUri: defaultBaseUri,\n    commitStrategy: defaultCommitStrategy,\n    vadSilenceThresholdSecs: defaultVadSilenceThresholdSecs,\n    vadThreshold: defaultVadThreshold,\n    minSpeechDurationMs: defaultMinSpeechDurationMs,\n    minSilenceDurationMs: defaultMinSilenceDurationMs,\n    languageCode: defaultLanguageCode,\n\n    // Mode options\n    microphone: defaultMicrophone,\n    audioFormat: defaultAudioFormat,\n    sampleRate: defaultSampleRate,\n\n    // Auto-connect\n    autoConnect = false,\n  } = options\n\n  const connectionRef = useRef<RealtimeConnection | null>(null)\n  const connectionIdCounterRef = useRef(0)\n  const activeConnectionIdRef = useRef<number | null>(null)\n\n  const [status, setStatus] = useState<ScribeStatus>(\"disconnected\")\n  const [partialTranscript, setPartialTranscript] = useState<string>(\"\")\n  const [committedTranscripts, setCommittedTranscripts] = useState<\n    TranscriptSegment[]\n  >([])\n  const [error, setError] = useState<string | null>(null)\n\n  const disconnect = useCallback(() => {\n    const connection = connectionRef.current\n    if (!connection) {\n      setStatus(\"disconnected\")\n      activeConnectionIdRef.current = null\n      return\n    }\n\n    activeConnectionIdRef.current = null\n    connectionRef.current = null\n\n    try {\n      const result = connection.close()\n      if (\n        typeof result === \"object\" &&\n        result !== null &&\n        \"catch\" in result &&\n        typeof (result as Promise<unknown>).catch === \"function\"\n      ) {\n        const promise = result as Promise<void>\n        promise.catch(() => {\n          /* noop */\n        })\n      }\n    } catch (err) {\n      console.warn(\"[useScribe] Failed to close connection\", err)\n    } finally {\n      setStatus(\"disconnected\")\n      onDisconnect?.()\n    }\n  }, [onDisconnect])\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      disconnect()\n    }\n  }, [disconnect])\n\n  const connect = useCallback(\n    async (runtimeOptions: Partial<ScribeHookOptions> = {}) => {\n      if (connectionRef.current) {\n        console.warn(\"Already connected\")\n        return\n      }\n\n      const connectionId = connectionIdCounterRef.current + 1\n      connectionIdCounterRef.current = connectionId\n\n      try {\n        setStatus(\"connecting\")\n        setError(null)\n\n        // Merge default options with runtime options\n        const token = runtimeOptions.token || defaultToken\n        const modelId = runtimeOptions.modelId || defaultModelId\n\n        if (!token) {\n          throw new Error(\"Token is required\")\n        }\n        if (!modelId) {\n          throw new Error(\"Model ID is required\")\n        }\n\n        // Determine mode: microphone or manual\n        const microphone = runtimeOptions.microphone || defaultMicrophone\n        const audioFormat = runtimeOptions.audioFormat || defaultAudioFormat\n        const sampleRate = runtimeOptions.sampleRate || defaultSampleRate\n\n        let connection: RealtimeConnection\n\n        const includeTimestamps =\n          runtimeOptions.includeTimestamps ??\n          !!(\n            runtimeOptions.onCommittedTranscriptWithTimestamps ||\n            onCommittedTranscriptWithTimestamps\n          )\n\n        if (microphone) {\n          // Microphone mode\n          connection = Scribe.connect({\n            token,\n            modelId,\n            baseUri: runtimeOptions.baseUri || defaultBaseUri,\n            commitStrategy:\n              runtimeOptions.commitStrategy || defaultCommitStrategy,\n            vadSilenceThresholdSecs:\n              runtimeOptions.vadSilenceThresholdSecs ||\n              defaultVadSilenceThresholdSecs,\n            vadThreshold: runtimeOptions.vadThreshold || defaultVadThreshold,\n            minSpeechDurationMs:\n              runtimeOptions.minSpeechDurationMs || defaultMinSpeechDurationMs,\n            minSilenceDurationMs:\n              runtimeOptions.minSilenceDurationMs ||\n              defaultMinSilenceDurationMs,\n            languageCode: runtimeOptions.languageCode || defaultLanguageCode,\n            microphone,\n            includeTimestamps,\n          } as MicrophoneOptions)\n        } else if (audioFormat && sampleRate) {\n          // Manual audio mode\n          connection = Scribe.connect({\n            token,\n            modelId,\n            baseUri: runtimeOptions.baseUri || defaultBaseUri,\n            commitStrategy:\n              runtimeOptions.commitStrategy || defaultCommitStrategy,\n            vadSilenceThresholdSecs:\n              runtimeOptions.vadSilenceThresholdSecs ||\n              defaultVadSilenceThresholdSecs,\n            vadThreshold: runtimeOptions.vadThreshold || defaultVadThreshold,\n            minSpeechDurationMs:\n              runtimeOptions.minSpeechDurationMs || defaultMinSpeechDurationMs,\n            minSilenceDurationMs:\n              runtimeOptions.minSilenceDurationMs ||\n              defaultMinSilenceDurationMs,\n            languageCode: runtimeOptions.languageCode || defaultLanguageCode,\n            includeTimestamps,\n            audioFormat,\n            sampleRate,\n          } as AudioOptions)\n        } else {\n          throw new Error(\n            \"Either microphone options or (audioFormat + sampleRate) must be provided\"\n          )\n        }\n\n        connectionRef.current = connection\n        activeConnectionIdRef.current = connectionId\n\n        const runIfCurrent =\n          <Args extends unknown[]>(handler: (...args: Args) => void) =>\n          (...args: Args) => {\n            if (activeConnectionIdRef.current !== connectionId) {\n              return\n            }\n            handler(...args)\n          }\n\n        // Set up event listeners\n        connection.on(\n          RealtimeEvents.SESSION_STARTED,\n          runIfCurrent(() => {\n            setStatus(\"connected\")\n            onSessionStarted?.()\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.PARTIAL_TRANSCRIPT,\n          runIfCurrent((data: unknown) => {\n            const message = data as PartialTranscriptMessage\n            setPartialTranscript(message.text)\n            setStatus(\"transcribing\")\n            onPartialTranscript?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.COMMITTED_TRANSCRIPT,\n          runIfCurrent((data: unknown) => {\n            const message = data as CommittedTranscriptMessage\n            const segment: TranscriptSegment = {\n              id: `${Date.now()}-${Math.random()}`,\n              text: message.text,\n              timestamp: Date.now(),\n              isFinal: true,\n            }\n            setCommittedTranscripts((prev) => [...prev, segment])\n            setPartialTranscript(\"\")\n            onCommittedTranscript?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS,\n          runIfCurrent((data: unknown) => {\n            const message = data as CommittedTranscriptWithTimestampsMessage\n            const segment: TranscriptSegment = {\n              id: `${Date.now()}-${Math.random()}`,\n              text: message.text,\n              timestamp: Date.now(),\n              isFinal: true,\n            }\n            setCommittedTranscripts((prev) => [...prev, segment])\n            setPartialTranscript(\"\")\n            onCommittedTranscriptWithTimestamps?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.ERROR,\n          runIfCurrent((err: unknown) => {\n            const message = err as ScribeErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onError?.(new Error(message.error))\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.AUTH_ERROR,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeAuthErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onAuthError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.QUOTA_EXCEEDED,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeQuotaExceededErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onQuotaExceededError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.COMMIT_THROTTLED,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeCommitThrottledErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onCommitThrottledError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.TRANSCRIBER_ERROR,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeTranscriberErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onTranscriberError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.UNACCEPTED_TERMS,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeUnacceptedTermsErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onUnacceptedTermsError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.RATE_LIMITED,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeRateLimitedErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onRateLimitedError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.INPUT_ERROR,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeInputErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onInputError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.QUEUE_OVERFLOW,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeQueueOverflowErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onQueueOverflowError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.RESOURCE_EXHAUSTED,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeResourceExhaustedErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onResourceExhaustedError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.SESSION_TIME_LIMIT_EXCEEDED,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeSessionTimeLimitExceededErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onSessionTimeLimitExceededError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.CHUNK_SIZE_EXCEEDED,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeChunkSizeExceededErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onChunkSizeExceededError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.INSUFFICIENT_AUDIO_ACTIVITY,\n          runIfCurrent((data: unknown) => {\n            const message = data as ScribeInsufficientAudioActivityErrorMessage\n            setError(message.error)\n            setStatus(\"error\")\n            onInsufficientAudioActivityError?.(message)\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.OPEN,\n          runIfCurrent(() => {\n            onConnect?.()\n          })\n        )\n\n        connection.on(\n          RealtimeEvents.CLOSE,\n          runIfCurrent(() => {\n            activeConnectionIdRef.current = null\n            connectionRef.current = null\n            setStatus(\"disconnected\")\n            onDisconnect?.()\n          })\n        )\n      } catch (err) {\n        const errorMessage =\n          err instanceof Error ? err.message : \"Failed to connect\"\n        setError(errorMessage)\n        setStatus(\"error\")\n        throw err\n      }\n    },\n    [\n      defaultToken,\n      defaultModelId,\n      defaultBaseUri,\n      defaultCommitStrategy,\n      defaultVadSilenceThresholdSecs,\n      defaultVadThreshold,\n      defaultMinSpeechDurationMs,\n      defaultMinSilenceDurationMs,\n      defaultLanguageCode,\n      defaultMicrophone,\n      defaultAudioFormat,\n      defaultSampleRate,\n      onSessionStarted,\n      onPartialTranscript,\n      onCommittedTranscript,\n      onCommittedTranscriptWithTimestamps,\n      onError,\n      onAuthError,\n      onQuotaExceededError,\n      onCommitThrottledError,\n      onTranscriberError,\n      onUnacceptedTermsError,\n      onRateLimitedError,\n      onInputError,\n      onQueueOverflowError,\n      onResourceExhaustedError,\n      onSessionTimeLimitExceededError,\n      onChunkSizeExceededError,\n      onInsufficientAudioActivityError,\n      onConnect,\n      onDisconnect,\n    ]\n  )\n\n  const sendAudio = useCallback(\n    (\n      audioBase64: string,\n      options?: { commit?: boolean; sampleRate?: number; previousText?: string }\n    ) => {\n      if (!connectionRef.current) {\n        throw new Error(\"Not connected to Scribe\")\n      }\n      connectionRef.current.send({ audioBase64, ...options })\n    },\n    []\n  )\n\n  const commit = useCallback(() => {\n    if (!connectionRef.current) {\n      throw new Error(\"Not connected to Scribe\")\n    }\n    connectionRef.current.commit()\n  }, [])\n\n  const clearTranscripts = useCallback(() => {\n    setCommittedTranscripts([])\n    setPartialTranscript(\"\")\n  }, [])\n\n  const getConnection = useCallback(() => {\n    return connectionRef.current\n  }, [])\n\n  // Auto-connect if enabled\n  useEffect(() => {\n    if (autoConnect) {\n      void connect()\n    }\n  }, [autoConnect, connect])\n\n  return {\n    // State\n    status,\n    isConnected: status === \"connected\" || status === \"transcribing\",\n    isTranscribing: status === \"transcribing\",\n    partialTranscript,\n    committedTranscripts,\n    error,\n\n    // Methods\n    connect,\n    disconnect,\n    sendAudio,\n    commit,\n    clearTranscripts,\n    getConnection,\n  }\n}\n\n// Export types and enums from client for convenience\nexport { AudioFormat, CommitStrategy, RealtimeEvents } from \"@elevenlabs/client\"\nexport type { RealtimeConnection } from \"@elevenlabs/client\"\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}