{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "speech-input",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "skeleton",
    "https://ui.elevenlabs.io/r/use-scribe.json"
  ],
  "files": [
    {
      "path": "components/ui/speech-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { motion } from \"framer-motion\"\nimport { MicIcon, SquareIcon, XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  useScribe,\n  type AudioFormat,\n  type CommitStrategy,\n} from \"@/registry/elevenlabs-ui/hooks/use-scribe\"\nimport { Button } from \"@/components/ui/button\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\n\nconst buttonVariants = cva(\"!px-0\", {\n  variants: {\n    size: {\n      default: \"h-9 w-9\",\n      sm: \"h-8 w-8\",\n      lg: \"h-10 w-10\",\n    },\n  },\n  defaultVariants: {\n    size: \"default\",\n  },\n})\n\ntype ButtonSize = VariantProps<typeof buttonVariants>[\"size\"]\n\nexport interface SpeechInputData {\n  /** The current partial (in-progress) transcript */\n  partialTranscript: string\n  /** Array of all committed (finalized) transcripts */\n  committedTranscripts: string[]\n  /** Full transcript combining committed and partial transcripts */\n  transcript: string\n}\n\ninterface SpeechInputContextValue {\n  isConnected: boolean\n  isConnecting: boolean\n  transcript: string\n  partialTranscript: string\n  committedTranscripts: string[]\n  error: string | null\n  start: () => Promise<void>\n  stop: () => void\n  cancel: () => void\n  size: ButtonSize\n}\n\nconst SpeechInputContext = React.createContext<SpeechInputContextValue | null>(\n  null\n)\n\nfunction useSpeechInput() {\n  const context = React.useContext(SpeechInputContext)\n  if (!context) {\n    throw new Error(\n      \"SpeechInput compound components must be used within a SpeechInput\"\n    )\n  }\n  return context\n}\n\nfunction buildTranscript({\n  partialTranscript,\n  committedTranscripts,\n}: {\n  partialTranscript: string\n  committedTranscripts: string[]\n}): string {\n  const committed = committedTranscripts.join(\" \").trim()\n  const partial = partialTranscript.trim()\n\n  if (committed && partial) {\n    return `${committed} ${partial}`\n  }\n  return committed || partial\n}\n\nfunction buildData({\n  partialTranscript,\n  committedTranscripts,\n}: {\n  partialTranscript: string\n  committedTranscripts: string[]\n}): SpeechInputData {\n  return {\n    partialTranscript,\n    committedTranscripts,\n    transcript: buildTranscript({ partialTranscript, committedTranscripts }),\n  }\n}\n\nexport interface SpeechInputProps {\n  children: React.ReactNode\n\n  /**\n   * Function that returns a token for authenticating with the speech service.\n   * This should be an async function that fetches a token from your backend.\n   */\n  getToken: () => Promise<string>\n\n  /**\n   * Called whenever the transcript changes (partial or committed)\n   */\n  onChange?: (data: SpeechInputData) => void\n\n  /**\n   * Called when recording is cancelled\n   */\n  onCancel?: (data: SpeechInputData) => void\n\n  /**\n   * Called when recording starts\n   */\n  onStart?: (data: SpeechInputData) => void\n\n  /**\n   * Called when recording stops\n   */\n  onStop?: (data: SpeechInputData) => void\n\n  /**\n   * Additional CSS classes for the root container\n   */\n  className?: string\n\n  /**\n   * Size variant for the component buttons\n   * @default \"default\"\n   */\n  size?: ButtonSize\n\n  /**\n   * Model ID for the speech recognition service\n   * @default \"scribe_v2_realtime\"\n   */\n  modelId?: string\n\n  /**\n   * Base URI for the speech recognition service\n   */\n  baseUri?: string\n\n  /**\n   * Strategy for committing transcripts\n   */\n  commitStrategy?: CommitStrategy\n\n  /**\n   * Silence threshold in seconds for VAD\n   */\n  vadSilenceThresholdSecs?: number\n\n  /**\n   * VAD threshold value\n   */\n  vadThreshold?: number\n\n  /**\n   * Minimum speech duration in milliseconds\n   */\n  minSpeechDurationMs?: number\n\n  /**\n   * Minimum silence duration in milliseconds\n   */\n  minSilenceDurationMs?: number\n\n  /**\n   * Language code for transcription (e.g., \"en\", \"es\", \"fr\")\n   */\n  languageCode?: string\n\n  /**\n   * Microphone configuration options\n   */\n  microphone?: {\n    deviceId?: string\n    echoCancellation?: boolean\n    noiseSuppression?: boolean\n    autoGainControl?: boolean\n    channelCount?: number\n  }\n\n  /**\n   * Audio format for manual audio mode\n   */\n  audioFormat?: AudioFormat\n\n  /**\n   * Sample rate for manual audio mode\n   */\n  sampleRate?: number\n\n  /**\n   * Called when an error occurs\n   */\n  onError?: (error: Error | Event) => void\n\n  /**\n   * Called when an authentication error occurs\n   */\n  onAuthError?: (data: { error: string }) => void\n\n  /**\n   * Called when a quota exceeded error occurs\n   */\n  onQuotaExceededError?: (data: { error: string }) => void\n}\n\nconst SpeechInput = React.forwardRef<HTMLDivElement, SpeechInputProps>(\n  function SpeechInput(\n    {\n      children,\n      getToken,\n      onChange,\n      onCancel,\n      onStart,\n      onStop,\n      className,\n      size = \"default\",\n      modelId = \"scribe_v2_realtime\",\n      baseUri,\n      commitStrategy,\n      vadSilenceThresholdSecs,\n      vadThreshold,\n      minSpeechDurationMs,\n      minSilenceDurationMs,\n      languageCode,\n      microphone = {\n        echoCancellation: true,\n        noiseSuppression: true,\n      },\n      audioFormat,\n      sampleRate,\n      onError,\n      onAuthError,\n      onQuotaExceededError,\n    },\n    ref\n  ) {\n    const transcriptsRef = React.useRef({\n      partialTranscript: \"\",\n      committedTranscripts: [] as string[],\n    })\n    const startRequestIdRef = React.useRef(0)\n\n    const scribe = useScribe({\n      modelId,\n      baseUri,\n      commitStrategy,\n      vadSilenceThresholdSecs,\n      vadThreshold,\n      minSpeechDurationMs,\n      minSilenceDurationMs,\n      languageCode,\n      audioFormat,\n      sampleRate,\n      microphone,\n      onPartialTranscript: (data) => {\n        transcriptsRef.current.partialTranscript = data.text\n        onChange?.(buildData(transcriptsRef.current))\n      },\n      onCommittedTranscript: (data) => {\n        transcriptsRef.current.committedTranscripts.push(data.text)\n        transcriptsRef.current.partialTranscript = \"\"\n        onChange?.(buildData(transcriptsRef.current))\n      },\n      onError,\n      onAuthError,\n      onQuotaExceededError,\n    })\n\n    const isConnecting = scribe.status === \"connecting\"\n\n    const start = React.useCallback(async () => {\n      const requestId = startRequestIdRef.current + 1\n      startRequestIdRef.current = requestId\n\n      transcriptsRef.current = {\n        partialTranscript: \"\",\n        committedTranscripts: [],\n      }\n      scribe.clearTranscripts()\n\n      try {\n        const token = await getToken()\n        if (startRequestIdRef.current !== requestId) {\n          return\n        }\n\n        await scribe.connect({\n          token,\n        })\n        if (startRequestIdRef.current !== requestId) {\n          scribe.disconnect()\n          return\n        }\n        onStart?.(buildData(transcriptsRef.current))\n      } catch (error) {\n        onError?.(error instanceof Error ? error : new Error(String(error)))\n      }\n    }, [getToken, scribe, onStart, onError])\n\n    const stop = React.useCallback(() => {\n      startRequestIdRef.current += 1\n      scribe.disconnect()\n      onStop?.(buildData(transcriptsRef.current))\n    }, [scribe, onStop])\n\n    const cancel = React.useCallback(() => {\n      startRequestIdRef.current += 1\n      const data = buildData(transcriptsRef.current)\n      scribe.disconnect()\n      scribe.clearTranscripts()\n      transcriptsRef.current = {\n        partialTranscript: \"\",\n        committedTranscripts: [],\n      }\n      onCancel?.(data)\n    }, [scribe, onCancel])\n\n    const contextValue: SpeechInputContextValue = React.useMemo(\n      () => ({\n        isConnected: scribe.isConnected,\n        isConnecting,\n        start,\n        stop,\n        cancel,\n        error: scribe.error,\n        size,\n        ...buildData({\n          partialTranscript: scribe.partialTranscript,\n          committedTranscripts: scribe.committedTranscripts.map((t) => t.text),\n        }),\n      }),\n      [\n        scribe.isConnected,\n        scribe.error,\n        scribe.partialTranscript,\n        scribe.committedTranscripts,\n        isConnecting,\n        start,\n        stop,\n        cancel,\n        size,\n      ]\n    )\n\n    React.useEffect(() => {\n      return () => {\n        startRequestIdRef.current += 1\n        scribe.disconnect()\n      }\n    }, [scribe.disconnect])\n\n    return (\n      <SpeechInputContext.Provider value={contextValue}>\n        <div\n          ref={ref}\n          className={cn(\n            \"relative inline-flex items-center overflow-hidden rounded-md transition-all duration-200\",\n            scribe.isConnected\n              ? \"bg-background dark:bg-muted shadow-[inset_0_0_0_1px_var(--color-input),0_1px_2px_0_rgba(0,0,0,0.05)]\"\n              : \"\",\n            className\n          )}\n        >\n          {children}\n        </div>\n      </SpeechInputContext.Provider>\n    )\n  }\n)\n\nSpeechInput.displayName = \"SpeechInput\"\n\nexport type SpeechInputRecordButtonProps = Omit<\n  React.ComponentPropsWithoutRef<typeof Button>,\n  \"size\"\n>\n\n/**\n * Toggle button for starting/stopping speech recording.\n * Shows a microphone icon when idle and a stop icon when recording.\n */\nconst SpeechInputRecordButton = React.forwardRef<\n  HTMLButtonElement,\n  SpeechInputRecordButtonProps\n>(function SpeechInputRecordButton(\n  { className, onClick, variant = \"ghost\", disabled, ...props },\n  ref\n) {\n  const speechInput = useSpeechInput()\n\n  return (\n    <Button\n      ref={ref}\n      type=\"button\"\n      variant={variant}\n      onClick={(e) => {\n        if (speechInput.isConnected) {\n          speechInput.stop()\n        } else {\n          speechInput.start()\n        }\n        onClick?.(e)\n      }}\n      disabled={disabled ?? speechInput.isConnecting}\n      className={cn(\n        buttonVariants({ size: speechInput.size }),\n        \"relative flex items-center justify-center transition-all\",\n        speechInput.isConnected && \"scale-[80%]\",\n        className\n      )}\n      aria-label={\n        speechInput.isConnected ? \"Stop recording\" : \"Start recording\"\n      }\n      {...props}\n    >\n      <Skeleton\n        className={cn(\n          \"absolute h-4 w-4 rounded-full transition-all duration-200\",\n          speechInput.isConnecting\n            ? \"bg-primary scale-90\"\n            : \"scale-[60%] bg-transparent\"\n        )}\n      />\n      <SquareIcon\n        className={cn(\n          \"text-destructive absolute h-4 w-4 fill-current transition-all duration-200\",\n          !speechInput.isConnecting && speechInput.isConnected\n            ? \"scale-100 opacity-100\"\n            : \"scale-[60%] opacity-0\"\n        )}\n      />\n      <MicIcon\n        className={cn(\n          \"absolute h-4 w-4 transition-all duration-200\",\n          !speechInput.isConnecting && !speechInput.isConnected\n            ? \"scale-100 opacity-100\"\n            : \"scale-[60%] opacity-0\"\n        )}\n      />\n    </Button>\n  )\n})\n\nSpeechInputRecordButton.displayName = \"SpeechInputRecordButton\"\n\nexport interface SpeechInputPreviewProps\n  extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Text to show when no transcript is available\n   * @default \"Listening...\"\n   */\n  placeholder?: string\n}\n\n/**\n * Displays the current transcript with a placeholder when empty.\n * Only visible when actively recording.\n */\nconst SpeechInputPreview = React.forwardRef<\n  HTMLDivElement,\n  SpeechInputPreviewProps\n>(function SpeechInputPreview(\n  { className, placeholder = \"Listening...\", ...props },\n  ref\n) {\n  const speechInput = useSpeechInput()\n\n  const displayText = speechInput.transcript || placeholder\n  const showPlaceholder = !speechInput.transcript.trim()\n\n  return (\n    <div\n      ref={ref}\n      inert={speechInput.isConnected ? undefined : true}\n      className={cn(\n        \"relative self-stretch text-sm transition-[opacity,transform,width] duration-200 ease-out\",\n        showPlaceholder\n          ? \"text-muted-foreground italic\"\n          : \"text-muted-foreground\",\n        speechInput.isConnected ? \"w-28 opacity-100\" : \"w-0 opacity-0\",\n        className\n      )}\n      title={displayText}\n      aria-hidden={!speechInput.isConnected}\n      {...props}\n    >\n      <div className=\"absolute inset-y-0 -right-1 -left-1 [mask-image:linear-gradient(to_right,transparent,black_10px,black_calc(100%-10px),transparent)]\">\n        <motion.p\n          key=\"text\"\n          layout=\"position\"\n          className=\"absolute top-0 right-0 bottom-0 flex h-full min-w-full items-center px-1 whitespace-nowrap\"\n        >\n          {displayText}\n        </motion.p>\n      </div>\n    </div>\n  )\n})\n\nSpeechInputPreview.displayName = \"SpeechInputPreview\"\n\nexport type SpeechInputCancelButtonProps = Omit<\n  React.ComponentPropsWithoutRef<typeof Button>,\n  \"size\"\n>\n\n/**\n * Button to cancel the current recording and discard the transcript.\n * Only visible when actively recording.\n */\nconst SpeechInputCancelButton = React.forwardRef<\n  HTMLButtonElement,\n  SpeechInputCancelButtonProps\n>(function SpeechInputCancelButton(\n  { className, onClick, variant = \"ghost\", ...props },\n  ref\n) {\n  const speechInput = useSpeechInput()\n\n  return (\n    <Button\n      ref={ref}\n      type=\"button\"\n      variant={variant}\n      inert={speechInput.isConnected ? undefined : true}\n      onClick={(e) => {\n        speechInput.cancel()\n        onClick?.(e)\n      }}\n      className={cn(\n        buttonVariants({ size: speechInput.size }),\n        \"transition-[opacity,transform,width] duration-200 ease-out\",\n        speechInput.isConnected\n          ? \"scale-[80%] opacity-100\"\n          : \"pointer-events-none w-0 scale-100 opacity-0\",\n        className\n      )}\n      aria-label=\"Cancel recording\"\n      {...props}\n    >\n      <XIcon className=\"h-3 w-3\" />\n    </Button>\n  )\n})\n\nSpeechInputCancelButton.displayName = \"SpeechInputCancelButton\"\n\nexport {\n  SpeechInput,\n  SpeechInputRecordButton,\n  SpeechInputPreview,\n  SpeechInputCancelButton,\n  useSpeechInput,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}