{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-speech-recognition",
  "title": "Speech recognition",
  "description": "SSR-safe Web Speech API dictation with final and interim transcripts, error codes, and session controls.",
  "files": [
    {
      "path": "registry/familis/ui/hooks/use-speech-recognition.ts",
      "content": "\"use client\"\n\nimport {\n  useCallback,\n  useEffect,\n  useEffectEvent,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\"\n\nexport type SpeechRecognitionErrorCode =\n  | \"aborted\"\n  | \"audio-capture\"\n  | \"language-not-supported\"\n  | \"network\"\n  | \"no-speech\"\n  | \"not-allowed\"\n  | \"service-not-allowed\"\n\nexport interface SpeechRecognitionOptions {\n  /** BCP 47 language tag. Defaults to the document language. */\n  lang?: string\n  /** Keep listening after the first final result until `stop` is called. */\n  continuous?: boolean\n  /** Expose partial results through `interimTranscript` while the user speaks. */\n  interimResults?: boolean\n  /** Called with each final phrase, for example to append it to a form field. */\n  onResult?: (transcript: string) => void\n  /** Called when recognition fails. Manual aborts are not reported. */\n  onError?: (error: SpeechRecognitionErrorCode) => void\n}\n\nexport interface SpeechRecognitionState {\n  isSupported: boolean\n  isListening: boolean\n  /** Final phrases recognized since the last reset, separated by spaces. */\n  transcript: string\n  /** The phrase still being recognized; empty unless `interimResults` is enabled. */\n  interimTranscript: string\n  error: SpeechRecognitionErrorCode | null\n  start: () => void\n  stop: () => void\n  abort: () => void\n  reset: () => void\n}\n\n// TypeScript's DOM library omits the recognizer itself, so the hook declares the subset it uses.\ninterface RecognitionAlternative {\n  transcript: string\n}\n\ninterface RecognitionResult extends ArrayLike<RecognitionAlternative> {\n  isFinal: boolean\n}\n\ninterface RecognitionResultEvent extends Event {\n  resultIndex: number\n  results: ArrayLike<RecognitionResult>\n}\n\ninterface RecognitionErrorEvent extends Event {\n  error: SpeechRecognitionErrorCode\n}\n\ninterface Recognition extends EventTarget {\n  lang: string\n  continuous: boolean\n  interimResults: boolean\n  start: () => void\n  stop: () => void\n  abort: () => void\n}\n\ntype RecognitionConstructor = new () => Recognition\n\n/**\n * Resolves the standard or WebKit-prefixed recognizer exposed by the browser.\n *\n * @returns The recognizer constructor, or `undefined` when the browser has none.\n */\nfunction getRecognitionConstructor(): RecognitionConstructor | undefined {\n  const scope = window as Window & {\n    SpeechRecognition?: RecognitionConstructor\n    webkitSpeechRecognition?: RecognitionConstructor\n  }\n  return scope.SpeechRecognition ?? scope.webkitSpeechRecognition\n}\n\n/**\n * Support never changes during a page's lifetime, so there is nothing to subscribe to.\n *\n * @returns A no-op unsubscribe function.\n */\nconst subscribe = () => () => {}\n\n/**\n * Reads support on the client after hydration.\n *\n * @returns Whether the browser exposes a speech recognizer.\n */\nconst getSnapshot = () => getRecognitionConstructor() !== undefined\n\n/**\n * Reports no support on the server so the first client render matches the markup.\n *\n * @returns Always `false`.\n */\nconst getServerSnapshot = () => false\n\n/**\n * Transcribes microphone input with the browser's Web Speech API.\n *\n * @param options - Recognition settings and callbacks. Setting changes apply on the next `start`.\n * @param options.lang - BCP 47 language tag. Defaults to the document language.\n * @param options.continuous - Keep listening after the first final result. Defaults to `false`.\n * @param options.interimResults - Expose partial results while the user speaks. Defaults to `false`.\n * @param options.onResult - Called with each final phrase.\n * @param options.onError - Called when recognition fails, except for manual aborts.\n * @returns Support and listening flags, transcripts, the latest error, and session controls.\n */\nexport function useSpeechRecognition({\n  lang = \"\",\n  continuous = false,\n  interimResults = false,\n  onResult,\n  onError,\n}: SpeechRecognitionOptions = {}): SpeechRecognitionState {\n  const isSupported = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n  const recognitionRef = useRef<Recognition | null>(null)\n  const [isListening, setIsListening] = useState(false)\n  const [transcript, setTranscript] = useState(\"\")\n  const [interimTranscript, setInterimTranscript] = useState(\"\")\n  const [error, setError] = useState<SpeechRecognitionErrorCode | null>(null)\n\n  const handleResult = useEffectEvent((event: RecognitionResultEvent) => {\n    let finalText = \"\"\n    let interimText = \"\"\n    // Results before resultIndex are final and were already appended by an earlier event.\n    for (let index = event.resultIndex; index < event.results.length; index++) {\n      const result = event.results[index]\n      if (result.isFinal) finalText += result[0].transcript\n      else interimText += result[0].transcript\n    }\n\n    setInterimTranscript(interimText.trim())\n    const phrase = finalText.trim()\n    if (!phrase) return\n    setTranscript((previous) => (previous ? `${previous} ${phrase}` : phrase))\n    onResult?.(phrase)\n  })\n\n  const handleError = useEffectEvent((event: RecognitionErrorEvent) => {\n    if (event.error === \"aborted\") return\n    setError(event.error)\n    onError?.(event.error)\n  })\n\n  useEffect(() => {\n    const Recognition = isSupported ? getRecognitionConstructor() : undefined\n    if (!Recognition) return\n\n    const recognition = new Recognition()\n    const onStart = () => setIsListening(true)\n    const onEnd = () => {\n      setIsListening(false)\n      setInterimTranscript(\"\")\n    }\n    const onResultEvent = (event: Event) => handleResult(event as RecognitionResultEvent)\n    const onErrorEvent = (event: Event) => handleError(event as RecognitionErrorEvent)\n\n    recognition.addEventListener(\"start\", onStart)\n    recognition.addEventListener(\"end\", onEnd)\n    recognition.addEventListener(\"result\", onResultEvent)\n    recognition.addEventListener(\"error\", onErrorEvent)\n    recognitionRef.current = recognition\n\n    return () => {\n      recognitionRef.current = null\n      // Remove listeners first so the abort does not update an unmounted component.\n      recognition.removeEventListener(\"start\", onStart)\n      recognition.removeEventListener(\"end\", onEnd)\n      recognition.removeEventListener(\"result\", onResultEvent)\n      recognition.removeEventListener(\"error\", onErrorEvent)\n      recognition.abort()\n      setIsListening(false)\n    }\n  }, [isSupported])\n\n  const start = useCallback(() => {\n    const recognition = recognitionRef.current\n    if (!recognition) return\n    recognition.lang = lang\n    recognition.continuous = continuous\n    recognition.interimResults = interimResults\n    setError(null)\n    try {\n      recognition.start()\n    } catch (startError) {\n      // Starting an active session throws; treat repeated starts as a no-op.\n      if (!(startError instanceof DOMException && startError.name === \"InvalidStateError\")) {\n        throw startError\n      }\n    }\n  }, [lang, continuous, interimResults])\n\n  const stop = useCallback(() => recognitionRef.current?.stop(), [])\n  const abort = useCallback(() => recognitionRef.current?.abort(), [])\n\n  const reset = useCallback(() => {\n    setTranscript(\"\")\n    setInterimTranscript(\"\")\n    setError(null)\n  }, [])\n\n  return {\n    isSupported,\n    isListening,\n    transcript,\n    interimTranscript,\n    error,\n    start,\n    stop,\n    abort,\n    reset,\n  }\n}\n",
      "type": "registry:hook"
    }
  ],
  "meta": {
    "storybookId": "ui-hooks-speech-recognition--default"
  },
  "type": "registry:hook"
}