{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-3",
  "title": "Grouped Command Palette",
  "description": "Command style search panel grouping results into Pages, People, and Files with icons, avatars, and meta, plus keyboard hints in the footer.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "avatar",
    "input",
    "kbd"
  ],
  "files": [
    {
      "path": "registry/blocks/search/3/search-block.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Input } from \"@/components/ui/input\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\nimport { cn } from \"@/lib/utils\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\n/** Props a call site may pass through to an icon. */\ntype IconProps = { className?: string; size?: number | string }\n\n/** An icon held in data and rendered later, e.g. `<item.icon className=\"size-4\" />`. */\ntype IconRenderer = (props: IconProps) => React.ReactNode\n\ntype Item = {\n  label: string\n  meta: string\n  icon?: IconRenderer\n  avatar?: string\n}\n\nconst groups: { label: string; items: Item[] }[] = [\n  {\n    label: \"Pages\",\n    items: [\n      {\n        label: \"Getting started\",\n        meta: \"Docs\",\n        icon: (p: IconProps) => (\n          <IconPlaceholder\n            lucide=\"FileText\"\n            tabler=\"IconFileText\"\n            hugeicons=\"File01Icon\"\n            phosphor=\"FileText\"\n            remixicon=\"RiFileTextLine\"\n            {...p}\n          />\n        ),\n      },\n      {\n        label: \"Theming and tokens\",\n        meta: \"Docs\",\n        icon: (p: IconProps) => (\n          <IconPlaceholder\n            lucide=\"FileText\"\n            tabler=\"IconFileText\"\n            hugeicons=\"File01Icon\"\n            phosphor=\"FileText\"\n            remixicon=\"RiFileTextLine\"\n            {...p}\n          />\n        ),\n      },\n      {\n        label: \"Deploying to production\",\n        meta: \"Docs\",\n        icon: (p: IconProps) => (\n          <IconPlaceholder\n            lucide=\"FileText\"\n            tabler=\"IconFileText\"\n            hugeicons=\"File01Icon\"\n            phosphor=\"FileText\"\n            remixicon=\"RiFileTextLine\"\n            {...p}\n          />\n        ),\n      },\n    ],\n  },\n  {\n    label: \"People\",\n    items: [\n      {\n        label: \"Priya Nair\",\n        meta: \"Staff Engineer\",\n        avatar: \"https://i.pravatar.cc/64?img=32\",\n      },\n      {\n        label: \"Leo Tanaka\",\n        meta: \"Product Designer\",\n        avatar: \"https://i.pravatar.cc/64?img=12\",\n      },\n    ],\n  },\n  {\n    label: \"Files\",\n    items: [\n      {\n        label: \"Q3 Roadmap.pdf\",\n        meta: \"2.4 MB\",\n        icon: (p: IconProps) => (\n          <IconPlaceholder\n            lucide=\"File\"\n            tabler=\"IconFile\"\n            hugeicons=\"FileIcon\"\n            phosphor=\"File\"\n            remixicon=\"RiFile3Line\"\n            {...p}\n          />\n        ),\n      },\n    ],\n  },\n]\n\nexport default function SearchBlock() {\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n  const itemRefs = React.useRef<(HTMLAnchorElement | null)[]>([])\n\n  const q = query.trim().toLowerCase()\n  const filteredGroups = groups\n    .map((group) => ({\n      ...group,\n      items: q\n        ? group.items.filter((item) =>\n            `${item.label} ${item.meta}`.toLowerCase().includes(q)\n          )\n        : group.items,\n    }))\n    .filter((group) => group.items.length > 0)\n\n  // Assign each visible row a flat index so arrow keys can move a highlight\n  // across groups.\n  let counter = 0\n  const indexedGroups = filteredGroups.map((group) => ({\n    label: group.label,\n    items: group.items.map((item) => ({ item, idx: counter++ })),\n  }))\n  const flatCount = counter\n  const activeIndex = flatCount ? Math.min(active, flatCount - 1) : 0\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      setActive((i) => Math.min(i + 1, flatCount - 1))\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault()\n      setActive((i) => Math.max(i - 1, 0))\n    } else if (e.key === \"Enter\") {\n      e.preventDefault()\n      itemRefs.current[activeIndex]?.click()\n    }\n  }\n\n  return (\n    <section className=\"flex w-full items-center justify-center bg-muted/30 px-6 py-16 text-foreground\">\n      <div className=\"w-full max-w-md rounded-lg border border-border bg-background\">\n        <div className=\"border-b border-border p-3\">\n          <div className=\"relative\">\n            <IconPlaceholder\n              lucide=\"Search\"\n              tabler=\"IconSearch\"\n              hugeicons=\"SearchIcon\"\n              phosphor=\"MagnifyingGlass\"\n              remixicon=\"RiSearchLine\"\n              className=\"absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground\"\n              aria-hidden=\"true\"\n            />\n            <Input\n              type=\"search\"\n              value={query}\n              onChange={(e) => {\n                setQuery(e.target.value)\n                setActive(0)\n              }}\n              onKeyDown={onKeyDown}\n              placeholder=\"Search pages, people, files...\"\n              aria-label=\"Search\"\n              className=\"pl-9\"\n            />\n          </div>\n        </div>\n\n        {flatCount === 0 ? (\n          <div className=\"px-4 py-12 text-center text-sm text-muted-foreground\">\n            No results for &ldquo;{query}&rdquo;.\n          </div>\n        ) : (\n          <div className=\"flex flex-col gap-4 p-3\">\n            {indexedGroups.map((group) => (\n              <div key={group.label} className=\"flex flex-col gap-1\">\n                <p className=\"px-2 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase\">\n                  {group.label}\n                </p>\n                {group.items.map(({ item, idx }) => {\n                  const isActive = idx === activeIndex\n                  return (\n                    <a\n                      key={item.label}\n                      ref={(el) => {\n                        itemRefs.current[idx] = el\n                      }}\n                      href=\"#\"\n                      onMouseEnter={() => setActive(idx)}\n                      className={cn(\n                        \"flex items-center gap-2.5 rounded-md px-2 py-2 text-sm transition-colors\",\n                        isActive ? \"bg-muted/60\" : \"hover:bg-muted/60\"\n                      )}\n                    >\n                      {item.avatar ? (\n                        <Avatar className=\"size-5\">\n                          <AvatarImage\n                            src={item.avatar}\n                            alt={item.label}\n                            className=\"grayscale\"\n                          />\n                          <AvatarFallback className=\"text-[9px]\">\n                            {item.label[0]}\n                          </AvatarFallback>\n                        </Avatar>\n                      ) : item.icon ? (\n                        <item.icon\n                          className=\"size-4 shrink-0 text-muted-foreground\"\n                          aria-hidden=\"true\"\n                        />\n                      ) : null}\n                      <span className=\"flex-1 truncate\">{item.label}</span>\n                      <span className=\"text-xs text-muted-foreground\">\n                        {item.meta}\n                      </span>\n                    </a>\n                  )\n                })}\n              </div>\n            ))}\n          </div>\n        )}\n\n        <div className=\"flex items-center gap-3 border-t border-border px-4 py-2.5 text-xs text-muted-foreground\">\n          <span className=\"flex items-center gap-1.5\">\n            <KbdGroup>\n              <Kbd>↑</Kbd>\n              <Kbd>↓</Kbd>\n            </KbdGroup>\n            Navigate\n          </span>\n          <span className=\"flex items-center gap-1.5\">\n            <Kbd>\n              <IconPlaceholder\n                lucide=\"CornerDownLeft\"\n                tabler=\"IconCornerDownLeft\"\n                hugeicons=\"CornerDownLeftIcon\"\n                phosphor=\"ArrowElbowDownLeft\"\n                remixicon=\"RiCornerDownLeftLine\"\n                className=\"size-3\"\n                aria-hidden=\"true\"\n              />\n            </Kbd>\n            Open\n          </span>\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/search-3.tsx"
    }
  ],
  "meta": {
    "height": "592px",
    "tier": "free"
  },
  "categories": [
    "search"
  ],
  "type": "registry:block"
}