{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "type": "registry:ui",
  "title": "Command Palette",
  "description": "⌘K opens it on the same frame, ranks matches and runs async commands in place.",
  "categories": [
    "command"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/kbd.json"
  ],
  "files": [
    {
      "path": "components/ui/command-palette.tsx",
      "content": "\"use client\";\nimport { Autocomplete } from \"@base-ui/react/autocomplete\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport { animate, useMotionValue, useReducedMotion, motion, type AnimationPlaybackControls } from \"motion/react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Loader, Search } from \"@/lib/icons\";\nimport { spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\nimport { Kbd, isTypingTarget, matchShortcut, usePlatform } from \"@/components/ui/kbd\";\n\n/* ------------------------------------------------------------------ */\n/* Data                                                                */\n/* ------------------------------------------------------------------ */\n\nexport type CommandItem = {\n  id: string;\n  label: string;\n  icon?: React.ReactNode;\n  /** Secondary text after the label: a path, an email, a count. It truncates before the label does. */\n  hint?: string;\n  /** More words that should find this item (\"dark\" for Toggle theme). */\n  keywords?: string[];\n  /** Shown on the right. Shortcuts with a modifier also run the item while the palette is open. */\n  shortcut?: string;\n  disabled?: boolean;\n  /** Runs when chosen. Return a promise to keep the palette open with a spinner until it settles; reject to show why in the row. */\n  onSelect?: () => void | Promise<unknown>;\n  /** Stay open after running, for toggles people flip more than once. */\n  keepOpen?: boolean;\n};\n\nexport type CommandGroup = { heading: string; items: CommandItem[] };\n\n/** One row as rendered: the item, where the query matched its label, and a key unique across sections. */\nexport type CommandEntry = { key: string; item: CommandItem; ranges: [number, number][]; score: number };\nexport type CommandSection = { value: string; items: CommandEntry[] };\n\n/* ------------------------------------------------------------------ */\n/* Matching                                                            */\n/* ------------------------------------------------------------------ */\n\n// Case- and accent-insensitive, with a map back to the original characters so\n// the highlighted runs line up with what's on screen (\"sao\" finds \"São\").\nfunction fold(s: string) {\n  let text = \"\";\n  const map: number[] = [];\n  for (let i = 0; i < s.length; i++) {\n    for (const ch of s[i].normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLocaleLowerCase()) {\n      text += ch;\n      map.push(i);\n    }\n  }\n  return { text, map };\n}\n\nconst isBoundary = (text: string, i: number) => i === 0 || /[\\s\\-_/.·:]/.test(text[i - 1]);\n\n// Where words start in the original label: after a separator, or a capital after a lowercase letter (GitHub → Git·Hub).\nfunction starts(label: string, map: number[]) {\n  return map.map((orig, i) => {\n    if (i === 0) return true;\n    const prev = label[orig - 1] ?? \"\";\n    const ch = label[orig];\n    return map[i - 1] !== orig && (/[\\s\\-_/.·:]/.test(prev) || (/\\p{Lu}/u.test(ch) && /\\p{Ll}/u.test(prev)));\n  });\n}\n\n// The query as an acronym: every letter starts a word or continues the one before\n// (\"ct\" → Change theme, \"ghi\" → GitHub issues). Scattered letters don't count.\nfunction acronym(text: string, boundary: boolean[], q: string) {\n  const hits: number[] = [];\n  const walk = (qi: number, from: number): boolean => {\n    if (qi === q.length) return true;\n    for (let i = from; i < text.length; i++) {\n      if (text[i] !== q[qi]) continue;\n      const prev = hits[hits.length - 1];\n      if (!boundary[i] && !(prev !== undefined && prev === i - 1)) continue;\n      hits.push(i);\n      if (walk(qi + 1, i + 1)) return true;\n      hits.pop();\n    }\n    return false;\n  };\n  return walk(0, 0) ? hits : null;\n}\n\nfunction merge(ranges: [number, number][]) {\n  const sorted = [...ranges].sort((a, b) => a[0] - b[0]);\n  const out: [number, number][] = [];\n  for (const r of sorted) {\n    const last = out[out.length - 1];\n    if (last && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);\n    else out.push([r[0], r[1]]);\n  }\n  return out;\n}\n\n/**\n * How well an item answers a query, and which characters of its label matched.\n * Every word of the query must land somewhere: the label (strongest at the start\n * of a word), then keywords, then the hint. Failing that, the query's letters in\n * order through the label. Returns null when it doesn't match at all.\n */\nexport function scoreCommand(item: CommandItem, query: string): { score: number; ranges: [number, number][] } | null {\n  const q = fold(query.trim()).text.replace(/\\s+/g, \" \");\n  if (!q) return { score: 0, ranges: [] };\n  const label = fold(item.label);\n  const back = (s: number, e: number): [number, number] => [label.map[s], label.map[e - 1] + 1];\n  if (label.text === q) return { score: 1000, ranges: [[0, item.label.length]] };\n\n  let score = label.text.startsWith(q) ? 300 : 0;\n  const ranges: [number, number][] = [];\n  const keywords = (item.keywords ?? []).map((k) => fold(k).text);\n  const hint = item.hint ? fold(item.hint).text : \"\";\n  let ok = true;\n  for (const token of q.split(\" \")) {\n    let at = -1;\n    for (let i = label.text.indexOf(token); i >= 0; i = label.text.indexOf(token, i + 1)) {\n      if (isBoundary(label.text, i)) {\n        at = i;\n        break;\n      }\n      if (at < 0) at = i;\n    }\n    if (at >= 0) {\n      score += isBoundary(label.text, at) ? (at === 0 ? 100 : 80) : 50;\n      ranges.push(back(at, at + token.length));\n    } else if (keywords.some((k) => k.startsWith(token))) score += 30;\n    else if (keywords.some((k) => k.includes(token)) || hint.includes(token)) score += 15;\n    else {\n      ok = false;\n      break;\n    }\n  }\n  if (ok) return { score, ranges: merge(ranges) };\n\n  const hits = acronym(label.text, starts(item.label, label.map), q.replace(/ /g, \"\"));\n  if (!hits) return null;\n  return { score: 40 - hits[0], ranges: merge(hits.map((h) => back(h, h + 1))) };\n}\n\n/**\n * The palette's search on its own: filters and ranks groups for a query, and\n * with an empty query puts recently used items first under \"Recent\".\n */\nexport function useCommandSearch(groups: CommandGroup[], query: string, { recent = [] as string[], recentLabel = \"Recent\" } = {}) {\n  return useMemo(() => {\n    const q = query.trim();\n    if (!q) {\n      const byId = new Map(groups.flatMap((g) => g.items.map((i) => [i.id, i] as const)));\n      const recentItems = recent.map((id) => byId.get(id)).filter((i): i is CommandItem => !!i && !i.disabled);\n      const shown = new Set(recentItems.map((i) => i.id));\n      const sections: CommandSection[] = [];\n      if (recentItems.length) sections.push({ value: recentLabel, items: recentItems.map((item) => ({ key: `recent:${item.id}`, item, ranges: [], score: 0 })) });\n      for (const g of groups) {\n        const items = g.items.filter((i) => !shown.has(i.id)).map((item) => ({ key: item.id, item, ranges: [], score: 0 }));\n        if (items.length) sections.push({ value: g.heading, items });\n      }\n      return sections;\n    }\n    const sections: CommandSection[] = [];\n    for (const g of groups) {\n      const items: CommandEntry[] = [];\n      for (const item of g.items) {\n        const m = scoreCommand(item, q);\n        if (m) items.push({ key: item.id, item, ranges: m.ranges, score: m.score });\n      }\n      // Stable sort: equal scores keep the order the app chose.\n      items.sort((a, b) => b.score - a.score);\n      if (items.length) sections.push({ value: g.heading, items });\n    }\n    // The group holding the best answer comes first.\n    return sections.sort((a, b) => b.items[0].score - a.items[0].score);\n  }, [groups, query, recent, recentLabel]);\n}\n\n/** A label with its matched runs in the primary color and the rest a step quieter. */\nexport function CommandHighlight({ text, ranges }: { text: string; ranges: [number, number][] }) {\n  if (!ranges.length) return <>{text}</>;\n  const parts: React.ReactNode[] = [];\n  let at = 0;\n  ranges.forEach(([s, e], i) => {\n    if (s > at) parts.push(<span key={`u${i}`} className=\"text-fg-3 transition-colors group-data-highlighted/item:text-fg-2\">{text.slice(at, s)}</span>);\n    parts.push(<mark key={`m${i}`} className=\"bg-transparent text-fg\">{text.slice(s, e)}</mark>);\n    at = e;\n  });\n  if (at < text.length) parts.push(<span key=\"tail\" className=\"text-fg-3 transition-colors group-data-highlighted/item:text-fg-2\">{text.slice(at)}</span>);\n  return <>{parts}</>;\n}\n\n/* ------------------------------------------------------------------ */\n/* Shared pieces                                                       */\n/* ------------------------------------------------------------------ */\n\n/** True after `on` has held for `delay`ms, and then for at least `min`ms, so a quick load never flashes a spinner. */\nexport function useDelayedFlag(on: boolean, delay = 150, min = 300) {\n  const [shown, setShown] = useState(false);\n  const since = useRef(0);\n  useEffect(() => {\n    let t: number | undefined;\n    if (on && !shown) {\n      t = window.setTimeout(() => {\n        since.current = performance.now();\n        setShown(true);\n      }, delay);\n    } else if (!on && shown) {\n      t = window.setTimeout(() => setShown(false), Math.max(0, min - (performance.now() - since.current)));\n    }\n    return () => window.clearTimeout(t);\n  }, [on, shown, delay, min]);\n  return shown;\n}\n\n/**\n * One highlight for a whole list. It glides after the pointer on a spring and\n * jumps for arrow keys and typing, because keyboard movement must feel instant.\n */\nexport function useListGlide(reduce: boolean) {\n  const [list, setList] = useState<HTMLElement | null>(null);\n  const y = useMotionValue(0);\n  const height = useMotionValue(36);\n  const opacity = useMotionValue(0);\n\n  useEffect(() => {\n    if (!list) return;\n    let keyboard = true;\n    let shown = false;\n    let running: AnimationPlaybackControls[] = [];\n    const stop = () => {\n      running.forEach((c) => c.stop());\n      running = [];\n    };\n    const place = () => {\n      const el = list.querySelector<HTMLElement>(\"[data-highlighted]\");\n      stop();\n      if (!el) {\n        shown = false;\n        opacity.jump(0);\n        return;\n      }\n      let top = 0;\n      for (let n: HTMLElement | null = el; n && n !== list; n = n.offsetParent as HTMLElement | null) top += n.offsetTop;\n      if (!shown || keyboard || reduce) {\n        y.jump(top);\n        height.jump(el.offsetHeight);\n      } else {\n        running.push(animate(y, top, spring.follow), animate(height, el.offsetHeight, spring.follow));\n      }\n      opacity.jump(el.hasAttribute(\"data-disabled\") ? 0.5 : 1);\n      shown = true;\n    };\n    const onKey = () => (keyboard = true);\n    const onPointer = () => (keyboard = false);\n    const observer = new MutationObserver(place);\n    observer.observe(list, { subtree: true, childList: true, attributes: true, attributeFilter: [\"data-highlighted\"] });\n    document.addEventListener(\"keydown\", onKey, true);\n    list.addEventListener(\"pointermove\", onPointer);\n    place();\n    return () => {\n      observer.disconnect();\n      document.removeEventListener(\"keydown\", onKey, true);\n      list.removeEventListener(\"pointermove\", onPointer);\n      stop();\n    };\n  }, [list, reduce, y, height, opacity]);\n\n  return { setList, y, height, opacity };\n}\n\n/** Measures a node's height so the region around it can ease to the new size as results filter. */\nexport function useMeasuredHeight() {\n  const [height, setHeight] = useState<number | null>(null);\n  const ref = useCallback((node: HTMLElement | null) => {\n    if (!node) return;\n    const ro = new ResizeObserver(() => setHeight(node.offsetHeight));\n    ro.observe(node);\n    return () => ro.disconnect();\n  }, []);\n  return [ref, height] as const;\n}\n\nexport const paletteClasses = {\n  backdrop: cn(\n    \"inset-0 z-(--z-overlay) bg-overlay\",\n    \"transition-opacity duration-200 ease-out-quart data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-150\",\n    \"data-instant:transition-none\",\n  ),\n  viewport: \"inset-0 z-(--z-dialog) flex items-start justify-center px-2 pb-2 sm:px-4\",\n  popup: cn(\n    \"relative flex max-h-full w-full max-w-[560px] min-w-0 flex-col overflow-hidden rounded-2xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n    // From the pointer: fade, settle from .98 and 4px up, 180ms. Out: 120ms, opacity and a hair of scale.\n    \"origin-top transition-[opacity,scale,translate] duration-[180ms] ease-out-expo\",\n    \"data-starting-style:-translate-y-1 data-starting-style:scale-[0.98] data-starting-style:opacity-0\",\n    \"data-ending-style:scale-[0.99] data-ending-style:opacity-0 data-ending-style:duration-[120ms] data-ending-style:ease-out-quart\",\n    \"motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n    // From the keyboard: no animation at all, in or out.\n    \"data-instant:transition-none\",\n  ),\n  input: \"h-full min-w-0 flex-1 bg-transparent text-base text-fg outline-none placeholder:text-fg-4 sm:text-[14px] [&::-webkit-search-cancel-button]:hidden\",\n  groupLabel: \"px-2.5 pt-3 pb-1.5 font-mono text-2xs uppercase tracking-[0.08em] text-fg-3 select-none\",\n  item: cn(\n    \"group/item relative z-[1] flex h-9 cursor-default scroll-my-1.5 items-center gap-2.5 rounded-lg px-2.5 text-[13px] text-fg outline-none select-none\",\n    \"pointer-coarse:h-11 data-disabled:text-fg-4\",\n  ),\n  glide: cn(\n    \"pointer-events-none absolute inset-x-0 top-0 rounded-lg bg-fg/[0.06]\",\n    // Pressing a row deepens the same highlight rather than drawing a second one.\n    \"transition-colors duration-100 group-has-[[data-highlighted]:active]/list:bg-fg/[0.1]\",\n  ),\n};\n\n/** The \"↑↓ Navigate · ↵ Open · esc Close\" row under the list. Hidden on touch, where none of it applies. */\nexport function PaletteFooter({ hints, children, className }: { hints: [keys: string | string[], label: string][]; children?: React.ReactNode; className?: string }) {\n  return (\n    <div className={cn(\"flex h-10 shrink-0 items-center gap-4 border-t border-line px-3.5 text-[11.5px] text-fg-3 pointer-coarse:hidden\", className)}>\n      {hints.map(([keys, label]) => (\n        <span key={label} className=\"flex items-center gap-1.5\">\n          <Kbd keys={keys} size=\"sm\" />\n          {label}\n        </span>\n      ))}\n      {children && <span className=\"ml-auto min-w-0 truncate text-fg-4\">{children}</span>}\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* CommandPalette                                                      */\n/* ------------------------------------------------------------------ */\n\nexport type CommandPaletteProps = {\n  groups: CommandGroup[];\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** The shortcut that opens and closes it. false to turn it off. */\n  hotkey?: string | false;\n  /** Where the hotkey listens. Defaults to the whole window; pass an element to scope it (and keep other ⌘K handlers from firing). */\n  hotkeyTarget?: React.RefObject<HTMLElement | null>;\n  placeholder?: string;\n  /** Item ids used most recently, newest first. Shown under Recent while the query is empty. */\n  recent?: string[];\n  defaultRecent?: string[];\n  onRecentChange?: (ids: string[]) => void;\n  /** How many recent items to keep. 0 turns recents off. */\n  recentLimit?: number;\n  /** Called with every item that runs, after its own onSelect. */\n  onSelect?: (item: CommandItem) => void;\n  /** Results are still arriving: a spinner replaces the search icon (after 150ms) and the empty state waits. */\n  loading?: boolean;\n  /** Render into this element instead of document.body. The backdrop then covers only the container. */\n  container?: Dialog.Portal.Props[\"container\"];\n  /** Accessible name of the dialog. */\n  label?: string;\n  /** Right side of the footer, e.g. a scope or a help link. */\n  footer?: React.ReactNode;\n  className?: string;\n  /** Usually a CommandPaletteTrigger. */\n  children?: React.ReactNode;\n};\n\ntype Failure = { key: string; message: string };\n\nexport function CommandPalette({\n  groups,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  hotkey = \"mod+k\",\n  hotkeyTarget,\n  placeholder = \"Search or jump to…\",\n  recent: recentProp,\n  defaultRecent = [],\n  onRecentChange,\n  recentLimit = 4,\n  onSelect,\n  loading = false,\n  container,\n  label = \"Command palette\",\n  footer,\n  className,\n  children,\n}: CommandPaletteProps) {\n  const reduce = !!useReducedMotion();\n  const platform = usePlatform();\n  const [open, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });\n  const [recent, setRecent] = useControllableState({ value: recentProp, defaultValue: defaultRecent, onChange: onRecentChange });\n  const [query, setQuery] = useState(\"\");\n  // Opened or closed from the keyboard: no transition either way.\n  const [instant, setInstant] = useState(false);\n  const [busy, setBusy] = useState<string | null>(null);\n  const [failure, setFailure] = useState<Failure | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const contained = container != null;\n\n  const shownRecent = useMemo(() => (recentLimit > 0 ? recent.slice(0, recentLimit) : []), [recent, recentLimit]);\n  const sections = useCommandSearch(groups, query, { recent: shownRecent });\n  const count = sections.reduce((n, s) => n + s.items.length, 0);\n  // A command started by its shortcut may not be in the filtered list, so the field shows it is working too.\n  const spinning = useDelayedFlag(loading || busy !== null);\n  const { setList: setGlideList, y: glideY, height: glideH, opacity: glideO } = useListGlide(reduce);\n  const [measure, contentHeight] = useMeasuredHeight();\n\n  const close = (fromKeyboard: boolean) => {\n    setInstant(fromKeyboard);\n    setOpen(false);\n  };\n\n  const run = async (entry: CommandEntry, fromKeyboard: boolean) => {\n    const { item } = entry;\n    if (item.disabled || busy) return;\n    setFailure(null);\n    if (recentLimit > 0) setRecent((r) => [item.id, ...r.filter((id) => id !== item.id)].slice(0, recentLimit * 2));\n    let result: void | Promise<unknown>;\n    try {\n      result = item.onSelect?.();\n    } catch (error) {\n      return setFailure({ key: entry.key, message: messageOf(error) });\n    }\n    if (result && typeof (result as Promise<unknown>).then === \"function\") {\n      setBusy(entry.key);\n      try {\n        await result;\n      } catch (error) {\n        setBusy(null);\n        return setFailure({ key: entry.key, message: messageOf(error) });\n      }\n      setBusy(null);\n    }\n    onSelect?.(item);\n    if (!item.keepOpen) close(fromKeyboard);\n  };\n\n  usePaletteHotkey({\n    hotkey,\n    target: hotkeyTarget,\n    open,\n    onToggle: () => {\n      setInstant(true);\n      setOpen(!open);\n    },\n  });\n\n  // Modifier shortcuts on items run them while the palette is open.\n  const onPopupKeyDown = (e: React.KeyboardEvent) => {\n    const native = e.nativeEvent;\n    if (!native.metaKey && !native.ctrlKey && !native.altKey) return;\n    for (const g of groups)\n      for (const item of g.items) {\n        if (item.shortcut && !item.disabled && matchShortcut(native, item.shortcut, platform)) {\n          e.preventDefault();\n          e.stopPropagation();\n          void run({ key: item.id, item, ranges: [], score: 0 }, true);\n          return;\n        }\n      }\n  };\n\n  return (\n    <Dialog.Root\n      open={open}\n      onOpenChange={(next, details) => {\n        const ev = details.event as Event | undefined;\n        setInstant(ev instanceof KeyboardEvent || (ev instanceof MouseEvent && ev.detail === 0));\n        setOpen(next);\n      }}\n      onOpenChangeComplete={(isOpen) => {\n        if (isOpen) return;\n        setQuery(\"\");\n        setFailure(null);\n        setBusy(null);\n      }}\n    >\n      {children}\n      <Dialog.Portal container={container}>\n        <Dialog.Backdrop data-instant={instant ? \"\" : undefined} className={cn(contained ? \"absolute\" : \"fixed\", paletteClasses.backdrop)} />\n        <Dialog.Viewport className={cn(contained ? \"absolute pt-14\" : \"fixed pt-[12vh]\", paletteClasses.viewport)}>\n          <Dialog.Popup\n            initialFocus={inputRef}\n            data-instant={instant ? \"\" : undefined}\n            onKeyDown={onPopupKeyDown}\n            className={cn(paletteClasses.popup, className)}\n          >\n            <Dialog.Title className=\"sr-only\">{label}</Dialog.Title>\n            <Autocomplete.Root\n              inline\n              open\n              items={sections}\n              filteredItems={sections}\n              value={query}\n              onValueChange={(next, details) => {\n                // Choosing an item would write its label into the field; the palette keeps what was typed.\n                if (details.reason === \"item-press\") return;\n                setQuery(next);\n                setFailure(null);\n              }}\n              onOpenChange={(next, details) => {\n                if (!next && details.reason === \"escape-key\") close(true);\n              }}\n              itemToStringValue={(entry: CommandEntry) => entry.item.label}\n              autoHighlight=\"always\"\n              keepHighlight\n            >\n              <div className=\"flex h-12 shrink-0 items-center gap-2.5 border-b border-line pr-2.5 pl-4 pointer-coarse:h-14\">\n                <span className=\"relative grid size-4 shrink-0 place-items-center text-fg-3\">\n                  <Search size={16} className={cn(\"transition-[opacity,scale] duration-150\", spinning && \"scale-75 opacity-0\")} />\n                  <Loader size={16} className={cn(\"absolute animate-spin transition-[opacity,scale] duration-150\", !spinning && \"scale-75 opacity-0\")} />\n                </span>\n                <Autocomplete.Input\n                  ref={inputRef}\n                  placeholder={placeholder}\n                  aria-label={placeholder.replace(/…$/, \"\")}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                  enterKeyHint=\"go\"\n                  className={paletteClasses.input}\n                />\n                <Dialog.Close\n                  aria-label=\"Close\"\n                  className={cn(\n                    \"relative grid shrink-0 place-items-center rounded-md outline-none\",\n                    \"transition-[scale,opacity] duration-150 hover:opacity-80 active:scale-[0.94]\",\n                    \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                    \"after:absolute after:-inset-3 after:content-[''] pointer-fine:after:hidden\",\n                  )}\n                >\n                  <Kbd keys=\"esc\" size=\"sm\" />\n                </Dialog.Close>\n              </div>\n\n              <div\n                style={{ height: contentHeight ?? undefined }}\n                className=\"max-h-[min(22rem,55dvh)] min-h-0 overflow-y-auto overscroll-contain transition-[height] duration-150 ease-out-quart motion-reduce:transition-none\"\n              >\n                <div ref={measure} className=\"p-1.5\">\n                  <Autocomplete.Empty>\n                    {count === 0 && (\n                      <div className=\"flex flex-col items-center gap-3 px-6 py-9 text-center\">\n                        {loading ? (\n                          <p className=\"text-[13px] text-fg-3\">Searching…</p>\n                        ) : query.trim() ? (\n                          <>\n                            <p className=\"max-w-full truncate text-[13px] text-fg-2\">\n                              No results for <span className=\"text-fg\">“{query.trim()}”</span>\n                            </p>\n                            <button\n                              type=\"button\"\n                              onClick={() => {\n                                setQuery(\"\");\n                                inputRef.current?.focus();\n                              }}\n                              className={cn(\n                                \"inline-flex h-7 items-center rounded-md border border-line-2 bg-raised px-2.5 text-[12px] font-medium text-fg shadow-[var(--shadow)]\",\n                                \"transition-[background-color,border-color,scale] duration-150 hover:border-fg-4 hover:bg-hover active:scale-[0.97]\",\n                                \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                              )}\n                            >\n                              Clear search\n                            </button>\n                          </>\n                        ) : (\n                          <p className=\"text-[13px] text-fg-3\">No commands yet</p>\n                        )}\n                      </div>\n                    )}\n                  </Autocomplete.Empty>\n                  <div ref={setGlideList} className=\"group/list relative\">\n                  <motion.div aria-hidden style={{ y: glideY, height: glideH, opacity: glideO }} className={paletteClasses.glide} />\n                  <Autocomplete.List className=\"outline-none empty:hidden\">\n                    {(section: CommandSection) => (\n                      <Autocomplete.Group key={section.value} items={section.items}>\n                        <Autocomplete.GroupLabel className={cn(paletteClasses.groupLabel, \"first:pt-1.5\")}>{section.value}</Autocomplete.GroupLabel>\n                        <Autocomplete.Collection>\n                          {(entry: CommandEntry) => (\n                            <CommandRow\n                              key={entry.key}\n                              entry={entry}\n                              busy={busy === entry.key}\n                              failure={failure?.key === entry.key ? failure.message : null}\n                              onRun={(fromKeyboard) => void run(entry, fromKeyboard)}\n                            />\n                          )}\n                        </Autocomplete.Collection>\n                      </Autocomplete.Group>\n                    )}\n                  </Autocomplete.List>\n                  </div>\n                </div>\n              </div>\n\n              <PaletteFooter\n                hints={[\n                  [[\"up\", \"down\"], \"Navigate\"],\n                  [\"enter\", \"Open\"],\n                ]}\n              >\n                {footer}\n              </PaletteFooter>\n              <Autocomplete.Status className=\"sr-only\">\n                {busy ? \"Working…\" : failure ? failure.message : query.trim() ? (count === 0 ? \"No results\" : `${count} ${count === 1 ? \"result\" : \"results\"}`) : \"\"}\n              </Autocomplete.Status>\n            </Autocomplete.Root>\n          </Dialog.Popup>\n        </Dialog.Viewport>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n}\n\n/**\n * The shortcut that toggles a palette. Scoped targets stop the event there, so an\n * app-wide handler for the same keys doesn't fire as well. Keys without a modifier\n * (\"/\") never open it from inside a field.\n */\nexport function usePaletteHotkey({\n  hotkey,\n  target,\n  open,\n  onToggle,\n}: {\n  hotkey: string | false;\n  target?: React.RefObject<HTMLElement | null>;\n  open: boolean;\n  onToggle: () => void;\n}) {\n  const platform = usePlatform();\n  const toggle = useRef(onToggle);\n  useEffect(() => {\n    toggle.current = onToggle;\n  });\n  useEffect(() => {\n    if (!hotkey) return;\n    const el: HTMLElement | Window = target?.current ?? window;\n    const bare = !/(mod|ctrl|meta|cmd|alt|option)\\+/i.test(hotkey);\n    const onKey = (event: Event) => {\n      const e = event as KeyboardEvent;\n      if (!matchShortcut(e, hotkey, platform)) return;\n      // A bare key (\"/\", \"?\") is a character while typing, open or not.\n      if (bare && isTypingTarget(e.target)) return;\n      e.preventDefault();\n      e.stopPropagation();\n      toggle.current();\n    };\n    el.addEventListener(\"keydown\", onKey);\n    return () => el.removeEventListener(\"keydown\", onKey);\n  }, [hotkey, target, platform, open]);\n}\n\nexport function messageOf(error: unknown) {\n  return error instanceof Error && error.message ? error.message : \"That didn’t work. Try again.\";\n}\n\nexport type CommandRowProps = {\n  entry: CommandEntry;\n  busy?: boolean;\n  failure?: string | null;\n  /** fromKeyboard is true for Enter, so the caller can close without animating. */\n  onRun: (fromKeyboard: boolean) => void;\n  /** Replaces the shortcut on the right, e.g. a check or a chevron. */\n  trailing?: React.ReactNode;\n};\n\n/** One palette row: icon, highlighted label, hint (or the failure), and the shortcut or a spinner in a fixed slot. */\nexport function CommandRow({ entry, busy, failure, onRun, trailing }: CommandRowProps) {\n  const { item } = entry;\n  return (\n    <Autocomplete.Item\n      value={entry}\n      disabled={item.disabled}\n      onClick={(e) => onRun(e.detail === 0)}\n      aria-busy={busy || undefined}\n      data-failed={failure ? \"\" : undefined}\n      className={paletteClasses.item}\n    >\n      {item.icon && (\n        <span className=\"flex size-4 shrink-0 items-center justify-center text-fg-3 transition-colors group-data-highlighted/item:text-fg-2 [&_svg]:size-4\">\n          {item.icon}\n        </span>\n      )}\n      <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n        <span className=\"max-w-full shrink-0 truncate\">\n          <CommandHighlight text={item.label} ranges={entry.ranges} />\n        </span>\n        {failure ? (\n          <span role=\"alert\" className=\"min-w-0 truncate text-[12.5px] text-danger\">\n            {failure}\n          </span>\n        ) : (\n          item.hint && <span className=\"min-w-0 truncate text-[12.5px] text-fg-3\">{item.hint}</span>\n        )}\n      </span>\n      <span className=\"relative flex shrink-0 items-center justify-end\">\n        {busy ? (\n          <Loader size={14} className=\"animate-spin text-fg-2\" />\n        ) : (\n          (trailing ?? (item.shortcut && <Kbd keys={item.shortcut} variant=\"ghost\" size=\"sm\" className=\"transition-colors group-data-highlighted/item:[&_kbd]:text-fg-2\" />))\n        )}\n      </span>\n    </Autocomplete.Item>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Trigger                                                             */\n/* ------------------------------------------------------------------ */\n\nexport type CommandPaletteTriggerProps = Omit<Dialog.Trigger.Props, \"children\"> & {\n  label?: string;\n  /** The hotkey to show. Match the palette's. */\n  shortcut?: string | false;\n};\n\n/** A search-field-shaped button that opens the palette and shows its shortcut. */\nexport function CommandPaletteTrigger({ label = \"Search or jump to…\", shortcut = \"mod+k\", className, ...rest }: CommandPaletteTriggerProps) {\n  return (\n    <Dialog.Trigger\n      className={cn(\n        \"group/trigger relative inline-flex h-8 w-full min-w-0 max-w-[280px] select-none items-center gap-2 rounded-lg border border-line-2 bg-raised pr-1.5 pl-2.5 text-[12.5px] text-fg-3 shadow-[var(--shadow)]\",\n        \"transition-[background-color,border-color,color,scale] duration-150 ease-out-quart hover:border-fg-4 hover:text-fg-2 active:scale-[0.98] active:duration-75\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"data-popup-open:border-fg-4 data-popup-open:text-fg-2\",\n        typeof className === \"string\" ? className : undefined,\n      )}\n      {...rest}\n    >\n      <Search size={14} className=\"shrink-0\" />\n      <span className=\"min-w-0 flex-1 truncate text-left\">{label}</span>\n      {shortcut && <Kbd keys={shortcut} size=\"sm\" className=\"pointer-coarse:hidden\" />}\n    </Dialog.Trigger>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/command-palette"
}
