{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "help-center-search",
  "title": "Help Center Search",
  "description": "Cmd+K search dialog for help center articles. Keyboard-navigable with arrow keys.",
  "files": [
    {
      "path": "registry/helpbase/components/search-dialog.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { useRouter } from \"next/navigation\"\nimport type { SearchItem } from \"@/lib/search\"\n\ninterface SearchDialogProps {\n  items: SearchItem[]\n}\n\nexport function SearchDialog({ items }: SearchDialogProps) {\n  const [open, setOpen] = useState(false)\n  const [query, setQuery] = useState(\"\")\n  const [selectedIndex, setSelectedIndex] = useState(0)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const listRef = useRef<HTMLDivElement>(null)\n  const router = useRouter()\n\n  // Cmd+K to open\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n        e.preventDefault()\n        setOpen((prev) => !prev)\n      }\n      if (e.key === \"Escape\") {\n        setOpen(false)\n      }\n    }\n    window.addEventListener(\"keydown\", onKeyDown)\n    return () => window.removeEventListener(\"keydown\", onKeyDown)\n  }, [])\n\n  // Focus input when opening\n  useEffect(() => {\n    if (open) {\n      setQuery(\"\")\n      setSelectedIndex(0)\n      // Small delay to let the dialog render\n      requestAnimationFrame(() => inputRef.current?.focus())\n    }\n  }, [open])\n\n  // Filter results\n  const results = useMemo(() => {\n    if (!query.trim()) return items\n    const terms = query.toLowerCase().split(/\\s+/)\n    return items.filter((item) => {\n      const text = `${item.title} ${item.description} ${item.categoryTitle}`.toLowerCase()\n      return terms.every((term) => text.includes(term))\n    })\n  }, [items, query])\n\n  // Scroll selected item into view\n  useEffect(() => {\n    const container = listRef.current\n    if (!container) return\n    const selected = container.querySelector(\"[data-selected=true]\")\n    if (selected) {\n      selected.scrollIntoView({ block: \"nearest\" })\n    }\n  }, [selectedIndex])\n\n  const navigate = useCallback(\n    (href: string) => {\n      setOpen(false)\n      router.push(href)\n    },\n    [router]\n  )\n\n  function onKeyDown(e: React.KeyboardEvent) {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      setSelectedIndex((i) => Math.min(i + 1, results.length - 1))\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault()\n      setSelectedIndex((i) => Math.max(i - 1, 0))\n    } else if (e.key === \"Enter\" && results[selectedIndex]) {\n      e.preventDefault()\n      navigate(results[selectedIndex].href)\n    }\n  }\n\n  if (!open) return null\n\n  return (\n    <>\n      {/* Backdrop */}\n      <div\n        className=\"fixed inset-0 z-50 bg-black/40 backdrop-blur-sm\"\n        onClick={() => setOpen(false)}\n      />\n\n      {/* Dialog */}\n      <div className=\"fixed inset-x-0 top-[20%] z-50 mx-auto w-full max-w-lg px-4\">\n        <div className=\"animate-scale-fade-in overflow-hidden rounded-xl border border-border bg-background shadow-2xl\">\n          {/* Search input */}\n          <div className=\"flex items-center gap-3 border-b border-border px-4\">\n            <SearchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n            <input\n              ref={inputRef}\n              type=\"text\"\n              aria-label=\"Search articles\"\n              placeholder=\"Search articles...\"\n              value={query}\n              onChange={(e) => {\n                setQuery(e.target.value)\n                setSelectedIndex(0)\n              }}\n              onKeyDown={onKeyDown}\n              className=\"h-12 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground\"\n            />\n            <kbd className=\"hidden h-5 items-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground sm:flex\">\n              ESC\n            </kbd>\n          </div>\n\n          {/* Results */}\n          <div ref={listRef} className=\"max-h-72 overflow-y-auto p-2\">\n            {results.length === 0 ? (\n              <div className=\"px-3 py-8 text-center text-sm text-muted-foreground\">\n                No results found for &ldquo;{query}&rdquo;\n              </div>\n            ) : (\n              results.map((item, index) => (\n                <button\n                  key={item.href}\n                  type=\"button\"\n                  data-selected={index === selectedIndex}\n                  onClick={() => navigate(item.href)}\n                  onMouseEnter={() => setSelectedIndex(index)}\n                  className=\"flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-left transition-colors data-[selected=true]:bg-muted\"\n                >\n                  <div className=\"mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-md bg-muted data-[selected=true]:bg-background\">\n                    <FileIcon className=\"size-3.5 text-muted-foreground\" />\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"text-sm font-medium\">{item.title}</div>\n                    <div className=\"mt-0.5 flex items-center gap-2 text-xs text-muted-foreground\">\n                      <span>{item.categoryTitle}</span>\n                      <span className=\"text-border\">·</span>\n                      <span className=\"line-clamp-1\">{item.description}</span>\n                    </div>\n                  </div>\n                  <ReturnIcon className=\"mt-1 size-3.5 shrink-0 text-muted-foreground opacity-0 data-[selected=true]:opacity-100\" />\n                </button>\n              ))\n            )}\n          </div>\n\n          {/* Footer */}\n          <div className=\"flex items-center justify-between border-t border-border px-4 py-2 text-xs text-muted-foreground\">\n            <div className=\"flex items-center gap-3\">\n              <span className=\"flex items-center gap-1\">\n                <kbd className=\"inline-flex size-4 items-center justify-center rounded border border-border bg-muted font-mono text-[10px]\">↑</kbd>\n                <kbd className=\"inline-flex size-4 items-center justify-center rounded border border-border bg-muted font-mono text-[10px]\">↓</kbd>\n                navigate\n              </span>\n              <span className=\"flex items-center gap-1\">\n                <kbd className=\"inline-flex h-4 items-center justify-center rounded border border-border bg-muted px-1 font-mono text-[10px]\">↵</kbd>\n                open\n              </span>\n            </div>\n            <span>{results.length} result{results.length !== 1 ? \"s\" : \"\"}</span>\n          </div>\n        </div>\n      </div>\n    </>\n  )\n}\n\nfunction SearchIcon({ className }: { className?: string }) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n      <circle cx=\"11\" cy=\"11\" r=\"8\" />\n      <path d=\"m21 21-4.3-4.3\" />\n    </svg>\n  )\n}\n\nfunction FileIcon({ className }: { className?: string }) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n      <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n      <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n    </svg>\n  )\n}\n\nfunction ReturnIcon({ className }: { className?: string }) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n      <polyline points=\"9 10 4 15 9 20\" />\n      <path d=\"M20 4v7a4 4 0 0 1-4 4H4\" />\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/search-dialog.tsx"
    },
    {
      "path": "registry/helpbase/components/search-trigger.tsx",
      "content": "\"use client\"\n\nexport function SearchTriggerHero() {\n  return (\n    <button\n      type=\"button\"\n      onClick={() => {\n        window.dispatchEvent(\n          new KeyboardEvent(\"keydown\", {\n            key: \"k\",\n            metaKey: true,\n            bubbles: true,\n          })\n        )\n      }}\n      className=\"group flex h-12 w-full items-center gap-3 rounded-xl border border-border bg-background px-4 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-foreground/20 hover:shadow-md\"\n    >\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        className=\"size-4 shrink-0 text-muted-foreground\"\n      >\n        <circle cx=\"11\" cy=\"11\" r=\"8\" />\n        <path d=\"m21 21-4.3-4.3\" />\n      </svg>\n      <span className=\"flex-1 text-left text-sm text-muted-foreground\">\n        Search for articles...\n      </span>\n      <kbd className=\"pointer-events-none hidden h-5 items-center gap-0.5 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground sm:flex\">\n        <span className=\"text-xs\">&#8984;</span>K\n      </kbd>\n    </button>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/search-trigger.tsx"
    },
    {
      "path": "registry/helpbase/lib/search.ts",
      "content": "import { getAllArticles, getCategories } from \"./content\"\n\nexport interface SearchItem {\n  title: string\n  description: string\n  category: string\n  categoryTitle: string\n  slug: string\n  href: string\n}\n\nexport async function getSearchIndex(): Promise<SearchItem[]> {\n  const [articles, categories] = await Promise.all([\n    getAllArticles(),\n    getCategories(),\n  ])\n\n  const categoryMap = new Map(categories.map((c) => [c.slug, c.title]))\n\n  return articles.map((article) => ({\n    title: article.title,\n    description: article.description,\n    category: article.category,\n    categoryTitle: categoryMap.get(article.category) ?? article.category,\n    slug: article.slug,\n    href: `/${article.category}/${article.slug}`,\n  }))\n}\n",
      "type": "registry:lib",
      "target": "lib/search.ts"
    }
  ],
  "type": "registry:component"
}