{"version":3,"file":"use-push-to-talk.cjs","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n    /** `KeyboardEvent.code`, which is layout-independent. */\n    code: string;\n    /** What to show a person. `code` itself is not a label. */\n    label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n    { code: \"Space\", label: \"Espaço\" },\n    { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n    { code: \"AltLeft\", label: \"Alt esquerdo\" },\n    { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n    { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n *     showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n    return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n    if (!(target instanceof HTMLElement)) return false;\n    if (target.isContentEditable) return true;\n    const tag = target.tagName;\n    return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n    /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n    code?: string;\n    /** Called once when the key goes down. */\n    onDown: () => void;\n    /** Called when the key comes up, the window blurs, or the hook unmounts. */\n    onUp: () => void;\n    /**\n     * Whether the binding is live. Default `true`.\n     *\n     * Turning it off releases first, so flipping a call from push-to-talk to an\n     * open microphone while the key is held does not leave `onUp` unfired.\n     */\n    enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n *    browser never sees the `keyup`, and the microphone stays open for as long as\n *    the person is looking at another window — exactly what push-to-talk exists\n *    to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n *    and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n *    `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n *     code: \"Space\",\n *     onDown: () => setMicEnabled(true),\n *     onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n    code = DEFAULT_PUSH_TO_TALK_KEY,\n    onDown,\n    onUp,\n    enabled = true,\n}: UsePushToTalkOptions): void {\n    const callbacks = useRef({ onDown, onUp });\n    useEffect(() => {\n        callbacks.current = { onDown, onUp };\n    });\n\n    useEffect(() => {\n        if (!enabled || typeof window === \"undefined\") return;\n        let held = false;\n\n        const release = (): void => {\n            if (!held) return;\n            held = false;\n            callbacks.current.onUp();\n        };\n\n        const handleKeyDown = (event: KeyboardEvent): void => {\n            if (event.code !== code || event.repeat) return;\n            if (isTypingTarget(event.target)) return;\n            event.preventDefault();\n            if (held) return;\n            held = true;\n            callbacks.current.onDown();\n        };\n\n        /**\n         * Release on the way up, and never mind where the keystroke landed.\n         *\n         * The field guard belongs on the way **down** — it is what stops a\n         * push-to-talk bound to Space from opening the microphone every time\n         * somebody writes a message. On the way up it did the opposite of its\n         * job: `keyup` is delivered to whatever is focused when the key rises,\n         * not to what was focused when it fell, so clicking into an input while\n         * still holding sent the release into the guard and left `held` true.\n         * The microphone stayed open until the window blurred.\n         *\n         * `held` is the guard this needs. If we never opened the microphone\n         * there is nothing to close, and `release()` already returns on that —\n         * so the only thing left to decide is `preventDefault`, which is\n         * honest only for a keystroke we actually acted on.\n         */\n        const handleKeyUp = (event: KeyboardEvent): void => {\n            if (event.code !== code || !held) return;\n            event.preventDefault();\n            release();\n        };\n\n        window.addEventListener(\"keydown\", handleKeyDown);\n        window.addEventListener(\"keyup\", handleKeyUp);\n        window.addEventListener(\"blur\", release);\n\n        return () => {\n            window.removeEventListener(\"keydown\", handleKeyDown);\n            window.removeEventListener(\"keyup\", handleKeyUp);\n            window.removeEventListener(\"blur\", release);\n            release();\n        };\n    }, [code, enabled]);\n}\n"],"mappings":"uBAsBA,IAAa,EAA8C,CACvD,CAAE,KAAM,QAAS,MAAO,QAAS,EACjC,CAAE,KAAM,cAAe,MAAO,eAAgB,EAC9C,CAAE,KAAM,UAAW,MAAO,cAAe,EACzC,CAAE,KAAM,YAAa,MAAO,gBAAiB,EAC7C,CAAE,KAAM,YAAa,MAAO,WAAY,CAC5C,EAGa,EAA2B,QASxC,SAAgB,EAAmB,EAAsB,CACrD,OAAO,EAAkB,KAAM,GAAQ,EAAI,OAAS,CAAI,CAAC,EAAE,OAAS,CACxE,CAYA,SAAS,EAAe,EAAqC,CACzD,GAAI,EAAE,aAAkB,aAAc,MAAO,GAC7C,GAAI,EAAO,kBAAmB,MAAO,GACrC,IAAM,EAAM,EAAO,QACnB,OAAO,IAAQ,SAAW,IAAQ,YAAc,IAAQ,QAC5D,CAmDA,SAAgB,EAAc,CAC1B,OAAO,EACP,SACA,OACA,UAAU,IACiB,CAC3B,IAAM,GAAA,EAAY,EAAA,OAAA,CAAO,CAAE,SAAQ,MAAK,CAAC,GACzC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,QAAU,CAAE,SAAQ,MAAK,CACvC,CAAC,GAED,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,OAAO,OAAW,IAAa,OAC/C,IAAI,EAAO,GAEL,MAAsB,CACnB,IACL,EAAO,GACP,EAAU,QAAQ,KAAK,EAC3B,EAEM,EAAiB,GAA+B,CAC9C,EAAM,OAAS,GAAQ,EAAM,QAC7B,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACjB,KACJ,EAAO,GACP,EAAU,QAAQ,OAAO,GAC7B,EAkBM,EAAe,GAA+B,CAC5C,EAAM,OAAS,GAAS,IAC5B,EAAM,eAAe,EACrB,EAAQ,EACZ,EAMA,OAJA,OAAO,iBAAiB,UAAW,CAAa,EAChD,OAAO,iBAAiB,QAAS,CAAW,EAC5C,OAAO,iBAAiB,OAAQ,CAAO,MAE1B,CACT,OAAO,oBAAoB,UAAW,CAAa,EACnD,OAAO,oBAAoB,QAAS,CAAW,EAC/C,OAAO,oBAAoB,OAAQ,CAAO,EAC1C,EAAQ,CACZ,CACJ,EAAG,CAAC,EAAM,CAAO,CAAC,CACtB"}