{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "type": "registry:block",
  "title": "Command Palette",
  "description": "⌘K palette with fuzzy filter, spring-animated active row and glass surface.",
  "author": "Saurabh <saurabh10102@gmail.com>",
  "dependencies": [
    "clsx",
    "lucide-react",
    "motion",
    "tailwind-merge"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/motion/command-palette.tsx",
      "type": "registry:component",
      "target": "@components/motion/command-palette.tsx",
      "content": "\"use client\";\n// asharca.github.io/ui/components/blocks/command-palette\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Search, type LucideIcon } from \"lucide-react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { useOnOpen } from \"@/lib/hooks/use-on-open\";\nimport { useRowCursor } from \"@/lib/hooks/use-row-cursor\";\nimport { useTouchCapable } from \"@/lib/hooks/use-touch-capable\";\nimport { PresenceGate } from \"@/lib/presence-gate\";\nimport { cn } from \"@/lib/utils\";\nimport { searchCommands } from \"@/lib/command-search\";\n\nexport type CommandItem = {\n  id: string;\n  label: string;\n  group?: string;\n  hint?: string;\n  keywords?: string[];\n  icon?: LucideIcon;\n  badge?: ReactNode;\n  onSelect: () => void;\n};\n\nexport interface CommandPaletteProps {\n  items: CommandItem[];\n  /** Opens with Cmd/Ctrl + this key. Default: \"k\" */\n  shortcut?: string;\n  placeholder?: string;\n  emptyMessage?: string;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\n// Opened via a keyboard shortcut many times a day — entrance must read as\n// instant. Tight spring, even faster exit.\nconst PANEL_SPRING = {\n  type: \"spring\",\n  stiffness: 560,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\nexport function CommandPalette({\n  items,\n  shortcut = \"k\",\n  placeholder = \"Type a command or search…\",\n  emptyMessage = \"No results found.\",\n  open: controlledOpen,\n  onOpenChange,\n}: CommandPaletteProps) {\n  const [internalOpen, setInternalOpen] = useState(false);\n  const controlled = controlledOpen !== undefined;\n  const open = controlled ? controlledOpen : internalOpen;\n  const setOpen = useCallback(\n    (v: boolean) => {\n      if (!controlled) setInternalOpen(v);\n      onOpenChange?.(v);\n    },\n    [controlled, onOpenChange],\n  );\n\n  const [query, setQuery] = useState(\"\");\n  // Portal target only exists client-side; render nothing during SSR/hydration.\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  const uid = useId();\n  const reduce = useReducedMotion();\n  const canTouch = useTouchCapable();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (\n        (e.metaKey || e.ctrlKey) &&\n        e.key.toLowerCase() === shortcut.toLowerCase()\n      ) {\n        e.preventDefault();\n        setOpen(!open);\n        return;\n      }\n      if (e.key === \"Escape\" && open) {\n        e.preventDefault();\n        setOpen(false);\n      }\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [open, shortcut, setOpen]);\n\n  useEffect(() => {\n    if (!open) return;\n    const root = document.documentElement;\n    const previousRootOverflow = root.style.overflow;\n    const previousBodyOverflow = document.body.style.overflow;\n    root.style.overflow = \"hidden\";\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      root.style.overflow = previousRootOverflow;\n      document.body.style.overflow = previousBodyOverflow;\n    };\n  }, [open]);\n\n  const filtered = useMemo(() => searchCommands(items, query), [items, query]);\n\n  // Reserve the icon column only when at least one item brings an icon, so\n  // icon-less lists don't render a dead gap before every label.\n  const hasIcons = useMemo(() => items.some((it) => it.icon), [items]);\n\n  const grouped = useMemo(() => {\n    const map = new Map<string, CommandItem[]>();\n    filtered.forEach((it) => {\n      const g = it.group ?? \"Results\";\n      const groupItems = map.get(g) ?? [];\n      groupItems.push(it);\n      map.set(g, groupItems);\n    });\n    return Array.from(map.entries());\n  }, [filtered]);\n\n  // Grouping reorders the list, so the rendered order is not the filtered\n  // order whenever two groups interleave. Everything that has to agree on\n  // \"which row\" — the highlight, the ids, Enter, the scroll — reads this one\n  // array, so they cannot drift apart.\n  const rows = useMemo(() => grouped.flatMap(([, list]) => list), [grouped]);\n\n  const { activeIndex: active, moveTo, moveActive } = useRowCursor(rows, query);\n\n  // Clearing the query would drop the cursor on its own, but only if it had\n  // changed; `moveTo(null)` covers reopening on an already-empty query.\n  useOnOpen(open, () => {\n    setQuery(\"\");\n    moveTo(null);\n  });\n\n  useEffect(() => {\n    if (!open) return;\n    const frame = requestAnimationFrame(() => inputRef.current?.focus());\n    return () => cancelAnimationFrame(frame);\n  }, [open]);\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      moveActive(1);\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      moveActive(-1);\n    } else if (e.key === \"Enter\") {\n      e.preventDefault();\n      const it = rows[active];\n      if (it) {\n        it.onSelect();\n        setOpen(false);\n      }\n    }\n  };\n\n  useEffect(() => {\n    if (!open) return;\n    const el = listRef.current?.querySelector<HTMLButtonElement>(\n      `[data-index=\"${active}\"]`,\n    );\n    el?.scrollIntoView({ block: \"nearest\" });\n  }, [active, open]);\n\n  if (!mounted) return null;\n\n  // Portaled to <body> so ancestors with transforms, filters, or fixed\n  // positioning can't trap the overlay in their stacking context, and mounted\n  // only while open. The chrome is two fixed siblings rather than one wrapper:\n  // the backdrop spans the viewport edges but carries the scrim colour, and the\n  // layer positioning the panel is inset off every edge. Both hang off\n  // `PresenceGate`, so interaction releases in the same commit that starts the\n  // exit rather than when it ends — `open` is already false for those frames.\n  // See tests/fixed-overlay-edge-sampling.test.tsx.\n  return createPortal(\n    <AnimatePresence initial={false}>\n      {open ? (\n        <PresenceGate key=\"backdrop\">\n          {({ gate }) => (\n            <motion.button\n              type=\"button\"\n              aria-label=\"Close command palette\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{\n                opacity: 0,\n                transition: { duration: 0.12, ease: EASE_OUT },\n              }}\n              transition={{ duration: 0.18, ease: EASE_OUT }}\n              {...gate}\n              onClick={() => setOpen(false)}\n              className=\"pointer-events-auto fixed inset-0 z-[100] bg-background/5 [backdrop-filter:blur(12px)_saturate(140%)] [-webkit-backdrop-filter:blur(12px)_saturate(140%)]\"\n            />\n          )}\n        </PresenceGate>\n      ) : null}\n\n      {open ? (\n        <PresenceGate key=\"panel-layer\">\n          {({ isPresent, gate }) => (\n            // The layer itself never takes pointer events, so it carries\n            // `inert` alone rather than the gate's pointer-events value.\n            <div\n              inert={!isPresent}\n              className=\"pointer-events-none fixed inset-x-4 bottom-4 top-[18vh] z-[100] flex items-start justify-center\"\n            >\n              <motion.div\n                role=\"dialog\"\n                aria-modal=\"true\"\n                aria-label=\"Command palette\"\n                initial={{\n                  opacity: 0,\n                  y: reduce ? 0 : -8,\n                  scale: reduce ? 1 : 0.97,\n                }}\n                animate={{ opacity: 1, y: 0, scale: 1 }}\n                exit={{\n                  opacity: 0,\n                  y: reduce ? 0 : -8,\n                  scale: reduce ? 1 : 0.97,\n                  transition: { duration: 0.12, ease: EASE_OUT },\n                }}\n                transition={reduce ? { duration: 0.1 } : PANEL_SPRING}\n                {...gate}\n                onKeyDown={onKeyDown}\n                className=\"pointer-events-auto w-full max-w-xl overflow-hidden rounded-2xl border border-border bg-card shadow-2xl will-change-transform\"\n              >\n                <div className=\"flex items-center gap-3 border-b border-border px-4\">\n                  <Search className=\"h-4 w-4 text-muted-foreground\" />\n                  <input\n                    ref={inputRef}\n                    value={query}\n                    onChange={(e) => setQuery(e.target.value)}\n                    placeholder={placeholder}\n                    role=\"combobox\"\n                    // The field only exists while the palette is open.\n                    aria-expanded=\"true\"\n                    aria-controls={`${uid}-list`}\n                    aria-activedescendant={\n                      rows.length > 0 ? `${uid}-opt-${active}` : undefined\n                    }\n                    aria-autocomplete=\"list\"\n                    className={cn(\n                      \"h-12 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none\",\n                      // The palette focuses this field the moment it opens, and iOS\n                      // zooms the page in on a focused field under 16px: the fixed\n                      // overlay is magnified off-center — clipped leading edge, half\n                      // an icon column — and the zoom outlives the palette. 16px on\n                      // touch keeps the page at scale 1; pointer devices keep 14px.\n                      canTouch && \"text-base\",\n                    )}\n                  />\n                  <kbd className=\"hidden rounded border border-border bg-background px-1.5 py-0.5 text-[10px] text-muted-foreground sm:inline-block\">\n                    ESC\n                  </kbd>\n                </div>\n                <div\n                  ref={listRef}\n                  id={`${uid}-list`}\n                  role=\"listbox\"\n                  aria-label=\"Commands\"\n                  className=\"max-h-[60vh] overflow-y-auto overscroll-contain p-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n                >\n                  {rows.length === 0 ? (\n                    <div className=\"p-8 text-center text-sm text-muted-foreground\">\n                      {emptyMessage}\n                    </div>\n                  ) : (\n                    grouped.map(([group, list]) => (\n                      <div key={group} className=\"mb-1 last:mb-0\">\n                        <div\n                          aria-hidden\n                          className=\"px-2 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\"\n                        >\n                          {group}\n                        </div>\n                        {list.map((it) => {\n                          // `rows` holds these very objects, in render order.\n                          const idx = rows.indexOf(it);\n                          const isActive = idx === active;\n                          const Icon = it.icon;\n                          return (\n                            <button\n                              key={it.id}\n                              type=\"button\"\n                              id={`${uid}-opt-${idx}`}\n                              role=\"option\"\n                              aria-selected={isActive}\n                              data-index={idx}\n                              onMouseEnter={() => moveTo(it.id)}\n                              onClick={() => {\n                                it.onSelect();\n                                setOpen(false);\n                              }}\n                              className={cn(\n                                \"relative isolate flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-sm transition-colors\",\n                                isActive\n                                  ? \"text-foreground\"\n                                  : \"text-muted-foreground\",\n                              )}\n                            >\n                              {isActive ? (\n                                <motion.span\n                                  layoutId={`${uid}-active`}\n                                  className=\"absolute inset-0 z-0 rounded-md bg-muted/60\"\n                                  transition={\n                                    reduce\n                                      ? { duration: 0 }\n                                      : // Tracks rapid arrow-key navigation — keep it tighter\n                                        // than SPRING_LAYOUT so it never lags the active row.\n                                        {\n                                          type: \"spring\",\n                                          stiffness: 480,\n                                          damping: 38,\n                                        }\n                                  }\n                                />\n                              ) : null}\n                              {Icon ? (\n                                <Icon className=\"relative z-10 h-4 w-4\" />\n                              ) : hasIcons ? (\n                                <span className=\"relative z-10 h-4 w-4\" />\n                              ) : null}\n                              <span className=\"relative z-10 flex-1 truncate\">\n                                {it.label}\n                              </span>\n                              {it.badge ? (\n                                <span className=\"relative z-10 shrink-0\">\n                                  {it.badge}\n                                </span>\n                              ) : null}\n                              {it.hint ? (\n                                <kbd className=\"relative z-10 rounded border border-border bg-background px-1.5 py-0.5 text-[10px] text-muted-foreground\">\n                                  {it.hint}\n                                </kbd>\n                              ) : null}\n                            </button>\n                          );\n                        })}\n                      </div>\n                    ))\n                  )}\n                </div>\n              </motion.div>\n            </div>\n          )}\n        </PresenceGate>\n      ) : null}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n"
    },
    {
      "path": "lib/command-search.ts",
      "type": "registry:lib",
      "target": "@lib/command-search.ts",
      "content": "export type SearchableCommand = {\n  label: string;\n  group?: string;\n  keywords?: string[];\n};\n\nfunction normalize(value: string) {\n  return value\n    .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n    .normalize(\"NFKD\")\n    .replace(/\\p{M}/gu, \"\")\n    .toLowerCase()\n    .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n    .trim();\n}\n\nfunction tokenScore(token: string, field: string) {\n  const words = field.split(\" \");\n  if (words.includes(token)) return 100;\n  if (words.some((word) => word.startsWith(token))) return 80;\n  if (field.includes(token)) return 60;\n  // Keep short queries precise; fuzzy abbreviations may skip at most two\n  // letters within a word, rather than matching across a whole description.\n  if (token.length < 3) return 0;\n  for (const word of words) {\n    if (word.length - token.length > 2) continue;\n    let matched = 0;\n    for (const char of word) {\n      if (char === token[matched]) matched++;\n    }\n    if (matched === token.length) return 20;\n  }\n  return 0;\n}\n\n/** Match every query word across fields and put direct name matches first. */\nexport function searchCommands<T extends SearchableCommand>(items: T[], query: string): T[] {\n  const normalized = normalize(query);\n  if (!normalized) return items;\n  const tokens = normalized.split(/\\s+/);\n  return items\n    .map((item) => {\n      const label = normalize(item.label);\n      const fields = [label, normalize(item.group ?? \"\"), ...(item.keywords ?? []).map(normalize)];\n      let score = label === normalized ? 10000 : label.startsWith(normalized) ? 2000 : 0;\n      for (const token of tokens) {\n        const best = Math.max(...fields.map((field, index) => {\n          const match = tokenScore(token, field);\n          return match ? match + (index === 0 ? 40 : 0) : 0;\n        }));\n        if (!best) return { item, score: 0 };\n        score += best;\n      }\n      return { item, score };\n    })\n    .filter(({ score }) => score > 0)\n    .sort((a, b) => b.score - a.score)\n    .map(({ item }) => item);\n}\n"
    },
    {
      "path": "lib/ease.ts",
      "type": "registry:lib",
      "target": "@lib/ease.ts",
      "content": "// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"
    },
    {
      "path": "lib/hooks/use-on-open.ts",
      "type": "registry:hook",
      "target": "@lib/hooks/use-on-open.ts",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\n\n/**\n * Runs `start` during the render in which `open` becomes true.\n *\n * Opening a list starts a fresh session — an empty query, the highlight back at\n * the top — and that is a resolution, not a side effect: a passive effect runs\n * after the commit, so the new session would carry the last one's state for a\n * window in which a key press can land.\n *\n * `start` may only set state belonging to the calling component. Anything that\n * reaches outside it — a consumer's callback, a DOM write, moving focus — is a\n * side effect, and React rejects a callback that sets another component's state\n * during this one's render. Put those in an effect keyed to `open`.\n */\nexport function useOnOpen(open: boolean, start: () => void) {\n  const [wasOpen, setWasOpen] = useState(open);\n  if (open !== wasOpen) {\n    setWasOpen(open);\n    if (open) start();\n  }\n}\n"
    },
    {
      "path": "lib/hooks/use-row-cursor.ts",
      "type": "registry:hook",
      "target": "@lib/hooks/use-row-cursor.ts",
      "content": "\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef, useState } from \"react\";\n\n/**\n * Where the keyboard or the pointer last moved to: the row's id, stamped with\n * the query it was placed under.\n */\ntype RowCursor = { id: string; query: string };\n\n/** The cursor's row, or -1 once the query has moved on or the row has left. */\nfunction indexOfCursor(\n  rows: readonly { id: string }[],\n  query: string,\n  cursor: RowCursor | null,\n) {\n  if (cursor === null || cursor.query !== query) return -1;\n  return rows.findIndex((row) => row.id === cursor.id);\n}\n\n/**\n * The highlighted row of a list whose rows can change under it.\n *\n * Which row is highlighted is resolved during render, never in a passive\n * effect: a passive effect runs after the commit, so a list that had just\n * changed would carry an `aria-activedescendant` naming a row that has left it,\n * and a key pressed in that window would commit the wrong row or nothing at\n * all.\n *\n * The cursor holds the row's id, not its position. A position alone cannot tell\n * a list that shrank from one that swapped its rows for a different set of the\n * same length, and the second case is the one that silently hands Enter to a\n * row the user never chose. A cursor whose row has left the list returns the\n * highlight to the first row rather than to the nearest surviving one: the row\n * the user aimed at is gone, and the first row is where a new query already\n * puts the highlight.\n *\n * Pass the query the list is filtered by. The cursor is stamped with it and\n * dropped when it changes, so a caller cannot forget to clear it — including a\n * caller whose query arrives as a prop and so never runs its own handler. Rows\n * that come back under a query that has moved on cannot revive it either.\n * `moveTo(null)` is for deliberate resets, such as reopening the list.\n *\n * Both callbacks keep one identity for the life of the component, and read the\n * rows and the query through a ref to do it. A caller will put them in an\n * effect's dependencies — the exhaustive-deps rule makes it — and a `moveTo`\n * rebuilt on every keystroke would re-run that effect on every keystroke.\n */\nexport function useRowCursor(rows: readonly { id: string }[], query: string) {\n  const [cursor, setCursor] = useState<RowCursor | null>(null);\n  // Written after commit, not during render: a render React discards or has not\n  // finished still runs the component body, and an event handler that read this\n  // in that window would stamp the cursor with a query the committed tree does\n  // not have.\n  const latest = useRef({ rows, query });\n  useLayoutEffect(() => {\n    latest.current = { rows, query };\n  });\n\n  const cursorRow = indexOfCursor(rows, query, cursor);\n  // Cleared rather than ignored: React re-runs this render with the cursor\n  // already gone, so rows that come back cannot revive a highlight the user has\n  // stopped aiming at.\n  if (cursor !== null && cursorRow < 0) setCursor(null);\n\n  const moveTo = useCallback(\n    (id: string | null) =>\n      setCursor(id === null ? null : { id, query: latest.current.query }),\n    [],\n  );\n\n  const moveActive = useCallback((direction: 1 | -1) => {\n    const { rows: live, query: liveQuery } = latest.current;\n    const last = live.length - 1;\n    if (last < 0) return;\n    // Steps from the row the cursor is really on, inside the update, so that\n    // two keys landing in one batch move two rows rather than one.\n    setCursor((current) => {\n      const at = Math.max(indexOfCursor(live, liveQuery, current), 0);\n      const next = Math.min(Math.max(at + direction, 0), last);\n      return { id: live[next].id, query: liveQuery };\n    });\n  }, []);\n\n  return { activeIndex: cursorRow < 0 ? 0 : cursorRow, moveTo, moveActive };\n}\n"
    },
    {
      "path": "lib/hooks/use-touch-capable.ts",
      "type": "registry:hook",
      "target": "@lib/hooks/use-touch-capable.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true on devices that can be touched, whatever else they claim.\n *\n * This is not the inverse of `useHoverCapable`: iPadOS Safari browses\n * desktop-class and answers `(hover: hover) and (pointer: fine)` with true\n * while a finger is the only input there is, so anything that treats\n * hover-capable as \"no touch here\" strands every iPad. Gate the *touch path*\n * of an interaction on this hook and leave hover-only polish on\n * `useHoverCapable`, so a component that opens on hover also opens on tap.\n */\nexport function useTouchCapable() {\n  const [canTouch, setCanTouch] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n    const mq = window.matchMedia?.(\"(any-pointer: coarse)\");\n    // iPadOS disguises its pointer media queries; maxTouchPoints it reports\n    // honestly, which is what makes it the standard iPad tell.\n    const update = () =>\n      setCanTouch(Boolean(mq?.matches) || navigator.maxTouchPoints > 0);\n    update();\n    mq?.addEventListener?.(\"change\", update);\n    return () => mq?.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canTouch;\n}\n"
    },
    {
      "path": "lib/presence-gate.tsx",
      "type": "registry:lib",
      "target": "@lib/presence-gate.tsx",
      "content": "\"use client\";\n\nimport { useIsPresent } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\nexport interface PresenceGateRenderProps {\n  /**\n   * False from the render that starts the exit animation onward. An overlay\n   * kept in the tree by `AnimatePresence` is still the topmost thing on the\n   * page, so anything it decides from `open` alone stays true for the whole\n   * exit — this is the boolean that already knows the overlay is leaving.\n   */\n  isPresent: boolean;\n  /**\n   * Spread onto every layer that takes pointer events while the overlay is\n   * open. Interaction releases in the same commit that starts the exit while\n   * the visual exit keeps playing: pointer events stop landing, and `inert`\n   * drops the subtree from focus order, from tab order and from the\n   * accessibility tree — an exiting dialog is not a dialog you can still type\n   * into. A layer that never takes pointer events (a wrapper that only centres\n   * the panel) takes `inert={!isPresent}` alone, so its own\n   * `pointer-events-none` is not overwritten.\n   */\n  gate: {\n    inert: boolean;\n    style: { pointerEvents: \"auto\" | \"none\" };\n  };\n}\n\nexport interface PresenceGateProps {\n  children: (props: PresenceGateRenderProps) => ReactNode;\n}\n\n/**\n * Reads the presence of the subtree it renders and hands it down.\n *\n * `useIsPresent` only answers inside the `AnimatePresence` subtree, and the\n * components that own an overlay render the `AnimatePresence` themselves, so\n * the boolean has to be read one component further down: this is that\n * component, and the render prop is how it reaches the layers.\n */\nexport function PresenceGate({ children }: PresenceGateProps) {\n  const isPresent = useIsPresent();\n\n  return children({\n    isPresent,\n    gate: {\n      inert: !isPresent,\n      style: { pointerEvents: isPresent ? \"auto\" : \"none\" },\n    },\n  });\n}\n"
    },
    {
      "path": "lib/utils.ts",
      "type": "registry:lib",
      "target": "@lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
    }
  ]
}
