{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"command-palette","type":"registry:component","title":"Command palette","description":"⌘K that grows out of the search pill: fuzzy search, nested lists, and commands that run in place.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/text-morph.json"],"files":[{"path":"registry/command-palette.tsx","type":"registry:component","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { TextMorph } from \"./text-morph\";\n\n/* ─────────────────────────────────────────────────────────\n * COMMAND PALETTE: ⌘K, from the pill that was always there\n *\n *   closed    a quiet \"Search… ⌘K\" pill\n *   opening   the pill itself lifts and grows into the palette:\n *             position, size and corners ease from pill to panel\n *             while its placeholder becomes the field's\n *   search    results narrow as you type, the letters that match\n *             are marked, the panel's height eases to fit and a\n *             highlight glides between rows\n *   nested    \"Change theme…\" opens its own list in place: it\n *             slides in from the right as a chip joins the field;\n *             Backspace slides back\n *   running   a command's shortcut blurs into a spinner, then a\n *             check that draws itself, and the palette folds back\n *             into the pill\n *   empty     \"No results for “xyz”\", morphing as you type\n *\n * A real dialog: focus moves in and back, arrow keys, Home/End,\n * Enter, Escape (back, then close) and the hotkey to toggle.\n * ───────────────────────────────────────────────────────── */\n\nexport interface CommandItem {\n  id: string;\n  label: string;\n  /** Rows are grouped under this heading, in the order groups first appear. */\n  group?: string;\n  icon?: React.ReactNode;\n  /** Shown on the right, e.g. [\"⌘\", \"N\"]. */\n  shortcut?: string[];\n  /** Other words that should find it, e.g. [\"dark\", \"light\"] for \"Change theme\". */\n  keywords?: string[];\n  description?: string;\n  /** Navigate here when chosen. */\n  href?: string;\n  /** Run it. Return a promise to show progress on the row before the palette closes. */\n  onSelect?: () => void | Promise<unknown>;\n  /** A nested list, opened in place (e.g. \"Change theme…\"). */\n  items?: CommandItem[];\n}\n\nexport interface CommandPaletteProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n  items: CommandItem[];\n  placeholder?: string;\n  /** The letter that opens it with ⌘ (or Ctrl). Empty string turns the hotkey off. */\n  hotkey?: string;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Show the \"Search… ⌘K\" pill. Without it, open with the hotkey or the open prop. */\n  showTrigger?: boolean;\n  /** The pill's words. */\n  triggerLabel?: string;\n}\n\ntype Phase = \"closed\" | \"opening\" | \"open\" | \"closing\";\ntype Page = { title: string; items: CommandItem[] };\ntype Row = { item: CommandItem; marks: number[]; score: number };\ntype Rect = { left: number; top: number; width: number; height: number; radius: number };\ntype RunState = { id: string; state: \"pending\" | \"done\" | \"error\" };\n\nconst EASE = \"cubic-bezier(0.16,1,0.3,1)\";\nconst DURATION = 440;\nconst WIDTH = 580;\nconst ROW = 40;\nconst LIST_MAX = 352;\nconst FOCUS = \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background\";\n\nconst reducedQuery = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(reducedQuery);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\nconst useReducedMotion = () =>\n  React.useSyncExternalStore(subscribeReduced, () => window.matchMedia(reducedQuery).matches, () => false);\n\n/** Icons trade places through a blur: the old one shrinks away as the new one grows in. */\nconst swap = (on: boolean, reduced: boolean): React.CSSProperties => ({\n  opacity: on ? 1 : 0,\n  transform: on ? \"none\" : \"scale(0.6)\",\n  filter: on ? \"none\" : \"blur(3px)\",\n  transition: reduced ? \"none\" : `opacity 220ms ${EASE}, transform 340ms ${EASE}, filter 220ms ${EASE}`,\n});\n\n/** Where the query's letters land in the text: one run if it's there whole, otherwise in order. */\nfunction match(text: string, query: string): number[] | null {\n  const t = text.toLowerCase();\n  const q = query.toLowerCase().replace(/\\s+/g, \" \").trim();\n  if (!q) return [];\n  const at = t.indexOf(q);\n  if (at >= 0) return Array.from({ length: q.length }, (_, i) => at + i);\n  const marks: number[] = [];\n  let j = 0;\n  for (let i = 0; i < t.length && j < q.length; i++) {\n    if (q[j] === \" \") j++;\n    if (t[i] === q[j]) {\n      marks.push(i);\n      j++;\n    }\n  }\n  return j >= q.length ? marks : null;\n}\n\nfunction rank(items: CommandItem[], query: string): Row[] {\n  const q = query.trim();\n  if (!q) return items.map((item) => ({ item, marks: [], score: 0 }));\n  const rows: Row[] = [];\n  for (const item of items) {\n    const marks = match(item.label, q);\n    if (marks) {\n      const contiguous = marks.length > 1 && marks[marks.length - 1] - marks[0] === marks.length - 1;\n      const start = marks[0] ?? 0;\n      const wordStart = start === 0 || item.label[start - 1] === \" \";\n      const score = (contiguous ? 60 : 30) + (start === 0 ? 40 : wordStart ? 20 : 0) - (marks[marks.length - 1] - start) * 0.5;\n      rows.push({ item, marks, score });\n    } else if (item.keywords?.some((k) => match(k, q)) || (item.description && match(item.description, q))) {\n      rows.push({ item, marks: [], score: 10 });\n    }\n  }\n  return rows.sort((a, b) => b.score - a.score);\n}\n\nfunction Marked({ text, marks }: { text: string; marks: number[] }) {\n  if (!marks.length) return <>{text}</>;\n  const set = new Set(marks);\n  return (\n    <>\n      {Array.from(text).map((ch, i) =>\n        set.has(i) ? (\n          <span key={i} className=\"font-medium text-foreground\">\n            {ch}\n          </span>\n        ) : (\n          <React.Fragment key={i}>{ch}</React.Fragment>\n        )\n      )}\n    </>\n  );\n}\n\nfunction Kbd({ children }: { children: React.ReactNode }) {\n  return (\n    <kbd className=\"flex h-5 min-w-5 items-center justify-center rounded-md px-1 font-sans text-[11px] text-muted-foreground shadow-[inset_0_0_0_1px_var(--border)]\">\n      {children}\n    </kbd>\n  );\n}\n\nfunction SearchIcon({ className = \"\" }: { className?: string }) {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" className={`size-4 shrink-0 ${className}`}>\n      <circle cx=\"7\" cy=\"7\" r=\"4.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n      <path d=\"m10.5 10.5 3 3\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nfunction Check({ drawn, reduced }: { drawn: boolean; reduced: boolean }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4\">\n      <path\n        d=\"M3.5 8.5 6.5 11.5 12.5 4.5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.8\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        pathLength={1}\n        strokeDasharray={1}\n        style={{ strokeDashoffset: drawn ? 0 : 1, transition: drawn && !reduced ? `stroke-dashoffset 360ms ${EASE} 80ms` : \"none\" }}\n      />\n    </svg>\n  );\n}\n\n/** One page of results. Kept as its own piece so the page you leave can slide out while the next slides in. */\nfunction Results({\n  rows,\n  active,\n  run,\n  reduced,\n  listId,\n  optionId,\n  setActive,\n  choose,\n  rowRefs,\n}: {\n  rows: Row[];\n  active: number;\n  run: RunState | null;\n  reduced: boolean;\n  listId?: string;\n  optionId?: (i: number) => string;\n  setActive?: (i: number) => void;\n  choose?: (i: number) => void;\n  rowRefs?: React.MutableRefObject<(HTMLDivElement | null)[]>;\n}) {\n  let lastGroup: string | undefined;\n  return (\n    <div id={listId} role={listId ? \"listbox\" : undefined} aria-label={listId ? \"Commands\" : undefined}>\n      {rows.map((row, i) => {\n        const { item } = row;\n        const heading = item.group && item.group !== lastGroup ? item.group : null;\n        lastGroup = item.group;\n        const running = run?.id === item.id;\n        const state = running ? run.state : null;\n        return (\n          <React.Fragment key={item.id}>\n            {heading && (\n              <div role=\"presentation\" className=\"px-3 pb-1 pt-2.5 text-[11.5px] font-medium text-muted-foreground first:pt-1\">\n                {heading}\n              </div>\n            )}\n            <div\n              ref={(el) => {\n                if (rowRefs) rowRefs.current[i] = el;\n              }}\n              id={optionId?.(i)}\n              role=\"option\"\n              aria-selected={i === active}\n              aria-disabled={Boolean(run) || undefined}\n              onPointerMove={() => i !== active && setActive?.(i)}\n              onClick={() => choose?.(i)}\n              className=\"relative flex cursor-pointer items-center gap-3 rounded-xl px-3 text-[13.5px] text-foreground/80\"\n              style={{ height: ROW }}\n            >\n              {item.icon && (\n                <span aria-hidden=\"true\" className=\"flex size-4 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4 [&_svg]:stroke-[1.7]\">\n                  {item.icon}\n                </span>\n              )}\n              <span className=\"min-w-0 truncate\">\n                <Marked text={item.label} marks={row.marks} />\n                {item.items && <span className=\"text-muted-foreground\">…</span>}\n              </span>\n              {item.description && <span className=\"hidden min-w-0 flex-1 truncate text-[12px] text-muted-foreground sm:block\">{item.description}</span>}\n              {/* The right edge: shortcut or chevron, which blurs into a spinner and then a check while it runs. */}\n              <span className=\"relative ml-auto flex h-5 shrink-0 items-center justify-end\">\n                <span className=\"flex items-center gap-1\" style={swap(!state, reduced)}>\n                  {item.items ? (\n                    <svg aria-hidden=\"true\" viewBox=\"0 0 12 12\" className=\"size-3 text-muted-foreground\">\n                      <path d=\"m4.5 2.5 3.5 3.5-3.5 3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                    </svg>\n                  ) : (\n                    item.shortcut?.map((k) => <Kbd key={k}>{k}</Kbd>)\n                  )}\n                </span>\n                <span aria-hidden=\"true\" className=\"absolute inset-y-0 right-0 flex items-center\" style={swap(state === \"pending\", reduced)}>\n                  <span className={`size-3.5 rounded-full border-[1.5px] border-border border-t-foreground/70 motion-reduce:animate-none ${state === \"pending\" ? \"animate-spin\" : \"\"}`} />\n                </span>\n                <span aria-hidden=\"true\" className=\"absolute inset-y-0 right-0 flex items-center text-emerald-600 dark:text-emerald-400\" style={swap(state === \"done\", reduced)}>\n                  <Check drawn={state === \"done\"} reduced={reduced} />\n                </span>\n              </span>\n            </div>\n          </React.Fragment>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function CommandPalette({\n  items,\n  placeholder = \"Search or jump to…\",\n  hotkey = \"k\",\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  showTrigger = true,\n  triggerLabel = \"Search…\",\n  className = \"\",\n  ...props\n}: CommandPaletteProps) {\n  const reduced = useReducedMotion();\n  const id = React.useId();\n  const [phase, setPhase] = React.useState<Phase>(defaultOpen ? \"open\" : \"closed\");\n  const [query, setQuery] = React.useState(\"\");\n  const [stack, setStack] = React.useState<Page[]>([]);\n  const [active, setActive] = React.useState(0);\n  const [run, setRun] = React.useState<RunState | null>(null);\n  const [from, setFrom] = React.useState<Rect | null>(null);\n  const [listHeight, setListHeight] = React.useState(0);\n  const [highlight, setHighlight] = React.useState({ top: 0, ready: false });\n  const [leaving, setLeaving] = React.useState<{ key: number; rows: Row[]; active: number; dir: number }[]>([]);\n  const [enterDir, setEnterDir] = React.useState(0);\n  const [mac, setMac] = React.useState(true);\n  const [portal, setPortal] = React.useState<HTMLElement | null>(null);\n\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const listRef = React.useRef<HTMLDivElement>(null);\n  const innerRef = React.useRef<HTMLDivElement>(null);\n  const paneRef = React.useRef<HTMLDivElement>(null);\n  const rowRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n  const returnFocus = React.useRef<HTMLElement | null>(null);\n  const timer = React.useRef(0);\n  const leaveKey = React.useRef(0);\n\n  const isOpen = phase === \"open\";\n  const mounted = phase !== \"closed\";\n  const page = stack[stack.length - 1];\n  const pageItems = page ? page.items : items;\n  const rows = React.useMemo(() => rank(pageItems, query), [pageItems, query]);\n\n  React.useEffect(() => {\n    setPortal(document.body);\n    setMac(/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent));\n  }, []);\n\n  // Where the pill is, so the palette can grow out of it (and fold back into it).\n  const pillRect = (): Rect | null => {\n    const el = triggerRef.current;\n    if (!el || !showTrigger) return null;\n    const r = el.getBoundingClientRect();\n    // Off screen, or hidden (a pill tucked away on narrow screens has no box): open in place instead.\n    if (!r.width || r.bottom < 0 || r.top > window.innerHeight) return null;\n    return { left: r.left, top: r.top, width: r.width, height: r.height, radius: r.height / 2 };\n  };\n\n  const target = (): Rect => {\n    const vw = document.documentElement.clientWidth;\n    const width = Math.min(WIDTH, vw - 24);\n    return { left: (vw - width) / 2, top: Math.max(48, Math.round(window.innerHeight * 0.16)), width, height: 0, radius: 20 };\n  };\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (openProp === undefined) {\n        window.clearTimeout(timer.current);\n        if (next) {\n          returnFocus.current = document.activeElement as HTMLElement | null;\n          setFrom(pillRect());\n          setPhase(reduced ? \"open\" : \"opening\");\n        } else {\n          setFrom(pillRect());\n          if (reduced) setPhase(\"closed\");\n          else {\n            setPhase(\"closing\");\n            timer.current = window.setTimeout(() => setPhase(\"closed\"), DURATION);\n          }\n          const back = returnFocus.current;\n          requestAnimationFrame(() => (back && document.contains(back) ? back : triggerRef.current)?.focus({ preventScroll: true }));\n        }\n      }\n      onOpenChange?.(next);\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [openProp, onOpenChange, reduced, showTrigger]\n  );\n\n  // Controlled: follow the prop through the same phases.\n  React.useEffect(() => {\n    if (openProp === undefined) return;\n    window.clearTimeout(timer.current);\n    if (openProp && (phase === \"closed\" || phase === \"closing\")) {\n      returnFocus.current = document.activeElement as HTMLElement | null;\n      setFrom(pillRect());\n      setPhase(reduced ? \"open\" : \"opening\");\n    } else if (!openProp && (phase === \"open\" || phase === \"opening\")) {\n      setFrom(pillRect());\n      if (reduced) setPhase(\"closed\");\n      else {\n        setPhase(\"closing\");\n        timer.current = window.setTimeout(() => setPhase(\"closed\"), DURATION);\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [openProp]);\n\n  // Paint at the pill's size first, then let it grow.\n  React.useEffect(() => {\n    if (phase !== \"opening\") return;\n    let inner = 0;\n    const outer = requestAnimationFrame(() => (inner = requestAnimationFrame(() => setPhase(\"open\"))));\n    return () => {\n      cancelAnimationFrame(outer);\n      cancelAnimationFrame(inner);\n    };\n  }, [phase]);\n\n  // A fresh start each time it opens.\n  React.useEffect(() => {\n    if (phase !== \"opening\" && !(phase === \"open\" && reduced)) return;\n    setQuery(\"\");\n    setStack([]);\n    setActive(0);\n    setRun(null);\n    setLeaving([]);\n    setHighlight({ top: 0, ready: false });\n  }, [phase, reduced]);\n\n  React.useEffect(() => {\n    if (isOpen) inputRef.current?.focus({ preventScroll: true });\n  }, [isOpen]);\n\n  React.useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  // The hotkey, from anywhere on the page.\n  React.useEffect(() => {\n    if (!hotkey) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key.toLowerCase() !== hotkey.toLowerCase() || !(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey) return;\n      e.preventDefault();\n      setOpen(!(phase === \"open\" || phase === \"opening\"));\n    };\n    document.addEventListener(\"keydown\", onKey);\n    return () => document.removeEventListener(\"keydown\", onKey);\n  }, [hotkey, phase, setOpen]);\n\n  // The page behind stays where it is.\n  React.useEffect(() => {\n    if (!mounted) return;\n    const html = document.documentElement;\n    const gap = window.innerWidth - html.clientWidth;\n    const before = { overflow: html.style.overflow, paddingRight: html.style.paddingRight };\n    html.style.overflow = \"hidden\";\n    if (gap > 0) html.style.paddingRight = `${gap}px`;\n    return () => {\n      html.style.overflow = before.overflow;\n      html.style.paddingRight = before.paddingRight;\n    };\n  }, [mounted]);\n\n  // The list's height, so the panel can ease to fit the results.\n  React.useLayoutEffect(() => {\n    const el = innerRef.current;\n    if (!el) return;\n    const measure = () => setListHeight((h) => (h === el.offsetHeight ? h : el.offsetHeight));\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [mounted]);\n\n  // Keep the active row in range as the results change.\n  React.useEffect(() => {\n    setActive((a) => Math.min(a, Math.max(0, rows.length - 1)));\n  }, [rows.length]);\n\n  // The highlight glides to the active row; the list scrolls only as far as it must.\n  React.useLayoutEffect(() => {\n    const row = rowRefs.current[active];\n    const list = listRef.current;\n    if (!row || !list) return;\n    const top = row.offsetTop;\n    setHighlight((h) => (h.top === top && h.ready ? h : { top, ready: h.ready || isOpen }));\n    if (top < list.scrollTop) list.scrollTop = top - 4;\n    else if (top + ROW > list.scrollTop + list.clientHeight) list.scrollTop = top + ROW - list.clientHeight + 4;\n  }, [active, rows, isOpen]);\n\n  // A new page slides in from the side you went.\n  React.useLayoutEffect(() => {\n    if (!enterDir || reduced) return;\n    paneRef.current?.animate(\n      [\n        { opacity: 0, filter: \"blur(4px)\", transform: `translateX(${enterDir * 28}px)` },\n        { opacity: 1, filter: \"blur(0px)\", transform: \"none\" },\n      ],\n      { duration: 360, easing: EASE, delay: 40, fill: \"backwards\" }\n    );\n  }, [stack.length, enterDir, reduced]);\n\n  React.useEffect(() => {\n    if (!leaving.length) return;\n    const t = window.setTimeout(() => setLeaving([]), 260);\n    return () => window.clearTimeout(t);\n  }, [leaving]);\n\n  const go = (dir: 1 | -1, next: Page[]) => {\n    if (!reduced) setLeaving((l) => [...l, { key: ++leaveKey.current, rows, active, dir }]);\n    setEnterDir(dir);\n    setStack(next);\n    setQuery(\"\");\n    setActive(0);\n    setHighlight((h) => ({ ...h, ready: false }));\n  };\n\n  const choose = async (i: number) => {\n    const row = rows[i];\n    if (!row || run) return;\n    const { item } = row;\n    setActive(i);\n    if (item.items) return go(1, [...stack, { title: item.label.replace(/…$/, \"\"), items: item.items }]);\n    if (item.href && !item.onSelect) {\n      setOpen(false);\n      window.location.assign(item.href);\n      return;\n    }\n    const result = item.onSelect?.();\n    if (!(result instanceof Promise)) return setOpen(false);\n    setRun({ id: item.id, state: \"pending\" });\n    try {\n      await result;\n      setRun({ id: item.id, state: \"done\" });\n      timer.current = window.setTimeout(() => setOpen(false), reduced ? 0 : 520);\n    } catch {\n      setRun(null);\n      rowRefs.current[i]?.animate(\n        [{ transform: \"none\" }, { transform: \"translateX(-4px)\" }, { transform: \"translateX(4px)\" }, { transform: \"translateX(-2px)\" }, { transform: \"none\" }],\n        { duration: 380, easing: \"ease-out\" }\n      );\n    }\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.nativeEvent.isComposing) return;\n    const moves: Record<string, number> = { ArrowDown: active + 1, ArrowUp: active - 1, Home: 0, End: rows.length - 1 };\n    if (e.key in moves && rows.length) {\n      if ((e.key === \"Home\" || e.key === \"End\") && query) return;\n      e.preventDefault();\n      setActive((moves[e.key] + rows.length) % rows.length);\n    } else if (e.key === \"Enter\") {\n      e.preventDefault();\n      choose(active);\n    } else if (e.key === \"Escape\") {\n      e.preventDefault();\n      if (query) setQuery(\"\");\n      else if (stack.length) go(-1, stack.slice(0, -1));\n      else setOpen(false);\n    } else if (e.key === \"Backspace\" && !query && stack.length) {\n      e.preventDefault();\n      go(-1, stack.slice(0, -1));\n    } else if (e.key === \"Tab\") {\n      // Focus stays in the field; the list is reached with the arrows.\n      e.preventDefault();\n    }\n  };\n\n  const final = mounted ? target() : null;\n  const shape = isOpen || !from ? final : from;\n  const listVisible = Math.min(listHeight, LIST_MAX);\n  const panelHeight = 52 + (listVisible ? listVisible + 12 : 0) + 37;\n  const empty = rows.length === 0;\n  const kbd = mac ? \"⌘\" : \"Ctrl\";\n\n  const panel =\n    mounted && final && shape ? (\n      <div className=\"fixed inset-0 z-[100]\" onKeyDown={onKeyDown}>\n        {/* A faint veil over the page; clicking it closes. */}\n        <div\n          aria-hidden=\"true\"\n          onPointerDown={() => setOpen(false)}\n          className=\"absolute inset-0 bg-background/55\"\n          style={{ opacity: isOpen ? 1 : 0, transition: reduced ? \"none\" : `opacity ${DURATION}ms ${EASE}` }}\n        />\n        <div\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-label=\"Command palette\"\n          className=\"absolute overflow-hidden bg-popover text-popover-foreground\"\n          style={{\n            left: shape.left,\n            top: shape.top,\n            width: shape.width,\n            height: isOpen || !from ? panelHeight : from.height,\n            borderRadius: shape.radius,\n            boxShadow: \"inset 0 0 0 1px var(--border)\",\n            opacity: !from && !isOpen ? 0 : 1,\n            transform: !from && !isOpen ? \"scale(0.97) translateY(-6px)\" : \"none\",\n            transition: reduced\n              ? \"none\"\n              : `left ${DURATION}ms ${EASE}, top ${DURATION}ms ${EASE}, width ${DURATION}ms ${EASE}, height ${DURATION}ms ${EASE}, border-radius ${DURATION}ms ${EASE}, opacity 240ms ${EASE}, transform ${DURATION}ms ${EASE}`,\n          }}\n        >\n          {/* The pill's words, fading as the panel grows out of it. */}\n          {from && (\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute left-0 top-0 flex items-center gap-2 px-3 text-[13px] text-muted-foreground\"\n              style={{ height: from.height, opacity: isOpen ? 0 : 1, transition: reduced ? \"none\" : isOpen ? \"opacity 120ms ease-out\" : \"opacity 200ms ease-out 200ms\" }}\n            >\n              <SearchIcon />\n              {triggerLabel}\n            </span>\n          )}\n\n          <div\n            inert={!isOpen}\n            className=\"absolute left-0 top-0 flex flex-col\"\n            style={{\n              width: final.width,\n              opacity: isOpen ? 1 : 0,\n              transition: reduced ? \"none\" : isOpen ? `opacity 260ms ease-out 120ms` : \"opacity 100ms ease-out\",\n            }}\n          >\n            <div className=\"flex h-[52px] shrink-0 items-center gap-2.5 border-b border-border px-4\">\n              <SearchIcon className=\"text-muted-foreground\" />\n              {/* Where you are, as a chip that opens in when you go into a list. */}\n              <span\n                className=\"grid shrink-0\"\n                style={{\n                  gridTemplateColumns: page ? \"1fr\" : \"0fr\",\n                  opacity: page ? 1 : 0,\n                  transition: reduced ? \"none\" : `grid-template-columns 360ms ${EASE}, opacity 240ms ${EASE}`,\n                }}\n              >\n                <span className=\"min-w-0 overflow-hidden\">\n                  <span className=\"mr-0.5 flex h-6 items-center whitespace-nowrap rounded-md bg-muted px-2 text-[12px] font-medium text-foreground\">\n                    <TextMorph>{page?.title ?? \"\"}</TextMorph>\n                  </span>\n                </span>\n              </span>\n              <input\n                ref={inputRef}\n                value={query}\n                onChange={(e) => {\n                  setQuery(e.target.value);\n                  setActive(0);\n                }}\n                placeholder={page ? `Search ${page.title.toLowerCase()}…` : placeholder}\n                role=\"combobox\"\n                aria-expanded=\"true\"\n                aria-controls={`${id}-list`}\n                aria-activedescendant={rows.length ? `${id}-opt-${active}` : undefined}\n                aria-autocomplete=\"list\"\n                spellCheck={false}\n                className=\"h-full min-w-0 flex-1 bg-transparent text-[14.5px] text-foreground outline-none placeholder:text-muted-foreground\"\n              />\n              <Kbd>esc</Kbd>\n            </div>\n\n            <div\n              ref={listRef}\n              className=\"relative overflow-y-auto overscroll-contain px-1.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n              style={{ height: listVisible ? listVisible + 12 : 0, transition: reduced || !isOpen ? \"none\" : `height 300ms ${EASE}` }}\n            >\n              <div ref={innerRef} className=\"relative py-1.5\">\n                {!empty && (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"absolute inset-x-0 rounded-xl bg-accent\"\n                    style={{\n                      top: 0,\n                      height: ROW,\n                      transform: `translateY(${highlight.top}px)`,\n                      transition: highlight.ready && !reduced ? `transform 200ms ${EASE}` : \"none\",\n                    }}\n                  />\n                )}\n                {leaving.map((l) => (\n                  <LeavingPane key={l.key} dir={l.dir}>\n                    <Results rows={l.rows} active={l.active} run={null} reduced={reduced} />\n                  </LeavingPane>\n                ))}\n                <div ref={paneRef} key={stack.length}>\n                  <Results\n                    rows={rows}\n                    active={active}\n                    run={run}\n                    reduced={reduced}\n                    listId={`${id}-list`}\n                    optionId={(i) => `${id}-opt-${i}`}\n                    setActive={(i) => !run && setActive(i)}\n                    choose={choose}\n                    rowRefs={rowRefs}\n                  />\n                </div>\n                {empty && (\n                  <p role=\"status\" className=\"flex items-center px-3 text-[13px] text-muted-foreground\" style={{ height: ROW }}>\n                    <TextMorph>{query ? `No results for “${query.trim()}”` : \"Nothing here yet\"}</TextMorph>\n                  </p>\n                )}\n              </div>\n            </div>\n\n            <div className=\"flex h-[37px] shrink-0 items-center gap-4 border-t border-border px-4 text-[11.5px] text-muted-foreground\">\n              <span className=\"flex items-center gap-1.5\">\n                <Kbd>↑</Kbd>\n                <Kbd>↓</Kbd>\n                to move\n              </span>\n              <span className=\"flex items-center gap-1.5\">\n                <Kbd>↵</Kbd>\n                to open\n              </span>\n              <span\n                className=\"grid\"\n                style={{\n                  gridTemplateColumns: page ? \"1fr\" : \"0fr\",\n                  opacity: page ? 1 : 0,\n                  transition: reduced ? \"none\" : `grid-template-columns 360ms ${EASE}, opacity 240ms ${EASE}`,\n                }}\n              >\n                <span className=\"flex min-w-0 items-center gap-1.5 overflow-hidden whitespace-nowrap\">\n                  <Kbd>⌫</Kbd>\n                  to go back\n                </span>\n              </span>\n            </div>\n          </div>\n        </div>\n      </div>\n    ) : null;\n\n  return (\n    <div className={`inline-flex ${className}`} {...props}>\n      {showTrigger && (\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          aria-haspopup=\"dialog\"\n          aria-expanded={isOpen}\n          onClick={() => setOpen(true)}\n          className={`group inline-flex h-9 items-center gap-2 rounded-full bg-background pl-3 pr-1.5 text-[13px] text-muted-foreground shadow-[inset_0_0_0_1px_var(--border)] transition-[box-shadow,color] duration-300 hover:text-foreground hover:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_22%,transparent)] ${FOCUS}`}\n          style={{ opacity: mounted ? 0 : 1, transition: mounted || reduced ? \"none\" : \"opacity 160ms ease-out\" }}\n        >\n          <SearchIcon />\n          <span className=\"pr-6\">{triggerLabel}</span>\n          <span className=\"flex items-center gap-0.5\">\n            <Kbd>{kbd}</Kbd>\n            <Kbd>{hotkey.toUpperCase()}</Kbd>\n          </span>\n        </button>\n      )}\n      {portal && panel ? createPortal(panel, portal) : null}\n    </div>\n  );\n}\n\n/** The page you left: it slides out toward the side you came from, then removes itself. */\nfunction LeavingPane({ dir, children }: { dir: number; children: React.ReactNode }) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  React.useLayoutEffect(() => {\n    const animation = ref.current?.animate(\n      [\n        { opacity: 1, filter: \"blur(0px)\", transform: \"none\" },\n        { opacity: 0, filter: \"blur(4px)\", transform: `translateX(${-dir * 28}px)` },\n      ],\n      { duration: 240, easing: EASE, fill: \"forwards\" }\n    );\n    return () => animation?.cancel();\n  }, [dir]);\n  return (\n    <div ref={ref} inert aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-0 top-1.5\">\n      {children}\n    </div>\n  );\n}\n"}],"css":{},"docs":"Docs and live preview: https://hairlineui.com/ui/command-palette"}