{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-keyboard-shortcut",
  "title": "useKeyboardShortcut",
  "description": "A hook that binds human-readable key combos like mod+k to a handler, with platform-aware modifiers and an input-focus guard.",
  "files": [
    {
      "path": "src/registry/hooks/use-keyboard-shortcut.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport interface UseKeyboardShortcutOptions {\n  /** Turn the listener off (e.g. a shortcut that only applies while a panel is open). Defaults to true. */\n  enabled?: boolean\n  /** Call `event.preventDefault()` on a match, blocking the browser's own behaviour (⌘S saves the page, ⌘K jumps to the address bar). Defaults to true. */\n  preventDefault?: boolean\n  /** Don't fire while focus is in an input / textarea / select / contenteditable. Defaults to true. */\n  ignoreInputs?: boolean\n  /**\n   * Where to attach the listener, `window` by default. Pass a node, or a ref object — the latter is\n   * the only reliable way to scope a shortcut to a container: `ref.current` is still null on the\n   * first render and assigning to a ref triggers no re-render, so passing `ref.current` leaves the\n   * listener on window forever.\n   */\n  target?: EventTarget | React.RefObject<EventTarget | null> | null\n}\n\ninterface ParsedShortcut {\n  key: string\n  mod: boolean\n  ctrl: boolean\n  meta: boolean\n  shift: boolean\n  alt: boolean\n}\n\n/** Human spelling → the lower-cased `KeyboardEvent.key` value. */\nconst KEY_ALIASES: Record<string, string> = {\n  esc: \"escape\",\n  space: \" \",\n  spacebar: \" \",\n  plus: \"+\",\n  up: \"arrowup\",\n  down: \"arrowdown\",\n  left: \"arrowleft\",\n  right: \"arrowright\",\n  return: \"enter\",\n  del: \"delete\",\n}\n\n/** Parses \"mod+shift+k\" into a structured shortcut; returns null when only modifiers were given and no actual key. */\nfunction parseShortcut(combo: string): ParsedShortcut | null {\n  const parsed: ParsedShortcut = {\n    key: \"\",\n    mod: false,\n    ctrl: false,\n    meta: false,\n    shift: false,\n    alt: false,\n  }\n\n  const parts = combo\n    .toLowerCase()\n    .split(\"+\")\n    .map(part => part.trim())\n    .filter(Boolean)\n\n  for (const part of parts) {\n    switch (part) {\n      case \"mod\":\n        parsed.mod = true\n        break\n      case \"ctrl\":\n      case \"control\":\n        parsed.ctrl = true\n        break\n      case \"meta\":\n      case \"cmd\":\n      case \"command\":\n        parsed.meta = true\n        break\n      case \"shift\":\n        parsed.shift = true\n        break\n      case \"alt\":\n      case \"option\":\n        parsed.alt = true\n        break\n      default:\n        parsed.key = KEY_ALIASES[part] ?? part\n    }\n  }\n\n  return parsed.key ? parsed : null\n}\n\n/** `mod` is ⌘ on Apple platforms and Ctrl elsewhere. Called from effects only — navigator is never read during render. */\nfunction isApplePlatform(): boolean {\n  if (typeof navigator === \"undefined\") return false\n  return /mac|iphone|ipad|ipod/i.test(navigator.userAgent)\n}\n\nfunction isEditableTarget(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/**\n * Binds one combo (or a set of them) to a callback. `keys` takes human spellings like `\"mod+k\"`,\n * `\"shift+?\"` or `\"escape\"`; `mod` resolves per platform to ⌘ or Ctrl, and the whole string is\n * case-insensitive. Pass an array to give one action several bindings (`[\"mod+k\", \"mod+/\"]`).\n *\n * - **Matching goes through `event.key`**, not the deprecated `keyCode`; modifiers are matched\n *   **exactly**: `\"escape\"` won't fire while Shift is held, and ⌘⇧K won't be mistaken for `\"mod+k\"`.\n * - **SSR-safe**: `window` / `navigator` are only touched inside effects — zero browser APIs during render.\n * - **The handler lives in a ref**: an inline arrow prop no longer rebinds the listener on every\n *   render; `keys` is serialized into a string dependency for the same reason, so an inline array\n *   doesn't thrash it either.\n * - **`target` accepts a ref object**: `{ target: panelRef }` is dereferenced inside the effect to\n *   scope the shortcut to a container; pass `panelRef.current` and it is null on the first frame, so\n *   the listener silently lands on window and never moves.\n * - Every `keydown` listener is removed on unmount and when `enabled` goes false.\n */\nexport function useKeyboardShortcut(\n  keys: string | string[],\n  handler: (event: KeyboardEvent) => void,\n  options: UseKeyboardShortcutOptions = {},\n): void {\n  const { enabled = true, preventDefault = true, ignoreInputs = true, target } = options\n\n  const handlerRef = React.useRef(handler)\n  React.useEffect(() => {\n    handlerRef.current = handler\n  })\n\n  // an inline array literal has a fresh identity every render; serialized, unchanged content means no rebind.\n  const keysKey = Array.isArray(keys) ? keys.join(\"|\") : keys\n\n  React.useEffect(() => {\n    if (!enabled) return\n\n    const shortcuts = keysKey\n      .split(\"|\")\n      .map(parseShortcut)\n      .filter((shortcut): shortcut is ParsedShortcut => shortcut !== null)\n    if (shortcuts.length === 0) return\n\n    // the ref object is dereferenced here in the effect: the DOM is mounted by now, so `current` is\n    // a real node, while the ref object's own identity is stable and never rebinds the listener.\n    const resolved = target instanceof EventTarget ? target : target?.current\n    const node: EventTarget = resolved ?? window\n    const appleMod = isApplePlatform()\n\n    const matches = (event: KeyboardEvent, shortcut: ParsedShortcut) => {\n      if (event.key.toLowerCase() !== shortcut.key) return false\n      // once mod expands to this platform's real modifier, all four are compared exactly.\n      const wantMeta = shortcut.meta || (shortcut.mod && appleMod)\n      const wantCtrl = shortcut.ctrl || (shortcut.mod && !appleMod)\n      return (\n        event.metaKey === wantMeta &&\n        event.ctrlKey === wantCtrl &&\n        event.shiftKey === shortcut.shift &&\n        event.altKey === shortcut.alt\n      )\n    }\n\n    const listener = (event: Event) => {\n      const keyboardEvent = event as KeyboardEvent\n      if (ignoreInputs && isEditableTarget(keyboardEvent.target)) return\n      if (!shortcuts.some(shortcut => matches(keyboardEvent, shortcut))) return\n      if (preventDefault) keyboardEvent.preventDefault()\n      handlerRef.current(keyboardEvent)\n    }\n\n    node.addEventListener(\"keydown\", listener)\n    return () => node.removeEventListener(\"keydown\", listener)\n  }, [enabled, keysKey, preventDefault, ignoreInputs, target])\n}\n\nexport default useKeyboardShortcut\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}