{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-focus-trap",
  "type": "registry:hook",
  "title": "useFocusTrap",
  "description": "Trap focus inside an overlay while it is open, and restore it to the trigger on close.",
  "categories": [
    "hooks",
    "accessibility"
  ],
  "files": [
    {
      "path": "hooks/use-focus-trap.ts",
      "type": "registry:hook",
      "target": "hooks/use-focus-trap.ts",
      "content": "import { useEffect, type RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]\",\n  \"button:not([disabled])\",\n  \"input:not([disabled])\",\n  \"select:not([disabled])\",\n  \"textarea:not([disabled])\",\n  '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\n/**\n * Traps Tab focus within `ref` while `active`, moving focus into the container\n * on activate and restoring it to the previously-focused element on deactivate.\n * Use for modal dialogs and menus (BottomDrawer, the MobileBottomNav menu).\n *\n * The container should be focusable as a fallback (give it `tabIndex={-1}`) for\n * the case where it has no focusable children yet.\n *\n * `initialFocus` controls where focus lands on activate:\n * - `'first'` (default): the first focusable child -- right for menus, where\n *   the user is about to act on an item.\n * - `'container'`: the container itself -- right for form drawers, so opening\n *   one does NOT auto-focus a text field and pop the mobile keyboard (which\n *   obscures the rest of the form). The container still receives focus for\n *   screen readers; Tab then reaches the fields.\n */\nexport function useFocusTrap(\n  ref: RefObject<HTMLElement | null>,\n  active: boolean,\n  options?: { initialFocus?: \"first\" | \"container\" },\n): void {\n  const initialFocus = options?.initialFocus ?? \"first\";\n  useEffect(() => {\n    if (!active) return;\n    const container = ref.current;\n    if (!container) return;\n\n    const previouslyFocused = document.activeElement as HTMLElement | null;\n\n    const focusable = () =>\n      Array.from(\n        container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),\n      ).filter((el) => el.offsetParent !== null || el === document.activeElement);\n\n    // Move focus into the container.\n    if (initialFocus === \"container\") {\n      container.focus();\n    } else {\n      (focusable()[0] ?? container).focus();\n    }\n\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key !== \"Tab\") return;\n      const items = focusable();\n      if (items.length === 0) {\n        e.preventDefault();\n        return;\n      }\n      const first = items[0];\n      const last = items[items.length - 1];\n      if (e.shiftKey && document.activeElement === first) {\n        e.preventDefault();\n        last.focus();\n      } else if (!e.shiftKey && document.activeElement === last) {\n        e.preventDefault();\n        first.focus();\n      }\n    };\n\n    document.addEventListener(\"keydown\", onKeyDown, true);\n    return () => {\n      document.removeEventListener(\"keydown\", onKeyDown, true);\n      // Restore focus to whatever was focused before the trap activated.\n      previouslyFocused?.focus?.();\n    };\n  }, [active, ref, initialFocus]);\n}\n"
    }
  ],
  "docs": "BottomDrawer uses this already. For a custom overlay, pass initialFocus: 'container' when the overlay holds a multi-field form, so opening it does not focus a text field and pop the mobile keyboard. Tab still reaches every field.",
  "meta": {
    "group": "hooks",
    "related": [
      "bottom-drawer",
      "use-route-focus"
    ],
    "exports": [
      "useFocusTrap"
    ],
    "siteSlug": "use-focus-trap"
  }
}
