{"version":3,"file":"TTSReader-C-gjoyvb.cjs","sources":["../app/components/tts/TTSReader.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\ntype VoiceOption = {\n  name: string;\n  lang: string;\n  voice: SpeechSynthesisVoice;\n};\n\nexport default function TTSReader() {\n  const supportsTTS =\n    typeof window !== \"undefined\" && \"speechSynthesis\" in window;\n\n  const [text, setText] = useState(\"\");\n  const [isSpeaking, setIsSpeaking] = useState(false);\n  const [isPaused, setIsPaused] = useState(false);\n  const [voices, setVoices] = useState<VoiceOption[]>([]);\n  const [selectedVoice, setSelectedVoice] = useState<string>(\"\");\n  const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);\n\n  // Load available voices\n  useEffect(() => {\n    if (!supportsTTS) return;\n\n    function refreshVoices() {\n      const list = window.speechSynthesis.getVoices();\n      const options: VoiceOption[] = list.map((v) => ({\n        name: v.name,\n        lang: v.lang,\n        voice: v,\n      }));\n      setVoices(options);\n      if (!selectedVoice && options.length > 0) {\n        // Prefer an English voice by default if available\n        const preferred =\n          options.find((v) => v.lang.toLowerCase().startsWith(\"en\")) ||\n          options[0];\n        setSelectedVoice(preferred.name);\n      }\n    }\n\n    refreshVoices();\n    window.speechSynthesis.addEventListener(\"voiceschanged\", refreshVoices);\n    return () =>\n      window.speechSynthesis.removeEventListener(\n        \"voiceschanged\",\n        refreshVoices\n      );\n  }, [supportsTTS, selectedVoice]);\n\n  // Keep text synced with current selection if any\n  useEffect(() => {\n    function handleSelectionChange() {\n      const sel = window.getSelection?.();\n      const value = sel?.toString() ?? \"\";\n      if (value && value.trim().length > 0) {\n        setText(value.trim());\n      }\n    }\n    document.addEventListener(\"selectionchange\", handleSelectionChange);\n    return () =>\n      document.removeEventListener(\"selectionchange\", handleSelectionChange);\n  }, []);\n\n  const currentVoice = useMemo(\n    () => voices.find((v) => v.name === selectedVoice)?.voice,\n    [voices, selectedVoice]\n  );\n\n  const canSpeak = supportsTTS && text.trim().length > 0;\n\n  function stopSpeaking() {\n    try {\n      window.speechSynthesis.cancel();\n    } catch {}\n    utteranceRef.current = null;\n    setIsSpeaking(false);\n    setIsPaused(false);\n  }\n\n  function speak() {\n    if (!supportsTTS || !canSpeak) return;\n    stopSpeaking();\n    const u = new SpeechSynthesisUtterance(text);\n    if (currentVoice) u.voice = currentVoice;\n    u.onstart = () => setIsSpeaking(true);\n    u.onend = () => {\n      setIsSpeaking(false);\n      setIsPaused(false);\n      utteranceRef.current = null;\n    };\n    u.onerror = () => {\n      setIsSpeaking(false);\n      setIsPaused(false);\n      utteranceRef.current = null;\n    };\n    utteranceRef.current = u;\n    window.speechSynthesis.speak(u);\n  }\n\n  function pause() {\n    if (!supportsTTS) return;\n    try {\n      window.speechSynthesis.pause();\n      setIsPaused(true);\n    } catch {}\n  }\n\n  function resume() {\n    if (!supportsTTS) return;\n    try {\n      window.speechSynthesis.resume();\n      setIsPaused(false);\n    } catch {}\n  }\n\n  if (!supportsTTS) {\n    return (\n      <div className=\"p-4 text-sm\">\n        Your browser does not support Text-to-Speech.\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex h-full w-full flex-col gap-3 p-3\">\n      <div className=\"flex items-center gap-2\">\n        <label className=\"text-sm text-foreground/80\">Voice</label>\n        <select\n          className=\"min-w-[220px] rounded-md border border-border bg-surface px-2 py-1 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n          value={selectedVoice}\n          onChange={(e) => setSelectedVoice(e.target.value)}\n        >\n          {voices.map((v) => (\n            <option\n              key={v.name}\n              value={v.name}\n            >{`${v.name} (${v.lang})`}</option>\n          ))}\n        </select>\n      </div>\n\n      <div className=\"flex-1\">\n        <label htmlFor=\"tts-textarea\" className=\"sr-only\">\n          Text to read\n        </label>\n        <textarea\n          id=\"tts-textarea\"\n          className=\"h-full w-full resize-none rounded-lg border border-border bg-background p-3 text-sm leading-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n          placeholder=\"Select text on the page to auto-fill, or type/paste here...\"\n          value={text}\n          onChange={(e) => setText(e.target.value)}\n        />\n      </div>\n\n      <div className=\"flex items-center justify-between gap-2\">\n        <div className=\"flex gap-2\">\n          <button\n            className=\"h-9 rounded-md bg-accent px-4 text-sm text-accent-foreground hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n            onClick={speak}\n            disabled={!canSpeak || isSpeaking}\n            aria-disabled={!canSpeak || isSpeaking}\n            aria-label=\"Play\"\n          >\n            Play\n          </button>\n          <button\n            className=\"h-9 rounded-md bg-surface px-4 text-sm hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n            onClick={isPaused ? resume : pause}\n            disabled={!isSpeaking}\n            aria-disabled={!isSpeaking}\n            aria-label={isPaused ? \"Resume\" : \"Pause\"}\n          >\n            {isPaused ? \"Resume\" : \"Pause\"}\n          </button>\n          <button\n            className=\"h-9 rounded-md bg-surface px-4 text-sm hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n            onClick={stopSpeaking}\n            disabled={!isSpeaking && !isPaused}\n            aria-disabled={!isSpeaking && !isPaused}\n            aria-label=\"Stop\"\n          >\n            Stop\n          </button>\n        </div>\n\n        <div className=\"text-xs text-foreground/70\">\n          {isSpeaking\n            ? isPaused\n              ? \"Paused\"\n              : \"Playing\"\n            : text\n            ? \"Ready\"\n            : \"Enter or select text\"}\n        </div>\n      </div>\n    </div>\n  );\n}\n"],"names":["TTSReader","supportsTTS","text","setText","useState","isSpeaking","setIsSpeaking","isPaused","setIsPaused","voices","setVoices","selectedVoice","setSelectedVoice","utteranceRef","useRef","useEffect","refreshVoices","options","v","preferred","handleSelectionChange","value","currentVoice","useMemo","canSpeak","stopSpeaking","speak","u","pause","resume","jsxs","jsx"],"mappings":"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"}