{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-search",
  "title": "Product Search",
  "description": "Batteries-included search experience: search bar, configurable filters, and an infinite-scroll results grid wired to useProductSearch.",
  "dependencies": [
    "@channel3/sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://ui.trychannel3.com/r/product-grid.json",
    "https://ui.trychannel3.com/r/search-bar.json",
    "https://ui.trychannel3.com/r/product-filters.json",
    "https://ui.trychannel3.com/r/use-product-search.json"
  ],
  "files": [
    {
      "path": "registry/default/components/product-search.tsx",
      "content": "import * as React from \"react\";\nimport type { OptionValue, Product } from \"@channel3/sdk/resources\";\nimport { SlidersHorizontal } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductGrid } from \"@/registry/default/components/product-grid\";\nimport { SearchBar } from \"@/registry/default/components/search-bar\";\nimport {\n  type BrandSearcher,\n  type CategoryLoader,\n  type CategorySearcher,\n  type WebsiteSearcher,\n  ProductFilters,\n  ProductFiltersBar,\n  ProductFiltersRoot,\n} from \"@/registry/default/components/product-filters\";\nimport {\n  type SearchFetcher,\n  useProductSearch,\n} from \"@/registry/default/hooks/use-product-search\";\n\nconst filterTransition = \"duration-200 ease-out\";\n\nfunction filterCollapseClass(open: boolean) {\n  return cn(\n    \"grid transition-[grid-template-rows,opacity]\",\n    filterTransition,\n    open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n  );\n}\n\n/** Reads an image file as base64 (without the `data:` URI prefix). */\nfunction readBase64(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      const result = String(reader.result);\n      resolve(result.slice(result.indexOf(\",\") + 1));\n    };\n    reader.onerror = () => reject(reader.error);\n    reader.readAsDataURL(file);\n  });\n}\n\nexport interface ProductSearchProps extends Omit<React.ComponentProps<\"div\">, \"onSelect\"> {\n  /** Server-side search fetcher (wraps `client.products.search` / `searchByImage`). */\n  fetchSearch: SearchFetcher;\n  searchBrands?: BrandSearcher;\n  searchWebsites?: WebsiteSearcher;\n  searchCategories?: CategorySearcher;\n  getCategory?: CategoryLoader;\n  filtersLayout?: \"sidebar\" | \"bar\";\n  colorPercentages?: boolean;\n  imageSearch?: boolean;\n  searchOnMount?: boolean;\n  paginate?: boolean;\n  initialQuery?: string;\n  placeholder?: string;\n  onSelect?: (product: Product) => void;\n  onSelectVariant?: (product: Product, value: OptionValue) => void;\n  showSwatches?: boolean;\n  locale?: string;\n}\n\nexport function ProductSearch({\n  fetchSearch,\n  searchBrands,\n  searchWebsites,\n  searchCategories,\n  getCategory,\n  filtersLayout = \"sidebar\",\n  colorPercentages = false,\n  imageSearch = false,\n  searchOnMount = false,\n  paginate = true,\n  initialQuery = \"\",\n  placeholder,\n  onSelect,\n  onSelectVariant,\n  showSwatches,\n  locale,\n  className,\n  ...props\n}: ProductSearchProps) {\n  const {\n    query,\n    setQuery,\n    filters,\n    setFilters,\n    searchByImage,\n    image,\n    results,\n    isLoading,\n    isLoadingMore,\n    hasMore,\n    sentinelRef,\n    submit,\n  } = useProductSearch({ fetchSearch, initialQuery, searchOnMount });\n\n  const bar = filtersLayout === \"bar\";\n  const [showFilters, setShowFilters] = React.useState(!bar);\n\n  const handleImage = async (file: File) => {\n    const base64Image = await readBase64(file);\n    searchByImage({ base64Image, label: file.name });\n  };\n\n  const imageNotice = image ? (\n    <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n      <span>Searching by image{image.label ? `: ${image.label}` : \"\"}</span>\n      <button\n        type=\"button\"\n        onClick={() => searchByImage(null)}\n        className=\"font-medium text-foreground underline-offset-4 hover:underline\"\n      >\n        Clear\n      </button>\n    </div>\n  ) : null;\n\n  const resultsBody =\n    query.trim().length === 0 && image == null && results.length === 0 && !isLoading ? (\n      <p className=\"py-16 text-center text-sm text-muted-foreground\">\n        Search for products to get started.\n      </p>\n    ) : (\n      <ProductGrid\n        products={results}\n        loading={isLoading}\n        onSelect={onSelect}\n        onSelectVariant={onSelectVariant}\n        showSwatches={showSwatches}\n        locale={locale}\n      />\n    );\n\n  const resultsColumn = (\n    <div className=\"flex flex-col gap-6\">\n      {resultsBody}\n      {isLoadingMore ? (\n        <p className=\"text-center text-sm text-muted-foreground\">Loading more…</p>\n      ) : null}\n      {paginate && hasMore ? (\n        <div ref={sentinelRef} aria-hidden className=\"h-px\" />\n      ) : null}\n    </div>\n  );\n\n  const searchInput = (\n    <SearchBar\n      value={query}\n      onValueChange={setQuery}\n      onSubmit={submit}\n      placeholder={placeholder}\n      loading={isLoading}\n      onImageSelected={imageSearch ? handleImage : undefined}\n      className=\"w-full max-w-xl\"\n    />\n  );\n\n  const filterToggle = (visibility: string) => (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      size=\"icon\"\n      className={cn(\"size-10 shrink-0\", visibility)}\n      aria-label=\"Toggle filters\"\n      aria-expanded={showFilters}\n      onClick={() => setShowFilters((value) => !value)}\n    >\n      <SlidersHorizontal className=\"size-4\" />\n    </Button>\n  );\n\n  if (bar) {\n    return (\n      <div data-slot=\"product-search\" className={cn(\"flex flex-col gap-4\", className)} {...props}>\n        <div className=\"flex items-center justify-center gap-2\">\n          {searchInput}\n          {filterToggle(\"\")}\n        </div>\n        {imageNotice}\n        <ProductFiltersRoot\n          value={filters}\n          onChange={setFilters}\n          searchBrands={searchBrands}\n          searchWebsites={searchWebsites}\n          searchCategories={searchCategories}\n          getCategory={getCategory}\n          colorPercentages={colorPercentages}\n        >\n          <div className={filterCollapseClass(showFilters)}>\n            <div className=\"overflow-hidden\">\n              <ProductFiltersBar />\n            </div>\n          </div>\n        </ProductFiltersRoot>\n        {resultsColumn}\n      </div>\n    );\n  }\n\n  return (\n    <div data-slot=\"product-search\" className={cn(\"flex flex-col gap-4\", className)} {...props}>\n      <div className=\"flex items-center gap-2\">\n        {searchInput}\n        {filterToggle(\"\")}\n      </div>\n\n      {imageNotice}\n\n      <div\n        className={cn(\n          \"grid grid-cols-1 gap-6 md:transition-[grid-template-columns]\",\n          filterTransition,\n          showFilters ? \"md:grid-cols-[13rem_minmax(0,1fr)]\" : \"md:grid-cols-[0fr_minmax(0,1fr)]\",\n        )}\n      >\n        <aside\n          className={cn(\n            \"min-w-0 overflow-hidden max-md:grid\",\n            filterCollapseClass(showFilters),\n            !showFilters && \"max-md:pointer-events-none\",\n          )}\n          aria-hidden={!showFilters}\n        >\n          <div\n            className={cn(\n              \"max-md:overflow-hidden md:w-[13rem] md:sticky md:top-4 md:transition-opacity\",\n              filterTransition,\n              showFilters ? \"md:opacity-100\" : \"md:pointer-events-none md:opacity-0\",\n            )}\n          >\n            <ProductFilters\n              value={filters}\n              onChange={setFilters}\n              searchBrands={searchBrands}\n              searchWebsites={searchWebsites}\n              searchCategories={searchCategories}\n              getCategory={getCategory}\n              colorPercentages={colorPercentages}\n            />\n          </div>\n        </aside>\n\n        {resultsColumn}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}
