{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-search",
  "type": "registry:component",
  "title": "File Search",
  "description": "Search box that streams matching keys with switchable match modes.",
  "dependencies": [
    "files-sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input"
  ],
  "files": [
    {
      "path": "registry/files-sdk/file-search/file-search.tsx",
      "type": "registry:component",
      "target": "components/files-sdk/file-search.tsx",
      "content": "\"use client\";\n\nimport type { SearchMatch, StoredFile } from \"files-sdk\";\nimport type { UseFilesResult } from \"files-sdk/react\";\nimport { FileIcon, Loader2Icon, SearchIcon } from \"lucide-react\";\nimport { useCallback, useId, useRef, useState } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface FileSearchProps {\n  /** A `useFiles()` instance — matches are streamed through `search()`. */\n  files: UseFilesResult;\n  /** Limit the search to keys under this prefix. */\n  prefix?: string;\n  /** Match mode shown first. Default `\"substring\"`. */\n  defaultMatch?: SearchMatch;\n  /** Cap on results collected per search. Default `100`. */\n  maxResults?: number;\n  /** Called when a result row is clicked. */\n  onSelect?: (file: StoredFile) => void;\n  className?: string;\n}\n\nconst MATCH_MODES: SearchMatch[] = [\"substring\", \"glob\", \"regex\", \"exact\"];\n\nconst formatBytes = (bytes: number): string => {\n  if (bytes === 0) {\n    return \"0 B\";\n  }\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  const exponent = Math.min(\n    Math.floor(Math.log(bytes) / Math.log(1024)),\n    units.length - 1\n  );\n  return `${(bytes / 1024 ** exponent).toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n};\n\n/**\n * A search box for a `useFiles()` instance. Streams `search()` results into a\n * list and lets you switch match mode (substring, glob, regex, exact) and toggle\n * case sensitivity. The previous search is aborted when a new one starts.\n */\nexport const FileSearch = ({\n  files,\n  prefix,\n  defaultMatch = \"substring\",\n  maxResults = 100,\n  onSelect,\n  className,\n}: FileSearchProps) => {\n  const [query, setQuery] = useState(\"\");\n  const [match, setMatch] = useState<SearchMatch>(defaultMatch);\n  const [caseInsensitive, setCaseInsensitive] = useState(true);\n  const [results, setResults] = useState<StoredFile[]>([]);\n  const [isSearching, setIsSearching] = useState(false);\n  const [hasSearched, setHasSearched] = useState(false);\n\n  const filesRef = useRef(files);\n  filesRef.current = files;\n  // A per-search controller so a new query cancels the in-flight generator.\n  const controllerRef = useRef<AbortController>(null);\n  const caseId = useId();\n\n  const run = useCallback(\n    async (event: React.FormEvent) => {\n      event.preventDefault();\n      if (!query.trim()) {\n        return;\n      }\n      controllerRef.current?.abort();\n      const controller = new AbortController();\n      controllerRef.current = controller;\n\n      setIsSearching(true);\n      setHasSearched(true);\n      setResults([]);\n      try {\n        const found: StoredFile[] = [];\n        for await (const file of filesRef.current.search(query, {\n          caseInsensitive,\n          match,\n          maxResults,\n          prefix,\n          signal: controller.signal,\n        })) {\n          found.push(file);\n        }\n        if (!controller.signal.aborted) {\n          setResults(found);\n        }\n      } catch {\n        // The hook mirrors the error to `files.error`; an invalid regex lands here.\n      } finally {\n        if (!controller.signal.aborted) {\n          setIsSearching(false);\n        }\n      }\n    },\n    [query, match, caseInsensitive, maxResults, prefix]\n  );\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <form className=\"flex flex-col gap-2\" onSubmit={run}>\n        <div className=\"flex gap-2\">\n          <Input\n            onChange={(event) => setQuery(event.target.value)}\n            placeholder=\"Search keys…\"\n            value={query}\n          />\n          <Button disabled={isSearching || !query.trim()} type=\"submit\">\n            {isSearching ? (\n              <Loader2Icon className=\"animate-spin\" />\n            ) : (\n              <SearchIcon />\n            )}\n            Search\n          </Button>\n        </div>\n        <div className=\"flex flex-wrap items-center gap-1.5\">\n          {MATCH_MODES.map((mode) => (\n            <Button\n              key={mode}\n              onClick={() => setMatch(mode)}\n              size=\"xs\"\n              type=\"button\"\n              variant={mode === match ? \"secondary\" : \"ghost\"}\n            >\n              {mode}\n            </Button>\n          ))}\n          <label\n            className=\"ml-auto flex items-center gap-1.5 text-muted-foreground text-xs\"\n            htmlFor={caseId}\n          >\n            <input\n              aria-label=\"Case-insensitive\"\n              checked={caseInsensitive}\n              className=\"size-3.5 accent-primary\"\n              id={caseId}\n              onChange={(event) => setCaseInsensitive(event.target.checked)}\n              type=\"checkbox\"\n            />\n            Case-insensitive\n          </label>\n        </div>\n      </form>\n\n      {hasSearched && !isSearching && (\n        <p className=\"text-muted-foreground text-xs\">\n          {results.length} {results.length === 1 ? \"match\" : \"matches\"}\n        </p>\n      )}\n\n      <ul className=\"flex flex-col gap-1\">\n        {results.map((file) => (\n          <li key={file.key}>\n            <button\n              className=\"flex w-full items-center gap-3 rounded-lg border border-border p-2 text-left transition-colors hover:bg-muted disabled:cursor-default disabled:hover:bg-transparent\"\n              disabled={!onSelect}\n              onClick={() => onSelect?.(file)}\n              type=\"button\"\n            >\n              <span className=\"flex size-9 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground\">\n                <FileIcon className=\"size-4\" />\n              </span>\n              <span className=\"min-w-0 flex-1\">\n                <span className=\"block truncate font-medium text-sm\">\n                  {file.key}\n                </span>\n                <span className=\"block text-muted-foreground text-xs\">\n                  {formatBytes(file.size)} · {file.type || \"unknown\"}\n                </span>\n              </span>\n            </button>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n};\n"
    }
  ]
}
