{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-dialog",
  "type": "registry:ui",
  "title": "Search Dialog",
  "description": "Async search with filter counts that roll, a live preview and stale-safe loading.",
  "categories": [
    "command"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/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/use-controllable-state.json",
    "https://www.stealth.pm/r/command-palette.json",
    "https://www.stealth.pm/r/copy-button.json",
    "https://www.stealth.pm/r/kbd.json"
  ],
  "files": [
    {
      "path": "components/ui/search-dialog.tsx",
      "content": "\"use client\";\nimport { Autocomplete } from \"@base-ui/react/autocomplete\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport NumberFlow from \"@number-flow/react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Clock, Loader, Search, Warning, X } from \"@/lib/icons\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\nimport {\n  CommandHighlight,\n  PaletteFooter,\n  paletteClasses,\n  scoreCommand,\n  useDelayedFlag,\n  useListGlide,\n  usePaletteHotkey,\n} from \"@/components/ui/command-palette\";\n\nexport type SearchResult = {\n  id: string;\n  title: string;\n  /** Which filter it belongs to: a SearchFilter value. */\n  kind: string;\n  /** Where it lives: \"Docs › Billing\", an email, a project. */\n  subtitle?: string;\n  icon?: React.ReactNode;\n  /** Body text for the preview. Every word of the query is marked in it. */\n  excerpt?: string;\n  /** Label and value rows under the excerpt in the preview. */\n  meta?: [label: string, value: React.ReactNode][];\n  /** Enables Copy link in the preview. */\n  href?: string;\n};\n\nexport type SearchFilter = { value: string; label: string };\n\nexport type SearchDialogProps = {\n  /** Called (debounced) as the query changes. Honor the signal: a newer query aborts the older request. */\n  search: (query: string, options: { signal: AbortSignal }) => Promise<SearchResult[]>;\n  /** Filter tabs after All. Each result's kind picks its tab and its group. */\n  filters?: SearchFilter[];\n  allLabel?: string;\n  onSelect?: (result: SearchResult, query: string) => void;\n  /** Replace the default preview. Receives the highlighted result and the query it matched. */\n  renderPreview?: (result: SearchResult, query: string) => React.ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Recent queries, newest first. Shown while the field is empty. */\n  recent?: string[];\n  defaultRecent?: string[];\n  onRecentChange?: (queries: string[]) => void;\n  /** Milliseconds of quiet typing before a search runs. */\n  debounce?: number;\n  hotkey?: string | false;\n  hotkeyTarget?: React.RefObject<HTMLElement | null>;\n  placeholder?: string;\n  container?: Dialog.Portal.Props[\"container\"];\n  label?: string;\n  className?: string;\n  children?: React.ReactNode;\n};\n\ntype Entry = { key: string; type: \"recent\"; query: string } | { key: string; type: \"result\"; result: SearchResult };\ntype Section = { value: string; items: Entry[] };\n\n// Every occurrence of every word of the query, for marking up an excerpt.\nfunction marks(text: string, query: string): [number, number][] {\n  const lower = text.toLocaleLowerCase();\n  const out: [number, number][] = [];\n  for (const token of query.toLocaleLowerCase().split(/\\s+/).filter((t) => t.length > 1)) {\n    for (let i = lower.indexOf(token); i >= 0; i = lower.indexOf(token, i + token.length)) out.push([i, i + token.length]);\n  }\n  out.sort((a, b) => a[0] - b[0]);\n  const merged: [number, number][] = [];\n  for (const r of out) {\n    const last = merged[merged.length - 1];\n    if (last && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);\n    else merged.push([...r]);\n  }\n  return merged;\n}\n\nfunction Marked({ text, query }: { text: string; query: string }) {\n  const ranges = marks(text, query);\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(text.slice(at, s));\n    parts.push(\n      <mark key={i} className=\"rounded-[2px] bg-fg/[0.12] text-fg\">\n        {text.slice(s, e)}\n      </mark>,\n    );\n    at = e;\n  });\n  parts.push(text.slice(at));\n  return <>{parts}</>;\n}\n\n/**\n * The request lifecycle on its own: debounces the query, aborts the one before,\n * keeps the last results on screen while the next arrive, and reports failure.\n */\nexport function useSearch(search: SearchDialogProps[\"search\"], query: string, debounce = 150) {\n  const [state, setState] = useState<{ results: SearchResult[] | null; for: string; loading: boolean; error: string | null }>({\n    results: null,\n    for: \"\",\n    loading: false,\n    error: null,\n  });\n  const [attempt, setAttempt] = useState(0);\n  const run = useRef(search);\n  useEffect(() => {\n    run.current = search;\n  });\n  const q = query.trim();\n  // A changed query marks the current results as refreshing in the same render, so nothing flickers \"done\" in between.\n  const [seen, setSeen] = useState({ q, attempt });\n  if (seen.q !== q || seen.attempt !== attempt) {\n    setSeen({ q, attempt });\n    setState((s) => (q ? { ...s, loading: true, error: null } : { results: null, for: \"\", loading: false, error: null }));\n  }\n\n  useEffect(() => {\n    if (!q) return;\n    const controller = new AbortController();\n    const timer = window.setTimeout(() => {\n      run\n        .current(q, { signal: controller.signal })\n        .then((results) => {\n          if (!controller.signal.aborted) setState({ results, for: q, loading: false, error: null });\n        })\n        .catch((error: unknown) => {\n          if (controller.signal.aborted) return;\n          const message = error instanceof Error && error.message ? error.message : \"Couldn’t search. Try again.\";\n          setState((s) => ({ ...s, loading: false, error: message }));\n        });\n    }, debounce);\n    return () => {\n      window.clearTimeout(timer);\n      controller.abort();\n    };\n  }, [q, debounce, attempt]);\n\n  return { ...state, retry: () => setAttempt((a) => a + 1) };\n}\n\nexport function SearchDialog({\n  search,\n  filters = [],\n  allLabel = \"All\",\n  onSelect,\n  renderPreview,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  recent: recentProp,\n  defaultRecent = [],\n  onRecentChange,\n  debounce = 150,\n  hotkey = \"/\",\n  hotkeyTarget,\n  placeholder = \"Search…\",\n  container,\n  label = \"Search\",\n  className,\n  children,\n}: SearchDialogProps) {\n  const reduce = !!useReducedMotion();\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  const [filter, setFilter] = useState(\"all\");\n  const [instant, setInstant] = useState(false);\n  const [highlighted, setHighlighted] = useState<{ key: string; byKeyboard: boolean } | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const contained = container != null;\n\n  const { results, for: resultQuery, loading, error, retry } = useSearch(search, query, debounce);\n  const q = query.trim();\n  const skeleton = useDelayedFlag(loading && results === null && !error);\n  const refreshing = useDelayedFlag(loading && results !== null);\n  const { setList: setGlideList, y: glideY, height: glideH, opacity: glideO } = useListGlide(reduce);\n\n  const tabs = useMemo(() => [{ value: \"all\", label: allLabel }, ...filters], [filters, allLabel]);\n  const counts = useMemo(() => {\n    const c: Record<string, number> = { all: results?.length ?? 0 };\n    for (const r of results ?? []) c[r.kind] = (c[r.kind] ?? 0) + 1;\n    return c;\n  }, [results]);\n\n  const sections = useMemo<Section[]>(() => {\n    if (!q) return recent.length ? [{ value: \"Recent searches\", items: recent.map((r) => ({ key: `recent:${r}`, type: \"recent\" as const, query: r })) }] : [];\n    if (!results) return [];\n    const shown = filter === \"all\" ? results : results.filter((r) => r.kind === filter);\n    if (filter !== \"all\") return shown.length ? [{ value: \"\", items: shown.map((result) => ({ key: result.id, type: \"result\" as const, result })) }] : [];\n    // In All, results group by kind in the order of the filters.\n    const order = filters.map((f) => f.value);\n    const kinds = [...new Set(shown.map((r) => r.kind))].sort((a, b) => (order.indexOf(a) + 1 || 99) - (order.indexOf(b) + 1 || 99));\n    return kinds.map((k) => ({\n      value: filters.find((f) => f.value === k)?.label ?? k,\n      items: shown.filter((r) => r.kind === k).map((result) => ({ key: result.id, type: \"result\" as const, result })),\n    }));\n  }, [q, recent, results, filter, filters]);\n\n  const entries = sections.flatMap((s) => s.items);\n  const current = entries.find((e) => e.key === highlighted?.key) ?? entries[0];\n  const preview = current?.type === \"result\" ? current.result : null;\n  const otherFilter = filter !== \"all\" && results?.length ? filters.find((f) => (counts[f.value] ?? 0) > 0 && f.value !== filter) : undefined;\n\n  // Results arrive after the keystroke that asked for them, so the list's own first-row highlight has\n  // already passed. Put it on the first row, as a real arrow key would, whenever a new set lands.\n  const listRef = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!open) return;\n    const frame = requestAnimationFrame(() => {\n      const list = listRef.current;\n      const input = inputRef.current;\n      if (!list || !input || list.querySelector(\"[data-highlighted]\") || !list.querySelector(\"[role=option]\")) return;\n      input.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"ArrowDown\", code: \"ArrowDown\", bubbles: true, cancelable: true }));\n    });\n    return () => cancelAnimationFrame(frame);\n  }, [open, results, filter, recent.length]);\n\n  const close = (fromKeyboard: boolean) => {\n    setInstant(fromKeyboard);\n    setOpen(false);\n  };\n\n  const remember = (text: string) => setRecent((r) => [text, ...r.filter((x) => x !== text)].slice(0, 6));\n  const forget = (text: string) => setRecent((r) => r.filter((x) => x !== text));\n\n  const choose = (entry: Entry, fromKeyboard: boolean) => {\n    if (entry.type === \"recent\") {\n      setQuery(entry.query);\n      inputRef.current?.focus();\n      return;\n    }\n    remember(resultQuery || q);\n    onSelect?.(entry.result, resultQuery || q);\n    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  const onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    // ⇧⌫ forgets the highlighted recent search.\n    if (e.shiftKey && (e.key === \"Backspace\" || e.key === \"Delete\") && current?.type === \"recent\") {\n      e.preventDefault();\n      forget(current.query);\n    }\n  };\n\n  const emptyState = q && !loading && !error && results && entries.length === 0;\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        setFilter(\"all\");\n        setHighlighted(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-6\" : \"fixed pt-[10vh]\", paletteClasses.viewport, \"pb-4\")}>\n          <Dialog.Popup\n            initialFocus={inputRef}\n            data-instant={instant ? \"\" : undefined}\n            className={cn(paletteClasses.popup, \"@container max-w-[720px]\", contained ? \"h-full max-h-[520px]\" : \"h-[min(520px,80dvh)]\", 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                if (details.reason === \"item-press\") return;\n                setQuery(next);\n              }}\n              onOpenChange={(next, details) => {\n                if (!next && details.reason === \"escape-key\") close(true);\n              }}\n              onItemHighlighted={(value: Entry | undefined, details) => setHighlighted(value ? { key: value.key, byKeyboard: details.reason !== \"pointer\" } : null)}\n              itemToStringValue={(entry: Entry) => (entry.type === \"recent\" ? entry.query : entry.result.title)}\n              autoHighlight=\"always\"\n              keepHighlight\n            >\n              <div className=\"flex h-12 shrink-0 items-center gap-2.5 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\", refreshing && \"scale-75 opacity-0\")} />\n                  <Loader size={16} className={cn(\"absolute animate-spin transition-[opacity,scale] duration-150\", !refreshing && \"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=\"search\"\n                  type=\"search\"\n                  onKeyDown={onInputKeyDown}\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              <Tabs.Root value={filter} onValueChange={(v) => setFilter(String(v))} className=\"flex min-h-0 flex-1 flex-col\">\n                <div className=\"shrink-0 overflow-x-auto border-b border-line px-2.5 [scrollbar-width:none]\">\n                  <Tabs.List\n                    aria-label=\"Filter results\"\n                    activateOnFocus\n                    // Typing while a filter has focus goes back to the field, keystroke included.\n                    onKeyDown={(e) => {\n                      if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey && e.key !== \" \") inputRef.current?.focus();\n                    }}\n                    className=\"relative flex h-10 w-max items-center gap-0.5\"\n                  >\n                    <Tabs.Indicator className=\"absolute top-1/2 left-0 h-7 w-(--active-tab-width) -translate-y-1/2 translate-x-(--active-tab-left) rounded-md bg-fg/[0.07] transition-[translate,width] duration-[220ms] ease-in-out-quart motion-reduce:transition-none\" />\n                    {tabs.map((t) => (\n                      <Tabs.Tab\n                        key={t.value}\n                        value={t.value}\n                        className={cn(\n                          \"relative z-[1] flex h-7 items-center gap-1.5 rounded-md px-2.5 text-[12.5px] font-medium text-fg-3 outline-none select-none\",\n                          \"transition-[color,scale] duration-150 hover:text-fg-2 active:scale-[0.97] data-active:text-fg\",\n                          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-fg-3\",\n                        )}\n                      >\n                        {t.label}\n                        {q && results && (\n                          <span className={cn(\"font-mono text-[11px] tabular transition-colors\", counts[t.value] ? \"text-fg-3\" : \"text-fg-4\")}>\n                            <NumberFlow value={counts[t.value] ?? 0} animated={!reduce} />\n                          </span>\n                        )}\n                      </Tabs.Tab>\n                    ))}\n                  </Tabs.List>\n                </div>\n\n                <Tabs.Panel value={filter} className=\"flex min-h-0 flex-1 outline-none\">\n                  <div className=\"relative min-w-0 flex-1 overflow-y-auto overscroll-contain @[600px]:max-w-[55%]\" aria-busy={loading || undefined}>\n                    {error && results && (\n                      <div className=\"flex items-center gap-2 border-b border-line bg-danger-soft px-4 py-2 text-[12px] text-danger\">\n                        <Warning size={14} className=\"shrink-0\" />\n                        <span className=\"min-w-0 flex-1 truncate\">{error} Showing earlier results.</span>\n                        <RetryButton onClick={retry} />\n                      </div>\n                    )}\n                    <div className=\"p-1.5\">\n                      {skeleton && <SkeletonRows />}\n                      {!skeleton && error && !results && (\n                        <div role=\"alert\" className=\"flex flex-col items-center gap-3 px-6 py-12 text-center\">\n                          <p className=\"text-[13px] text-fg-2\">{error}</p>\n                          <RetryButton onClick={retry} />\n                        </div>\n                      )}\n                      {emptyState && (\n                        <div className=\"flex flex-col items-center gap-3 px-6 py-12 text-center\">\n                          <p className=\"max-w-full truncate text-[13px] text-fg-2\">\n                            No {filter === \"all\" ? \"results\" : (filters.find((f) => f.value === filter)?.label.toLowerCase() ?? \"results\")} for{\" \"}\n                            <span className=\"text-fg\">“{resultQuery}”</span>\n                          </p>\n                          {otherFilter ? (\n                            <SmallButton onClick={() => setFilter(otherFilter.value)}>\n                              See {counts[otherFilter.value]} in {otherFilter.label}\n                            </SmallButton>\n                          ) : (\n                            <SmallButton\n                              onClick={() => {\n                                setQuery(\"\");\n                                inputRef.current?.focus();\n                              }}\n                            >\n                              Clear search\n                            </SmallButton>\n                          )}\n                        </div>\n                      )}\n                      {!q && !recent.length && <p className=\"px-3 py-12 text-center text-[13px] text-fg-3\">Search across everything in this workspace</p>}\n                      <div\n                        ref={(node) => {\n                          listRef.current = node;\n                          setGlideList(node);\n                        }}\n                        className={cn(\"group/list relative\", skeleton && \"hidden\")}\n                      >\n                        <motion.div aria-hidden style={{ y: glideY, height: glideH, opacity: glideO }} className={paletteClasses.glide} />\n                        <Autocomplete.List className={cn(\"outline-none empty:hidden transition-opacity duration-150\", loading && results && \"opacity-60\")}>\n                          {(section: Section) => (\n                            <Autocomplete.Group key={section.value || \"results\"} items={section.items}>\n                              {section.value && <Autocomplete.GroupLabel className={cn(paletteClasses.groupLabel, \"first:pt-1.5\")}>{section.value}</Autocomplete.GroupLabel>}\n                              <Autocomplete.Collection>\n                                {(entry: Entry) =>\n                                  entry.type === \"recent\" ? (\n                                    <Autocomplete.Item key={entry.key} value={entry} onClick={(e) => choose(entry, e.detail === 0)} className={paletteClasses.item}>\n                                      <Clock size={16} className=\"shrink-0 text-fg-3\" />\n                                      <span className=\"min-w-0 flex-1 truncate\">{entry.query}</span>\n                                      <button\n                                        type=\"button\"\n                                        tabIndex={-1}\n                                        aria-label={`Remove “${entry.query}” from recent searches`}\n                                        onPointerDown={(e) => e.stopPropagation()}\n                                        onClick={(e) => {\n                                          e.stopPropagation();\n                                          e.preventDefault();\n                                          forget(entry.query);\n                                          inputRef.current?.focus();\n                                        }}\n                                        className={cn(\n                                          \"relative -mr-1 grid size-6 shrink-0 place-items-center rounded-md text-fg-4 opacity-0 transition-[opacity,color,background-color,scale]\",\n                                          \"group-data-highlighted/item:opacity-100 pointer-coarse:opacity-100 hover:bg-fg/[0.06] hover:text-fg-2 active:scale-[0.9]\",\n                                          \"after:absolute after:-inset-2 after:content-['']\",\n                                        )}\n                                      >\n                                        <X size={12} />\n                                      </button>\n                                    </Autocomplete.Item>\n                                  ) : (\n                                    <Autocomplete.Item\n                                      key={entry.key}\n                                      value={entry}\n                                      onClick={(e) => choose(entry, e.detail === 0)}\n                                      className={cn(paletteClasses.item, \"h-12 pointer-coarse:h-14\")}\n                                    >\n                                      {entry.result.icon && (\n                                        <span className=\"flex size-4 shrink-0 text-fg-3 transition-colors group-data-highlighted/item:text-fg-2 [&_svg]:size-4\">{entry.result.icon}</span>\n                                      )}\n                                      <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                                        <span className=\"truncate\">\n                                          <CommandHighlight text={entry.result.title} ranges={scoreCommand({ id: entry.key, label: entry.result.title }, resultQuery)?.ranges ?? []} />\n                                        </span>\n                                        {entry.result.subtitle && <span className=\"truncate text-[12px] text-fg-3\">{entry.result.subtitle}</span>}\n                                      </span>\n                                    </Autocomplete.Item>\n                                  )\n                                }\n                              </Autocomplete.Collection>\n                            </Autocomplete.Group>\n                          )}\n                        </Autocomplete.List>\n                      </div>\n                    </div>\n                  </div>\n\n                  <aside aria-label=\"Preview\" className=\"hidden min-w-0 flex-1 border-l border-line bg-fg/[0.015] @[600px]:flex\">\n                    {preview ? (\n                      <motion.div\n                        key={preview.id}\n                        // Arrow keys swap the preview on the same frame; the pointer gets a short fade so a sweep across rows doesn't strobe.\n                        initial={highlighted?.byKeyboard === false && !reduce ? { opacity: 0 } : false}\n                        animate={{ opacity: 1 }}\n                        transition={{ duration: 0.14, ease: \"easeOut\" }}\n                        className=\"flex min-w-0 flex-1 flex-col\"\n                      >\n                        <div className=\"flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto overscroll-contain p-5 [&>*]:shrink-0\">\n                          {renderPreview ? renderPreview(preview, resultQuery) : <DefaultPreview result={preview} query={resultQuery} />}\n                        </div>\n                        {!renderPreview && (\n                          // Actions stay put at the bottom however long the preview runs.\n                          <div className=\"flex shrink-0 items-center gap-2 border-t border-line px-5 py-3\">\n                            <button\n                              type=\"button\"\n                              onClick={() => choose({ key: preview.id, type: \"result\", result: preview }, false)}\n                              className={cn(\n                                \"inline-flex h-7 items-center gap-1.5 rounded-md bg-fg px-2.5 text-[12px] font-medium text-frame\",\n                                \"transition-[background-color,scale] duration-150 hover:bg-fg/90 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                              Open\n                              <Kbd keys=\"enter\" variant=\"ghost\" size=\"sm\" className=\"[&_kbd]:text-frame/60\" />\n                            </button>\n                            {preview.href && <CopyButton value={preview.href} size=\"sm\" label=\"Copy link\" copiedLabel=\"Copied\" />}\n                          </div>\n                        )}\n                      </motion.div>\n                    ) : (\n                      <div className=\"grid flex-1 place-items-center px-6 text-center text-[12.5px] text-fg-4\">{skeleton || error ? \"\" : q ? \"Nothing to preview\" : \"Results show a preview here\"}</div>\n                    )}\n                  </aside>\n                </Tabs.Panel>\n              </Tabs.Root>\n\n              <PaletteFooter\n                hints={\n                  current?.type === \"recent\"\n                    ? [\n                        [\"enter\", \"Search again\"],\n                        [\"shift+backspace\", \"Remove\"],\n                      ]\n                    : [\n                        [[\"up\", \"down\"], \"Navigate\"],\n                        [\"enter\", \"Open\"],\n                        [\"tab\", \"Filters\"],\n                      ]\n                }\n              />\n              <Autocomplete.Status className=\"sr-only\">\n                {!q ? \"\" : loading ? \"Searching…\" : error ? error : `${entries.length} ${entries.length === 1 ? \"result\" : \"results\"}`}\n              </Autocomplete.Status>\n            </Autocomplete.Root>\n          </Dialog.Popup>\n        </Dialog.Viewport>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n}\n\nfunction DefaultPreview({ result, query }: { result: SearchResult; query: string }) {\n  return (\n    <>\n      <div className=\"flex min-w-0 items-center gap-2 text-[12px] text-fg-3\">\n        {result.icon && <span className=\"flex shrink-0 [&_svg]:size-3.5\">{result.icon}</span>}\n        <span className=\"truncate\">{result.subtitle}</span>\n      </div>\n      <h3 className=\"text-[15px] leading-[1.3] font-medium tracking-[-0.015em] text-fg text-balance\">\n        <Marked text={result.title} query={query} />\n      </h3>\n      {result.excerpt && (\n        <p className=\"line-clamp-5 text-[12.5px] leading-[1.55] text-fg-2 text-pretty\">\n          <Marked text={result.excerpt} query={query} />\n        </p>\n      )}\n      {result.meta && (\n        <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 border-t border-line pt-3 text-[12px]\">\n          {result.meta.map(([k, v]) => (\n            <div key={k} className=\"contents\">\n              <dt className=\"text-fg-3\">{k}</dt>\n              <dd className=\"min-w-0 truncate text-fg-2\">{v}</dd>\n            </div>\n          ))}\n        </dl>\n      )}\n    </>\n  );\n}\n\nfunction SkeletonRows() {\n  return (\n    <div aria-hidden className=\"flex flex-col\">\n      {[62, 48, 71, 55].map((w, i) => (\n        <div key={i} className=\"flex h-12 items-center gap-2.5 px-2.5\">\n          <span className=\"size-4 shrink-0 rounded-[4px] bg-fg/[0.07] animate-pulse-soft\" />\n          <span className=\"flex flex-1 flex-col gap-1.5\">\n            <span className=\"h-2.5 rounded-full bg-fg/[0.08] animate-pulse-soft\" style={{ width: `${w}%` }} />\n            <span className=\"h-2 rounded-full bg-fg/[0.05] animate-pulse-soft\" style={{ width: `${w - 22}%` }} />\n          </span>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction SmallButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\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      {children}\n    </button>\n  );\n}\n\nfunction RetryButton({ onClick }: { onClick: () => void }) {\n  return <SmallButton onClick={onClick}>Try again</SmallButton>;\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/search-dialog"
}
