{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-1",
  "title": "Search 1: results page",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A search results page: a large search box, the type of result as a filter with live counts, a date filter and sort, results with a trail and highlighted words, and a no-results page that suggests what to try.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/kbd.json",
    "https://ui.ballmac.com/r/match-highlight.json",
    "https://ui.ballmac.com/r/select.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/search-1/search-1.tsx",
      "content": "// Ballmac UI: Search 1. https://ui.ballmac.com/blocks/search-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUpRight, ChevronRight, FileText, FolderKanban, Lightbulb, Paperclip, Search, SearchX, User, X } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Kbd } from \"@/components/ballmac/kbd\"\nimport { countMatches, MatchHighlight, matchesAll, queryTokens } from \"@/components/ballmac/match-highlight\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ballmac/select\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Search1Kind = \"doc\" | \"person\" | \"project\" | \"file\"\n\ntype Search1Result = {\n  id: string\n  title: string\n  /** An excerpt around the match. Also searched. */\n  snippet?: string\n  /** Where it lives, as a trail: [\"Docs\", \"API\", \"Webhooks\"]. Also searched. */\n  path?: string[]\n  kind: Search1Kind\n  /** Already written for people (\"3 days ago\"). */\n  updated?: string\n  /** How many days ago it changed, for the date filter and for sorting by newest. */\n  ageDays?: number\n  author?: string\n  href?: string\n}\n\ntype Search1Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"results\"> &\n  StateProps & {\n    /** Everything that can be found. The page filters it by the search, type and date. */\n    results?: Search1Result[]\n    /** The search when the page opens. */\n    defaultQuery?: string\n    /** Called with every change of the search. Debounce it before calling your API. */\n    onQueryChange?: (query: string) => void\n    /** Called when a result is opened. Results with an href are also real links. */\n    onOpen?: (result: Search1Result) => void\n    /** How long the search took, already formatted (\"0.14 s\"). */\n    timing?: string\n    /** How many results show before \"Show more\". */\n    pageSize?: number\n    /** A spelling you suggest when there are few results (\"webhook\" for \"webhok\"). */\n    didYouMean?: string\n  }\n\nconst kinds: Record<Search1Kind, { label: string; plural: string; icon: typeof FileText; tile: string }> = {\n  doc: { label: \"Doc\", plural: \"Docs\", icon: FileText, tile: \"bg-chart-1/20\" },\n  person: { label: \"Person\", plural: \"People\", icon: User, tile: \"bg-chart-2/20\" },\n  project: { label: \"Project\", plural: \"Projects\", icon: FolderKanban, tile: \"bg-chart-4/20\" },\n  file: { label: \"File\", plural: \"Files\", icon: Paperclip, tile: \"bg-chart-3/25\" },\n}\n\nconst sample: Search1Result[] = [\n  { id: \"r1\", kind: \"doc\", title: \"Verify webhook signatures\", snippet: \"Every webhook request is signed. Check the signature header against your signing secret before you trust the payload, and reject requests older than five minutes.\", path: [\"Docs\", \"API\", \"Webhooks\"], updated: \"3 days ago\", ageDays: 3, author: \"Priya Shah\", href: \"#\" },\n  { id: \"r2\", kind: \"doc\", title: \"Webhook events and retries\", snippet: \"Every event we send, its payload, and how failed webhook deliveries retry: five attempts over 24 hours with exponential backoff.\", path: [\"Docs\", \"API\", \"Webhooks\"], updated: \"last week\", ageDays: 9, author: \"Alex Kim\", href: \"#\" },\n  { id: \"r3\", kind: \"project\", title: \"Webhook relay\", snippet: \"A small service that receives webhooks, stores them for replay, and forwards them to your local machine while you build.\", path: [\"Projects\", \"Developer tools\"], updated: \"2 days ago\", ageDays: 2, author: \"Mara Ellison\", href: \"#\" },\n  { id: \"r4\", kind: \"person\", title: \"Priya Shah\", snippet: \"Platform engineer. Wrote the webhook retry logic and maintains the delivery dashboard.\", path: [\"People\", \"Engineering\"], updated: \"online now\", ageDays: 0, href: \"#\" },\n  { id: \"r5\", kind: \"file\", title: \"webhook-event-schema.json\", snippet: \"JSON Schema for all 42 webhook event types, generated from the API definition on every release.\", path: [\"Files\", \"Schemas\"], updated: \"2 weeks ago\", ageDays: 16, author: \"Theo Nakamura\", href: \"#\" },\n  { id: \"r6\", kind: \"doc\", title: \"Set up a webhook endpoint\", snippet: \"Create an endpoint in the dashboard, choose the events it receives, and send a test event to check it works.\", path: [\"Docs\", \"Getting started\"], updated: \"last month\", ageDays: 34, author: \"Alex Kim\", href: \"#\" },\n  { id: \"r7\", kind: \"file\", title: \"Webhook incident review.pdf\", snippet: \"What went wrong on 3 March when delayed webhook deliveries affected 6% of customers, and what changed after.\", path: [\"Files\", \"Reports\"], updated: \"7 months ago\", ageDays: 220, author: \"Jordan Lee\", href: \"#\" },\n  { id: \"r8\", kind: \"doc\", title: \"Create and rotate an API key\", snippet: \"Keys are shown once. Here is how to replace one safely without downtime, including keys used by webhook consumers.\", path: [\"Docs\", \"API\", \"Authentication\"], updated: \"4 days ago\", ageDays: 4, author: \"Priya Shah\", href: \"#\" },\n  { id: \"r9\", kind: \"person\", title: \"Sam Okafor\", snippet: \"Support lead. Answers webhook and billing questions from customers every day.\", path: [\"People\", \"Support\"], updated: \"away\", ageDays: 0, href: \"#\" },\n  { id: \"r10\", kind: \"project\", title: \"Event inspector\", snippet: \"A browser tool for looking at webhook payloads, headers and delivery attempts as they arrive.\", path: [\"Projects\", \"Developer tools\"], updated: \"5 weeks ago\", ageDays: 36, author: \"Theo Nakamura\", href: \"#\" },\n  { id: \"r11\", kind: \"doc\", title: \"Download invoices and receipts\", snippet: \"Find every invoice as a PDF, with your company details and tax number on it.\", path: [\"Docs\", \"Billing\"], updated: \"2 months ago\", ageDays: 61, author: \"Sam Okafor\", href: \"#\" },\n  { id: \"r12\", kind: \"file\", title: \"Rate limits.xlsx\", snippet: \"Limits per plan for every API route, with the headers that tell you how much is left.\", path: [\"Files\", \"Reference\"], updated: \"3 months ago\", ageDays: 92, author: \"Priya Shah\", href: \"#\" },\n]\n\nconst rangeDays = { any: Infinity, week: 7, month: 30, year: 365 } as const\ntype Range = keyof typeof rangeDays\n\nconst initials = (name: string) => name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)\n\nfunction SearchSkeleton() {\n  return (\n    <div className=\"grid gap-8 lg:grid-cols-[14rem_minmax(0,1fr)]\" aria-hidden=\"true\">\n      <div className=\"grid content-start gap-2\">\n        {[0, 1, 2, 3, 4].map((i) => (\n          <Skeleton key={i} className=\"h-9 rounded-lg\" />\n        ))}\n      </div>\n      <div className=\"grid gap-3\">\n        {[0, 1, 2, 3].map((i) => (\n          <div key={i} className=\"flex gap-4 rounded-xl border p-4\">\n            <Skeleton className=\"size-10 shrink-0 rounded-lg\" />\n            <div className=\"grid flex-1 gap-2\">\n              <Skeleton className=\"h-3 w-40\" />\n              <Skeleton className=\"h-4 w-2/3\" />\n              <Skeleton className=\"h-3 w-full\" />\n              <Skeleton className=\"h-3 w-4/5\" />\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\n/**\n * A search results page: a large search box (press / to reach it), the kind of result as a filter with live counts, a date filter and\n * a sort, results with a trail, highlighted words and who changed them last, and a no-results page that helps instead of just saying no.\n */\nfunction Search1({\n  results = sample,\n  defaultQuery = \"webhook\",\n  onQueryChange,\n  onOpen,\n  timing,\n  pageSize = 5,\n  didYouMean,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Search1Props) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [query, setQuery] = React.useState(defaultQuery)\n  const [kind, setKind] = React.useState<\"all\" | Search1Kind>(\"all\")\n  const [range, setRange] = React.useState<Range>(\"any\")\n  const [sort, setSort] = React.useState<\"relevance\" | \"newest\">(\"relevance\")\n  const [shown, setShown] = React.useState(pageSize)\n  const ready = state === \"ready\" || state === \"empty\"\n\n  const tokens = queryTokens(query)\n  const haystack = (r: Search1Result) => `${r.title} ${r.snippet ?? \"\"} ${(r.path ?? []).join(\" \")} ${r.author ?? \"\"}`\n  const matched = state === \"empty\" ? [] : results.filter((r) => matchesAll(haystack(r), tokens))\n  const inRange = matched.filter((r) => range === \"any\" || (r.ageDays ?? 0) <= rangeDays[range])\n  const counts = (Object.keys(kinds) as Search1Kind[]).map((k) => ({ kind: k, n: inRange.filter((r) => r.kind === k).length })).filter((c) => results.some((r) => r.kind === c.kind))\n  const filtered = inRange.filter((r) => kind === \"all\" || r.kind === kind)\n  const score = (r: Search1Result) => countMatches(r.title, tokens) * 3 + countMatches(r.snippet ?? \"\", tokens) + countMatches((r.path ?? []).join(\" \"), tokens)\n  const ordered = [...filtered].sort((a, b) => (sort === \"newest\" ? (a.ageDays ?? 0) - (b.ageDays ?? 0) : score(b) - score(a)))\n  const visible = ordered.slice(0, shown)\n  const filtersOn = kind !== \"all\" || range !== \"any\"\n\n  React.useEffect(() => setShown(pageSize), [query, kind, range, sort, pageSize])\n  // \"/\" jumps to the search from anywhere on the page, unless you are typing in a field\n  React.useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"/\" || e.metaKey || e.ctrlKey || e.altKey) return\n      const el = e.target as HTMLElement | null\n      if (el && (el.tagName === \"INPUT\" || el.tagName === \"TEXTAREA\" || el.tagName === \"SELECT\" || el.isContentEditable)) return\n      e.preventDefault()\n      inputRef.current?.focus()\n    }\n    document.addEventListener(\"keydown\", onKey)\n    return () => document.removeEventListener(\"keydown\", onKey)\n  }, [])\n\n  const change = (value: string) => {\n    setQuery(value)\n    onQueryChange?.(value)\n  }\n\n  const heading = !tokens.length ? \"Search\" : filtered.length === 1 ? \"1 result\" : `${filtered.length} results`\n  const noMatch = ready && filtered.length === 0\n\n  const content = (\n    <div className=\"grid gap-8 lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-10\">\n      <aside aria-label=\"Filters\" className=\"grid content-start gap-6\">\n        <fieldset className=\"grid gap-2\">\n          <legend className=\"text-muted-foreground mb-1 text-xs font-medium\">Type</legend>\n          <div className=\"flex flex-wrap gap-2 lg:grid lg:gap-0.5\">\n            {[{ kind: \"all\" as const, n: inRange.length }, ...counts].map((c) => {\n              const label = c.kind === \"all\" ? \"All\" : kinds[c.kind].plural\n              const Icon = c.kind === \"all\" ? Search : kinds[c.kind].icon\n              return (\n                <label\n                  key={c.kind}\n                  className={cn(\n                    \"has-[:focus-visible]:ring-ring/50 flex cursor-pointer items-center gap-2.5 rounded-full border px-3 py-1.5 text-sm transition-colors duration-150 has-[:focus-visible]:ring-[3px]\",\n                    \"hover:bg-accent/60 has-[:checked]:bg-accent has-[:checked]:font-medium has-[:checked]:border-foreground/25\",\n                    \"lg:rounded-lg lg:border-transparent lg:px-2.5 lg:has-[:checked]:border-transparent\",\n                    !c.n && c.kind !== \"all\" && \"text-muted-foreground\"\n                  )}\n                >\n                  <input type=\"radio\" name={`${id}-kind`} value={c.kind} checked={kind === c.kind} onChange={() => setKind(c.kind)} className=\"sr-only\" />\n                  <Icon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n                  <span className=\"flex-1\">{label}</span>\n                  <span className=\"text-muted-foreground text-xs tabular-nums\">{c.n}</span>\n                </label>\n              )\n            })}\n          </div>\n        </fieldset>\n        <div className=\"grid gap-2\">\n          <span id={`${id}-range`} className=\"text-muted-foreground text-xs font-medium\">\n            Updated\n          </span>\n          <Select value={range} onValueChange={(v) => setRange(v as Range)}>\n            <SelectTrigger aria-labelledby={`${id}-range`} className=\"w-full lg:w-full\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"any\">Any time</SelectItem>\n              <SelectItem value=\"week\">Past week</SelectItem>\n              <SelectItem value=\"month\">Past month</SelectItem>\n              <SelectItem value=\"year\">Past year</SelectItem>\n            </SelectContent>\n          </Select>\n        </div>\n        {filtersOn ? (\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            className=\"justify-start\"\n            onClick={() => {\n              setKind(\"all\")\n              setRange(\"any\")\n            }}\n          >\n            <X aria-hidden=\"true\" />\n            Clear filters\n          </Button>\n        ) : null}\n      </aside>\n\n      <div className=\"min-w-0\">\n        {noMatch ? (\n          <div className=\"bg-card rounded-2xl border px-6 py-14 text-center\">\n            <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n              <SearchX className=\"size-6\" />\n            </span>\n            <h2 className=\"mt-5 text-lg font-semibold tracking-tight text-balance\">{emptyState?.title ?? (tokens.length ? `No results for “${query.trim()}”` : \"Nothing to search yet\")}</h2>\n            <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-sm text-sm text-pretty\">{emptyState?.description ?? (filtersOn ? \"Nothing matches with these filters on.\" : \"We looked everywhere we could.\")}</p>\n            {didYouMean && tokens.length ? (\n              <p className=\"mt-4 text-sm\">\n                Did you mean{\" \"}\n                <button type=\"button\" onClick={() => change(didYouMean)} className=\"focus-visible:ring-ring/50 rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-[3px]\">\n                  {didYouMean}\n                </button>\n                ?\n              </p>\n            ) : null}\n            <ul className=\"text-muted-foreground mx-auto mt-6 grid max-w-xs gap-2 text-start text-sm\">\n              {[\"Check the spelling\", \"Try fewer or more general words\", \"Try a different type or time\"].map((t) => (\n                <li key={t} className=\"flex items-center gap-2.5\">\n                  <Lightbulb aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n                  {t}\n                </li>\n              ))}\n            </ul>\n            <div className=\"mt-6 flex flex-wrap justify-center gap-2\">\n              {filtersOn ? (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() => {\n                    setKind(\"all\")\n                    setRange(\"any\")\n                  }}\n                >\n                  Clear filters\n                </Button>\n              ) : null}\n              {emptyState?.action}\n            </div>\n          </div>\n        ) : (\n          <>\n            <div className=\"mb-4 flex flex-wrap items-center gap-x-4 gap-y-2\">\n              <h2 className=\"text-sm font-medium tabular-nums\">\n                {heading}\n                {tokens.length ? (\n                  <span className=\"text-muted-foreground font-normal\">\n                    {\" \"}\n                    for “{query.trim()}”{timing ? ` · ${timing}` : \"\"}\n                  </span>\n                ) : null}\n              </h2>\n              <p role=\"status\" className=\"sr-only\">\n                {tokens.length ? `${heading} for ${query.trim()}` : \"\"}\n              </p>\n              <div className=\"ms-auto flex items-center gap-2\">\n                <span id={`${id}-sort`} className=\"text-muted-foreground text-xs\">\n                  Sort by\n                </span>\n                <Select value={sort} onValueChange={(v) => setSort(v as typeof sort)}>\n                  <SelectTrigger size=\"sm\" aria-labelledby={`${id}-sort`} className=\"w-32\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectItem value=\"relevance\">Relevance</SelectItem>\n                    <SelectItem value=\"newest\">Newest</SelectItem>\n                  </SelectContent>\n                </Select>\n              </div>\n            </div>\n            {didYouMean && tokens.length && filtered.length < 3 ? (\n              <p className=\"text-muted-foreground mb-4 text-sm\">\n                Did you mean{\" \"}\n                <button type=\"button\" onClick={() => change(didYouMean)} className=\"text-foreground focus-visible:ring-ring/50 rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-[3px]\">\n                  {didYouMean}\n                </button>\n                ?\n              </p>\n            ) : null}\n            <ol key={`${query}|${kind}|${range}|${sort}`} aria-label=\"Results\" className=\"animate-in fade-in-0 slide-in-from-bottom-1 grid gap-3 duration-300 motion-reduce:animate-none\">\n              {visible.map((r) => {\n                const K = kinds[r.kind]\n                const Title = r.href ? \"a\" : \"button\"\n                return (\n                  <li key={r.id} className=\"bg-card hover:border-foreground/25 focus-within:ring-ring/50 group/result relative flex gap-4 rounded-xl border p-4 transition-[border-color,box-shadow] duration-200 focus-within:ring-[3px] hover:shadow-sm\">\n                    {r.kind === \"person\" ? (\n                      <span aria-hidden=\"true\" className={cn(\"flex size-10 shrink-0 items-center justify-center rounded-full text-sm font-semibold\", K.tile)}>\n                        {initials(r.title)}\n                      </span>\n                    ) : (\n                      <span aria-hidden=\"true\" className={cn(\"flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg]:size-[1.15rem]\", K.tile)}>\n                        <K.icon />\n                      </span>\n                    )}\n                    <div className=\"min-w-0 flex-1\">\n                      {r.path?.length ? (\n                        <p className=\"text-muted-foreground flex flex-wrap items-center gap-x-1 text-xs\">\n                          {r.path.map((p, i) => (\n                            <React.Fragment key={p}>\n                              {i ? <ChevronRight aria-hidden=\"true\" className=\"size-3 rtl:rotate-180\" /> : null}\n                              <span>{p}</span>\n                            </React.Fragment>\n                          ))}\n                        </p>\n                      ) : null}\n                      <h3 className=\"mt-0.5 text-base font-semibold tracking-tight text-pretty\">\n                        <Title\n                          {...(r.href ? { href: r.href } : { type: \"button\" as const })}\n                          onClick={() => onOpen?.(r)}\n                          className=\"group-hover/result:text-primary rounded-sm text-start outline-none after:absolute after:inset-0 after:content-['']\"\n                        >\n                          <MatchHighlight text={r.title} query={tokens} />\n                        </Title>\n                      </h3>\n                      {r.snippet ? (\n                        <p className=\"text-muted-foreground mt-1 line-clamp-2 text-sm leading-relaxed text-pretty\">\n                          <MatchHighlight text={r.snippet} query={tokens} />\n                        </p>\n                      ) : null}\n                      <p className=\"text-muted-foreground mt-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs\">\n                        <span className=\"text-foreground/80 font-medium\">{K.label}</span>\n                        {r.updated ? <span>{r.kind === \"person\" ? r.updated : `Updated ${r.updated}`}</span> : null}\n                        {r.author && r.kind !== \"person\" ? <span>by {r.author}</span> : null}\n                      </p>\n                    </div>\n                    <ArrowUpRight aria-hidden=\"true\" className=\"text-muted-foreground group-hover/result:text-foreground size-4 shrink-0 transition-transform duration-200 group-hover/result:-translate-y-0.5 group-hover/result:translate-x-0.5 motion-reduce:transition-none motion-reduce:group-hover/result:translate-x-0 motion-reduce:group-hover/result:translate-y-0 rtl:-scale-x-100 rtl:group-hover/result:-translate-x-0.5\" />\n                  </li>\n                )\n              })}\n            </ol>\n            {ordered.length > visible.length ? (\n              <div className=\"mt-6 flex flex-col items-center gap-2\">\n                <p className=\"text-muted-foreground text-xs tabular-nums\">\n                  Showing {visible.length} of {ordered.length}\n                </p>\n                <Button variant=\"outline\" onClick={() => setShown((n) => n + pageSize)}>\n                  Show {Math.min(pageSize, ordered.length - visible.length)} more\n                </Button>\n              </div>\n            ) : null}\n          </>\n        )}\n      </div>\n    </div>\n  )\n\n  return (\n    <section data-slot=\"search-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <h1 id={`${id}-title`} className=\"sr-only\">\n        Search\n      </h1>\n      <div className=\"focus-within:border-ring focus-within:ring-ring/40 bg-background mb-8 flex h-14 items-center gap-3 rounded-2xl border px-4 shadow-[0_8px_24px_-14px_rgb(0_0_0/0.25)] transition-shadow focus-within:ring-[3px]\">\n        <Search aria-hidden=\"true\" className=\"text-muted-foreground size-5 shrink-0\" />\n        <input\n          ref={inputRef}\n          type=\"search\"\n          aria-label=\"Search\"\n          aria-describedby={`${id}-hint`}\n          autoComplete=\"off\"\n          spellCheck={false}\n          disabled={!ready}\n          value={query}\n          placeholder=\"Search docs, people, projects and files\"\n          onChange={(e) => change(e.target.value)}\n          className=\"placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-base outline-none disabled:opacity-50 [&::-webkit-search-cancel-button]:hidden\"\n        />\n        {query ? (\n          <button\n            type=\"button\"\n            aria-label=\"Clear search\"\n            onClick={() => {\n              change(\"\")\n              inputRef.current?.focus()\n            }}\n            className=\"text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring/50 flex size-7 items-center justify-center rounded-md outline-none focus-visible:ring-[3px]\"\n          >\n            <X aria-hidden=\"true\" className=\"size-4\" />\n          </button>\n        ) : (\n          <Kbd className=\"hidden sm:inline-flex\" aria-hidden=\"true\">\n            /\n          </Kbd>\n        )}\n      </div>\n      <p id={`${id}-hint`} className=\"sr-only\">\n        Results update as you type. Press slash anywhere on the page to jump here.\n      </p>\n      <StateView\n        state={state === \"loading\" || state === \"error\" ? state : \"ready\"}\n        onRetry={onRetry}\n        label=\"Searching\"\n        loading={<SearchSkeleton />}\n        error={{ title: errorState?.title ?? \"Search is not available right now\", description: errorState?.description ?? \"Your search was not lost. Check your connection and try again.\", ...errorState }}\n      >\n        {content}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Search1, type Search1Props, type Search1Result, type Search1Kind }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/search-1/search-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "search",
      "results",
      "filters",
      "facets",
      "highlight",
      "query",
      "docs",
      "site search",
      "enterprise search"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "A site or workspace search results page. <Search1 results={[{ id, kind: 'doc' | 'person' | 'project' | 'file', title, snippet, path: ['Docs', 'API'], updated, ageDays, author, href }]} defaultQuery onQueryChange onOpen timing pageSize didYouMean />. It filters and ranks the results you pass; for server search, pass the results your API returned for onQueryChange and keep the filters here.",
      "whenToUse": [
        "The page behind a site, docs or workspace search",
        "Mixed results of several types that people narrow by type and date"
      ],
      "whenNotToUse": [
        "A quick-jump palette (use command or spotlight-search)",
        "Searching inside one list (use search-field with the list)",
        "A help center home (use help-1)"
      ],
      "composesWith": [
        "header-2",
        "footer-2",
        "app-shell-1",
        "match-highlight"
      ],
      "a11y": [
        {
          "keys": "/",
          "action": "Jumps to the search from anywhere on the page, unless you are typing in a field"
        },
        {
          "keys": "Arrow keys on the Type filter",
          "action": "Move between types (native radio buttons); counts are part of each label"
        },
        {
          "keys": "Typing",
          "action": "The result count is announced politely; each result is one link with a single tab stop"
        }
      ],
      "customization": [
        "pageSize and Show more; timing for \"0.14 s\"",
        "ageDays drives the Updated filter and Newest sort; updated is the label people read",
        "didYouMean appears when there are fewer than three results and on the no-results page"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "search-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton filters and results under the real search box, announced as searching; the box is disabled."
      },
      {
        "id": "empty",
        "title": "No results",
        "example": "search-1-no-results",
        "trigger": "state=\"empty\" (or a search that matches nothing)",
        "note": "A page that says what was searched, suggests a spelling (didYouMean), lists what to try and offers to clear filters; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not search",
        "example": "search-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying the search was kept, with Try again (onRetry)."
      }
    ],
    "examples": [
      "search-1-demo",
      "search-1-few",
      "search-1-no-results",
      "search-1-loading",
      "search-1-error"
    ],
    "url": "https://ui.ballmac.com/components/search-1"
  },
  "categories": [
    "blocks",
    "search"
  ],
  "type": "registry:block"
}