{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-filters",
  "title": "Product Filters",
  "description": "Compound, configurable filter panel (stacked sidebar or horizontal popover bar): price, gender, age, condition, availability, hex color palette with opt-in percentages, brand and category typeahead, and category-scoped attribute filters.",
  "dependencies": [
    "@channel3/sdk",
    "lucide-react",
    "react-colorful"
  ],
  "registryDependencies": [
    "accordion",
    "badge",
    "button",
    "input",
    "slider",
    "popover",
    "https://ui.trychannel3.com/r/search.json",
    "https://ui.trychannel3.com/r/use-async-options.json"
  ],
  "files": [
    {
      "path": "registry/default/components/product-filters.tsx",
      "content": "import * as React from \"react\";\nimport type { Brand, Category, CategoryAttribute, CategorySummary, Website } from \"@channel3/sdk/resources\";\nimport { Check, ChevronDown, Pipette, Plus, X } from \"lucide-react\";\nimport { HexColorPicker } from \"react-colorful\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport {\n  AGE_OPTIONS,\n  AVAILABILITY_OPTIONS,\n  categoryAttributeGroups,\n  CONDITION_OPTIONS,\n  countActiveFilters,\n  countCategoryAttributes,\n  deriveAttributes,\n  type DimensionRange,\n  EMPTY_FILTERS,\n  facetCounts,\n  GENDER_OPTIONS,\n  LENGTH_UNIT_OPTIONS,\n  type LengthUnit,\n  normalizeHex,\n  type SearchFiltersState,\n  setAttributeValues,\n  setColorPercentage,\n  WEIGHT_UNIT_OPTIONS,\n  type WeightUnit,\n} from \"@/registry/default/lib/search\";\nimport { useAsyncOptions } from \"@/registry/default/hooks/use-async-options\";\n\n/** Searches brands by free text (wraps `client.brands.search`). */\nexport type BrandSearcher = (query: string) => Promise<Brand[]>;\nexport type WebsiteSearcher = (query: string) => Promise<Website[]>;\n/** Searches categories by free text (wraps `client.categories.search`). */\nexport type CategorySearcher = (query: string) => Promise<CategorySummary[]>;\n/** Loads a full category (with attributes) by slug (wraps `client.categories.retrieve`). */\nexport type CategoryLoader = (slug: string) => Promise<Category>;\n\ntype FiltersUpdater = Partial<SearchFiltersState> | ((current: SearchFiltersState) => Partial<SearchFiltersState>);\n\ninterface ProductFiltersContextValue {\n  filters: SearchFiltersState;\n  update: (updater: FiltersUpdater) => void;\n  searchBrands?: BrandSearcher;\n  searchWebsites?: WebsiteSearcher;\n  searchCategories?: CategorySearcher;\n  getCategory?: CategoryLoader;\n  /** Reveal a per-color target-share slider on each selected color. */\n  colorPercentages: boolean;\n}\n\nconst ProductFiltersContext = React.createContext<ProductFiltersContextValue | null>(null);\n\nfunction useProductFilters(component: string): ProductFiltersContextValue {\n  const context = React.useContext(ProductFiltersContext);\n  if (!context) {\n    throw new Error(`${component} must be used within <ProductFilters> or <ProductFiltersRoot>`);\n  }\n  return context;\n}\n\nexport interface ProductFiltersProps extends Omit<React.ComponentProps<\"div\">, \"onChange\"> {\n  value: SearchFiltersState;\n  onChange: (filters: SearchFiltersState) => void;\n  searchBrands?: BrandSearcher;\n  searchWebsites?: WebsiteSearcher;\n  searchCategories?: CategorySearcher;\n  getCategory?: CategoryLoader;\n  /** Reveal a per-color target-share slider on each selected color. Defaults to off. */\n  colorPercentages?: boolean;\n}\n\nfunction Root({\n  value,\n  onChange,\n  searchBrands,\n  searchWebsites,\n  searchCategories,\n  getCategory,\n  colorPercentages = false,\n  children,\n  ...rest\n}: ProductFiltersProps & { children: React.ReactNode }) {\n  const ref = React.useRef(value);\n  ref.current = value;\n\n  const update = React.useCallback(\n    (updater: FiltersUpdater) => {\n      const base = ref.current;\n      const patch = typeof updater === \"function\" ? updater(base) : updater;\n      const next = { ...base, ...patch };\n      ref.current = next;\n      onChange(next);\n    },\n    [onChange],\n  );\n\n  const context = React.useMemo<ProductFiltersContextValue>(\n    () => ({ filters: value, update, searchBrands, searchWebsites, searchCategories, getCategory, colorPercentages }),\n    [value, update, searchBrands, searchWebsites, searchCategories, getCategory, colorPercentages],\n  );\n\n  return (\n    <ProductFiltersContext.Provider value={context}>\n      <div data-slot=\"product-filters\" {...rest}>\n        {children}\n      </div>\n    </ProductFiltersContext.Provider>\n  );\n}\n\nfunction Field({\n  label,\n  children,\n  className,\n}: {\n  label: React.ReactNode;\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      <span className=\"text-sm font-medium\">{label}</span>\n      {children}\n    </div>\n  );\n}\n\nfunction Chip({ children, onRemove }: { children: React.ReactNode; onRemove: () => void }) {\n  return (\n    <span className={cn(filterPillClass(true), \"max-w-full min-w-0 pr-1\")}>\n      <span\n        className=\"inline-flex min-w-0 items-center gap-1 overflow-hidden\"\n        title={typeof children === \"string\" ? children : undefined}\n      >\n        {typeof children === \"string\" ? (\n          <span className=\"truncate\">{children}</span>\n        ) : (\n          children\n        )}\n      </span>\n      <button\n        type=\"button\"\n        onClick={onRemove}\n        aria-label=\"Remove\"\n        className=\"flex size-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-background hover:text-foreground\"\n      >\n        <X className=\"size-3\" />\n      </button>\n    </span>\n  );\n}\n\nconst TOGGLE_FACET_CLASS = \"flex flex-wrap gap-1.5\";\n\nconst FILTER_PILL_BASE =\n  \"inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-xs font-medium whitespace-nowrap transition-colors outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50\";\n\nfunction filterPillClass(selected: boolean) {\n  return cn(\n    FILTER_PILL_BASE,\n    selected\n      ? \"border-foreground/40 bg-accent text-accent-foreground\"\n      : \"border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground\",\n  );\n}\n\nconst NO_SPINNER_CLASS =\n  \"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none\";\n\nfunction parseNumberInput(raw: string): number | null {\n  if (raw.trim() === \"\") {\n    return null;\n  }\n  const value = Number(raw);\n  return Number.isFinite(value) ? value : null;\n}\n\nconst THIN_SCROLLBAR_CLASS =\n  \"[scrollbar-width:thin] [scrollbar-color:var(--border)_transparent] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border\";\n\nfunction sliderThumb(value: number | readonly number[], index: number): number | undefined {\n  if (Array.isArray(value)) {\n    return value[index];\n  }\n  return index === 0 ? (value as number) : undefined;\n}\n\nfunction priceFromSliderRange(\n  range: number | readonly number[],\n  max: number,\n): { minPrice: number | null; maxPrice: number | null } {\n  const low = sliderThumb(range, 0) ?? 0;\n  const high = sliderThumb(range, 1) ?? max;\n  return {\n    minPrice: low <= 0 ? null : low,\n    maxPrice: high >= max ? null : high,\n  };\n}\n\nfunction PriceControl({ max = 1000, step = 10 }: { max?: number; step?: number }) {\n  const { filters, update } = useProductFilters(\"ProductFiltersPrice\");\n  const { minPrice, maxPrice } = filters.price;\n\n  const setPrice = (next: { minPrice?: number | null; maxPrice?: number | null }) =>\n    update((current) => ({ price: { ...current.price, ...next } }));\n\n  const sliderDefault: [number, number] = [minPrice ?? 0, maxPrice ?? max];\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"flex items-center gap-2\">\n        <div className=\"relative flex-1\">\n          <span className=\"pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-sm text-muted-foreground\">\n            $\n          </span>\n          <Input\n            type=\"number\"\n            inputMode=\"decimal\"\n            min={0}\n            aria-label=\"Minimum price\"\n            placeholder=\"Min\"\n            value={minPrice ?? \"\"}\n            onChange={(event) => setPrice({ minPrice: parseNumberInput(event.target.value) })}\n            className={cn(\"pl-6\", NO_SPINNER_CLASS)}\n          />\n        </div>\n        <span className=\"text-muted-foreground\">–</span>\n        <div className=\"relative flex-1\">\n          <span className=\"pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-sm text-muted-foreground\">\n            $\n          </span>\n          <Input\n            type=\"number\"\n            inputMode=\"decimal\"\n            min={0}\n            aria-label=\"Maximum price\"\n            placeholder=\"Max\"\n            value={maxPrice ?? \"\"}\n            onChange={(event) => setPrice({ maxPrice: parseNumberInput(event.target.value) })}\n            className={cn(\"pl-6\", NO_SPINNER_CLASS)}\n          />\n        </div>\n      </div>\n      <Slider\n        key={`${sliderDefault[0]}-${sliderDefault[1]}`}\n        min={0}\n        max={max}\n        step={step}\n        defaultValue={sliderDefault}\n        onValueCommit={(range) => setPrice(priceFromSliderRange(range, max))}\n        aria-label=\"Price range\"\n        className=\"mt-1\"\n      />\n    </div>\n  );\n}\n\nfunction Price(props: { max?: number; step?: number }) {\n  return (\n    <Field label=\"Price\">\n      <PriceControl {...props} />\n    </Field>\n  );\n}\n\ntype ToggleFacetProps<V extends string> =\n  | {\n      type: \"single\";\n      options: ReadonlyArray<{ value: V; label: string }>;\n      value: V | null;\n      onChange: (value: V | null) => void;\n    }\n  | {\n      type: \"multiple\";\n      options: ReadonlyArray<{ value: V; label: string }>;\n      value: V[];\n      onChange: (value: V[]) => void;\n    };\n\nfunction ToggleFacet<V extends string>(props: ToggleFacetProps<V>) {\n  const isSelected = (value: V) =>\n    props.type === \"single\" ? props.value === value : props.value.includes(value);\n\n  const toggle = (value: V) => {\n    if (props.type === \"single\") {\n      props.onChange(props.value === value ? null : value);\n      return;\n    }\n    props.onChange(\n      props.value.includes(value)\n        ? props.value.filter((entry) => entry !== value)\n        : [...props.value, value],\n    );\n  };\n\n  return (\n    <div className={TOGGLE_FACET_CLASS}>\n      {props.options.map((option) => {\n        const selected = isSelected(option.value);\n        return (\n          <button\n            key={option.value}\n            type=\"button\"\n            aria-pressed={selected}\n            onClick={() => toggle(option.value)}\n            className={filterPillClass(selected)}\n          >\n            {option.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction GenderControl() {\n  const { filters, update } = useProductFilters(\"ProductFiltersGender\");\n  return (\n    <ToggleFacet\n      type=\"single\"\n      options={GENDER_OPTIONS}\n      value={filters.gender}\n      onChange={(gender) => update({ gender })}\n    />\n  );\n}\n\nfunction Gender() {\n  return (\n    <Field label=\"Gender\">\n      <GenderControl />\n    </Field>\n  );\n}\n\nfunction AgeControl() {\n  const { filters, update } = useProductFilters(\"ProductFiltersAge\");\n  return (\n    <ToggleFacet\n      type=\"multiple\"\n      options={AGE_OPTIONS}\n      value={filters.age}\n      onChange={(age) => update({ age })}\n    />\n  );\n}\n\nfunction Age() {\n  return (\n    <Field label=\"Age\">\n      <AgeControl />\n    </Field>\n  );\n}\n\nfunction ConditionControl() {\n  const { filters, update } = useProductFilters(\"ProductFiltersCondition\");\n  return (\n    <ToggleFacet\n      type=\"single\"\n      options={CONDITION_OPTIONS}\n      value={filters.condition}\n      onChange={(condition) => update({ condition })}\n    />\n  );\n}\n\nfunction Condition() {\n  return (\n    <Field label=\"Condition\">\n      <ConditionControl />\n    </Field>\n  );\n}\n\nfunction AvailabilityControl() {\n  const { filters, update } = useProductFilters(\"ProductFiltersAvailability\");\n  return (\n    <ToggleFacet\n      type=\"multiple\"\n      options={AVAILABILITY_OPTIONS}\n      value={filters.availability}\n      onChange={(availability) => update({ availability })}\n    />\n  );\n}\n\nfunction Availability() {\n  return (\n    <Field label=\"Availability\">\n      <AvailabilityControl />\n    </Field>\n  );\n}\n\nfunction UnitDropdown<V extends string>({\n  label,\n  options,\n  value,\n  onChange,\n}: {\n  label: string;\n  options: ReadonlyArray<{ value: V; label: string }>;\n  value: V;\n  onChange: (value: V) => void;\n}) {\n  const [open, setOpen] = React.useState(false);\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger\n        type=\"button\"\n        aria-label={label}\n        className={cn(buttonVariants({ variant: \"outline\", size: \"sm\" }), \"h-7 gap-1 px-2 text-xs\")}\n      >\n        {value}\n        <ChevronDown className=\"size-3 text-muted-foreground\" aria-hidden />\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" className=\"w-auto min-w-[4.5rem] p-1\">\n        <ul>\n          {options.map((option) => (\n            <li key={option.value}>\n              <button\n                type=\"button\"\n                onClick={() => {\n                  onChange(option.value);\n                  setOpen(false);\n                }}\n                className=\"flex w-full items-center justify-between gap-3 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent\"\n              >\n                {option.label}\n                {option.value === value ? <Check className=\"size-3.5\" aria-hidden /> : null}\n              </button>\n            </li>\n          ))}\n        </ul>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction DimensionRow({\n  label,\n  range,\n  onChange,\n}: {\n  label: string;\n  range: DimensionRange;\n  onChange: (next: Partial<DimensionRange>) => void;\n}) {\n  return (\n    <div className=\"flex items-center gap-2\">\n      <span className=\"w-12 shrink-0 text-sm text-muted-foreground\">{label}</span>\n      <Input\n        type=\"number\"\n        inputMode=\"decimal\"\n        min={0}\n        aria-label={`Minimum ${label.toLowerCase()}`}\n        placeholder=\"Min\"\n        value={range.min ?? \"\"}\n        onChange={(event) => onChange({ min: parseNumberInput(event.target.value) })}\n        className={cn(\"h-8 flex-1\", NO_SPINNER_CLASS)}\n      />\n      <span className=\"text-muted-foreground\">–</span>\n      <Input\n        type=\"number\"\n        inputMode=\"decimal\"\n        min={0}\n        aria-label={`Maximum ${label.toLowerCase()}`}\n        placeholder=\"Max\"\n        value={range.max ?? \"\"}\n        onChange={(event) => onChange({ max: parseNumberInput(event.target.value) })}\n        className={cn(\"h-8 flex-1\", NO_SPINNER_CLASS)}\n      />\n    </div>\n  );\n}\n\nfunction DimensionsControl() {\n  const { filters, update } = useProductFilters(\"ProductFiltersDimensions\");\n  const { dimensions } = filters;\n\n  const setRange = (\n    field: \"length\" | \"width\" | \"height\" | \"weight\",\n    next: Partial<DimensionRange>,\n  ) =>\n    update((current) => ({\n      dimensions: {\n        ...current.dimensions,\n        [field]: { ...current.dimensions[field], ...next },\n      },\n    }));\n\n  const setLengthUnit = (lengthUnit: LengthUnit) =>\n    update((current) => ({ dimensions: { ...current.dimensions, lengthUnit } }));\n  const setWeightUnit = (weightUnit: WeightUnit) =>\n    update((current) => ({ dimensions: { ...current.dimensions, weightUnit } }));\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"flex items-center justify-between gap-2\">\n        <div className=\"flex items-center gap-1.5\">\n          <span className=\"text-xs text-muted-foreground\">Length</span>\n          <UnitDropdown\n            label=\"Length unit\"\n            options={LENGTH_UNIT_OPTIONS}\n            value={dimensions.lengthUnit}\n            onChange={setLengthUnit}\n          />\n        </div>\n        <div className=\"flex items-center gap-1.5\">\n          <span className=\"text-xs text-muted-foreground\">Weight</span>\n          <UnitDropdown\n            label=\"Weight unit\"\n            options={WEIGHT_UNIT_OPTIONS}\n            value={dimensions.weightUnit}\n            onChange={setWeightUnit}\n          />\n        </div>\n      </div>\n      <div className=\"flex flex-col gap-2\">\n        <DimensionRow\n          label=\"Length\"\n          range={dimensions.length}\n          onChange={(next) => setRange(\"length\", next)}\n        />\n        <DimensionRow\n          label=\"Width\"\n          range={dimensions.width}\n          onChange={(next) => setRange(\"width\", next)}\n        />\n        <DimensionRow\n          label=\"Height\"\n          range={dimensions.height}\n          onChange={(next) => setRange(\"height\", next)}\n        />\n        <DimensionRow\n          label=\"Weight\"\n          range={dimensions.weight}\n          onChange={(next) => setRange(\"weight\", next)}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction DimensionsField() {\n  return (\n    <Field label=\"Dimensions\">\n      <DimensionsControl />\n    </Field>\n  );\n}\n\nfunction ColorsControl() {\n  const { filters, update, colorPercentages } = useProductFilters(\"ProductFiltersColors\");\n  const [draft, setDraft] = React.useState(\"#3b82f6\");\n\n  const addColor = (raw: string) => {\n    const hex = normalizeHex(raw);\n    if (!hex || filters.colors.some((color) => color.hex === hex)) {\n      return;\n    }\n    update((current) => ({ colors: [...current.colors, { hex }] }));\n    setDraft(hex);\n  };\n\n  const removeColor = (hex: string) =>\n    update((current) => ({ colors: current.colors.filter((color) => color.hex !== hex) }));\n\n  const setPercentage = (hex: string, percentage: number | null) =>\n    update((current) => ({ colors: setColorPercentage(current.colors, hex, percentage) }));\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"flex items-center gap-2\">\n        <Popover>\n          <PopoverTrigger\n            type=\"button\"\n            aria-label=\"Open color picker\"\n            className=\"group relative size-8 shrink-0 overflow-hidden rounded-md border ring-offset-background transition-all hover:scale-105 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n          >\n            <span className=\"absolute inset-0\" style={{ backgroundColor: draft }} />\n            <span className=\"absolute inset-0 flex items-center justify-center bg-black/0 text-white opacity-0 transition-opacity group-hover:bg-black/20 group-hover:opacity-100\">\n              <Pipette className=\"size-4 drop-shadow\" />\n            </span>\n          </PopoverTrigger>\n          <PopoverContent align=\"start\" className=\"w-auto p-3\">\n            <div className=\"flex flex-col gap-3\">\n              <HexColorPicker color={draft} onChange={setDraft} />\n              <Input\n                value={draft}\n                onChange={(event) => setDraft(event.target.value)}\n                onKeyDown={(event) => {\n                  if (event.key === \"Enter\") {\n                    event.preventDefault();\n                    addColor(draft);\n                  }\n                }}\n                spellCheck={false}\n                aria-label=\"Hex color\"\n                className=\"h-8 font-mono\"\n              />\n            </div>\n          </PopoverContent>\n        </Popover>\n        <Input\n          value={draft}\n          onChange={(event) => setDraft(event.target.value)}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\") {\n              event.preventDefault();\n              addColor(draft);\n            }\n          }}\n          spellCheck={false}\n          aria-label=\"Hex color\"\n          className=\"h-8 w-28 font-mono\"\n        />\n        <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => addColor(draft)}>\n          Add\n        </Button>\n      </div>\n\n      {filters.colors.length > 0 ? (\n        <div className=\"flex flex-col gap-2\">\n          {filters.colors.map((color) => (\n            <div key={color.hex} className=\"flex items-center gap-2\">\n              <span\n                className=\"size-5 shrink-0 rounded-full border\"\n                style={{ backgroundColor: color.hex }}\n              />\n              <span className=\"font-mono text-xs\">{color.hex}</span>\n              {colorPercentages ? (\n                <div className=\"flex flex-1 items-center gap-2\">\n                  <Slider\n                    min={0}\n                    max={100}\n                    step={5}\n                    value={[Math.round((color.percentage ?? 0) * 100)]}\n                    onValueChange={(value) => {\n                      const next = sliderThumb(value, 0) ?? 0;\n                      setPercentage(color.hex, next <= 0 ? null : next / 100);\n                    }}\n                    aria-label={`Target share for ${color.hex}`}\n                    className=\"flex-1\"\n                  />\n                  <span className=\"w-9 text-right text-xs tabular-nums text-muted-foreground\">\n                    {color.percentage != null ? `${Math.round(color.percentage * 100)}%` : \"—\"}\n                  </span>\n                </div>\n              ) : (\n                <span className=\"flex-1\" />\n              )}\n              <button\n                type=\"button\"\n                onClick={() => removeColor(color.hex)}\n                aria-label={`Remove ${color.hex}`}\n                className=\"flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n              >\n                <X className=\"size-3.5\" />\n              </button>\n            </div>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Colors() {\n  return (\n    <Field label=\"Color\">\n      <ColorsControl />\n    </Field>\n  );\n}\n\ninterface TypeaheadOptionProps<T> {\n  placeholder: string;\n  fetcher: (query: string) => Promise<T[]>;\n  getKey: (option: T) => string;\n  renderOption: (option: T) => React.ReactNode;\n  onPick: (option: T) => void;\n}\n\nfunction OptionList<T>({\n  query,\n  options,\n  isLoading,\n  getKey,\n  renderOption,\n  onPick,\n}: {\n  query: string;\n  options: T[];\n  isLoading: boolean;\n  getKey: (option: T) => string;\n  renderOption: (option: T) => React.ReactNode;\n  onPick: (option: T) => void;\n}) {\n  return (\n    <ul className={cn(\"max-h-60 overflow-auto p-1\", THIN_SCROLLBAR_CLASS)}>\n      {isLoading ? (\n        <li className=\"px-2 py-1.5 text-sm text-muted-foreground\">Searching…</li>\n      ) : null}\n      {!isLoading && options.length === 0 && query.trim() ? (\n        <li className=\"px-2 py-1.5 text-sm text-muted-foreground\">No matches</li>\n      ) : null}\n      {options.map((option) => (\n        <li key={getKey(option)}>\n          <button\n            type=\"button\"\n            onClick={() => onPick(option)}\n            className=\"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent\"\n          >\n            {renderOption(option)}\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction InlineTypeahead<T>({\n  placeholder,\n  fetcher,\n  getKey,\n  renderOption,\n  onPick,\n  autoFocus = false,\n}: TypeaheadOptionProps<T> & { autoFocus?: boolean }) {\n  const { query, setQuery, options, isLoading } = useAsyncOptions<T>({ fetch: fetcher });\n  return (\n    <div className=\"flex flex-col\">\n      <Input\n        autoFocus={autoFocus}\n        value={query}\n        onChange={(event) => setQuery(event.target.value)}\n        placeholder={placeholder}\n        className=\"h-8\"\n      />\n      {query.trim() ? (\n        <OptionList\n          query={query}\n          options={options}\n          isLoading={isLoading}\n          getKey={getKey}\n          renderOption={renderOption}\n          onPick={(option) => {\n            onPick(option);\n            setQuery(\"\");\n          }}\n        />\n      ) : null}\n    </div>\n  );\n}\n\nfunction Typeahead<T>({\n  triggerLabel,\n  ...rest\n}: TypeaheadOptionProps<T> & { triggerLabel: string }) {\n  const [open, setOpen] = React.useState(false);\n  const { query, setQuery, options, isLoading } = useAsyncOptions<T>({ fetch: rest.fetcher });\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger\n        type=\"button\"\n        className={cn(buttonVariants({ variant: \"outline\", size: \"sm\" }), \"w-fit gap-1.5\")}\n      >\n        <Plus className=\"size-4\" />\n        {triggerLabel}\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-72 p-0\">\n        <div className=\"border-b p-2\">\n          <Input\n            autoFocus\n            value={query}\n            onChange={(event) => setQuery(event.target.value)}\n            placeholder={rest.placeholder}\n            className=\"h-8\"\n          />\n        </div>\n        <OptionList\n          query={query}\n          options={options}\n          isLoading={isLoading}\n          getKey={rest.getKey}\n          renderOption={rest.renderOption}\n          onPick={(option) => {\n            rest.onPick(option);\n            setOpen(false);\n            setQuery(\"\");\n          }}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction BrandsControl({ inline = false, autoFocus = false }: { inline?: boolean; autoFocus?: boolean }) {\n  const { filters, update, searchBrands } = useProductFilters(\"ProductFiltersBrands\");\n  if (!searchBrands) {\n    return null;\n  }\n\n  const add = (brand: Brand) =>\n    update((current) =>\n      current.brands.some((existing) => existing.id === brand.id)\n        ? {}\n        : { brands: [...current.brands, brand] },\n    );\n  const remove = (id: string) =>\n    update((current) => ({ brands: current.brands.filter((brand) => brand.id !== id) }));\n\n  const renderOption = (brand: Brand) => (\n    <>\n      {brand.logo_url ? (\n        <img src={brand.logo_url} alt=\"\" className=\"size-5 rounded object-contain\" />\n      ) : null}\n      <span>{brand.name}</span>\n    </>\n  );\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {inline ? (\n        <InlineTypeahead<Brand>\n          placeholder=\"Search brands\"\n          fetcher={searchBrands}\n          getKey={(brand) => brand.id}\n          onPick={add}\n          renderOption={renderOption}\n          autoFocus={autoFocus}\n        />\n      ) : (\n        <Typeahead<Brand>\n          triggerLabel=\"Add brand\"\n          placeholder=\"Search brands\"\n          fetcher={searchBrands}\n          getKey={(brand) => brand.id}\n          onPick={add}\n          renderOption={renderOption}\n        />\n      )}\n      {filters.brands.length > 0 ? (\n        <div className=\"flex flex-wrap gap-1.5\">\n          {filters.brands.map((brand) => (\n            <Chip key={brand.id} onRemove={() => remove(brand.id)}>\n              {brand.logo_url ? (\n                <img\n                  src={brand.logo_url}\n                  alt=\"\"\n                  className=\"size-4 shrink-0 rounded object-contain\"\n                />\n              ) : null}\n              <span className=\"truncate\">{brand.name}</span>\n            </Chip>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Brands() {\n  const { searchBrands } = useProductFilters(\"ProductFiltersBrands\");\n  if (!searchBrands) {\n    return null;\n  }\n  return (\n    <Field label=\"Brands\">\n      <BrandsControl />\n    </Field>\n  );\n}\n\nfunction websiteLabel(website: Website): string {\n  return website.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\");\n}\n\nfunction WebsitesControl({ inline = false, autoFocus = false }: { inline?: boolean; autoFocus?: boolean }) {\n  const { filters, update, searchWebsites } = useProductFilters(\"ProductFiltersWebsites\");\n  if (!searchWebsites) {\n    return null;\n  }\n\n  const add = (website: Website) =>\n    update((current) =>\n      current.websites.some((existing) => existing.id === website.id)\n        ? {}\n        : { websites: [...current.websites, website] },\n    );\n  const remove = (id: string) =>\n    update((current) => ({ websites: current.websites.filter((website) => website.id !== id) }));\n\n  const renderOption = (website: Website) => <span>{websiteLabel(website)}</span>;\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {inline ? (\n        <InlineTypeahead<Website>\n          placeholder=\"Search websites\"\n          fetcher={searchWebsites}\n          getKey={(website) => website.id}\n          onPick={add}\n          renderOption={renderOption}\n          autoFocus={autoFocus}\n        />\n      ) : (\n        <Typeahead<Website>\n          triggerLabel=\"Add website\"\n          placeholder=\"Search websites\"\n          fetcher={searchWebsites}\n          getKey={(website) => website.id}\n          onPick={add}\n          renderOption={renderOption}\n        />\n      )}\n      {filters.websites.length > 0 ? (\n        <div className=\"flex flex-wrap gap-1.5\">\n          {filters.websites.map((website) => (\n            <Chip key={website.id} onRemove={() => remove(website.id)}>\n              {websiteLabel(website)}\n            </Chip>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Websites() {\n  const { searchWebsites } = useProductFilters(\"ProductFiltersWebsites\");\n  if (!searchWebsites) {\n    return null;\n  }\n  return (\n    <Field label=\"Websites\">\n      <WebsitesControl />\n    </Field>\n  );\n}\n\nfunction CategoryControl({ inline = false, autoFocus = false }: { inline?: boolean; autoFocus?: boolean }) {\n  const { filters, update, searchCategories, getCategory } = useProductFilters(\n    \"ProductFiltersCategory\",\n  );\n\n  if (!searchCategories) {\n    return null;\n  }\n\n  const add = async (category: CategorySummary) => {\n    if (filters.categories.some((existing) => existing.slug === category.slug)) {\n      return;\n    }\n    update((current) => {\n      const categories = [...current.categories, category];\n      return {\n        categories,\n        ...deriveAttributes(categories, current.attributesByCategory, current.attributes),\n      };\n    });\n\n    if (getCategory && !(category.slug in filters.attributesByCategory)) {\n      let attributes: CategoryAttribute[] = [];\n      try {\n        const full = await getCategory(category.slug);\n        attributes = full.attributes ?? [];\n      } catch {\n        attributes = [];\n      }\n      update((current) => {\n        const byCategory = { ...current.attributesByCategory, [category.slug]: attributes };\n        return deriveAttributes(current.categories, byCategory, current.attributes);\n      });\n    }\n  };\n\n  const remove = (slug: string) =>\n    update((current) => {\n      const categories = current.categories.filter((category) => category.slug !== slug);\n      const byCategory = { ...current.attributesByCategory };\n      delete byCategory[slug];\n      return { categories, ...deriveAttributes(categories, byCategory, current.attributes) };\n    });\n\n  const renderOption = (category: CategorySummary) => (\n    <div className=\"flex flex-col\">\n      <span>{category.title}</span>\n      {category.path && category.path.length > 1 ? (\n        <span className=\"text-xs text-muted-foreground\">\n          {category.path.map((ref) => ref.title).join(\" › \")}\n        </span>\n      ) : null}\n    </div>\n  );\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {inline ? (\n        <InlineTypeahead<CategorySummary>\n          placeholder=\"Search categories\"\n          fetcher={searchCategories}\n          getKey={(category) => category.slug}\n          onPick={add}\n          renderOption={renderOption}\n          autoFocus={autoFocus}\n        />\n      ) : (\n        <Typeahead<CategorySummary>\n          triggerLabel=\"Add category\"\n          placeholder=\"Search categories\"\n          fetcher={searchCategories}\n          getKey={(category) => category.slug}\n          onPick={add}\n          renderOption={renderOption}\n        />\n      )}\n      {filters.categories.length > 0 ? (\n        <div className=\"flex flex-wrap gap-1.5\">\n          {filters.categories.map((category) => (\n            <Chip key={category.slug} onRemove={() => remove(category.slug)}>\n              {category.title}\n            </Chip>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction CategoryField() {\n  const { searchCategories } = useProductFilters(\"ProductFiltersCategory\");\n  if (!searchCategories) {\n    return null;\n  }\n  return (\n    <Field label=\"Category\">\n      <CategoryControl />\n    </Field>\n  );\n}\n\nfunction AttributeField({\n  attribute,\n  showLabel = true,\n}: {\n  attribute: CategoryAttribute;\n  showLabel?: boolean;\n}) {\n  const { filters, update } = useProductFilters(\"ProductFiltersAttributes\");\n  const control = (\n    <ToggleFacet\n      type=\"multiple\"\n      options={(attribute.values ?? []).map((option) => ({ value: option, label: option }))}\n      value={filters.attributes[attribute.slug] ?? []}\n      onChange={(value) =>\n        update((current) => ({\n          attributes: setAttributeValues(current.attributes, attribute.slug, value),\n        }))\n      }\n    />\n  );\n\n  if (!showLabel) {\n    return (\n      <div className=\"flex flex-col gap-1.5\">\n        <span className=\"text-xs font-medium text-muted-foreground\">{attribute.name}</span>\n        {control}\n      </div>\n    );\n  }\n\n  return <Field label={attribute.name}>{control}</Field>;\n}\n\nfunction CategoryAttributeFields({ attributes }: { attributes: CategoryAttribute[] }) {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {attributes.map((attribute) => (\n        <AttributeField key={attribute.slug} attribute={attribute} />\n      ))}\n    </div>\n  );\n}\n\nfunction Attributes() {\n  const { filters } = useProductFilters(\"ProductFiltersAttributes\");\n  const groups = categoryAttributeGroups(filters);\n  if (groups.length === 0) {\n    return null;\n  }\n\n  return (\n    <>\n      {groups.map(({ category, attributes }) => (\n        <Field key={category.slug} label={category.title}>\n          <CategoryAttributeFields attributes={attributes} />\n        </Field>\n      ))}\n    </>\n  );\n}\n\nfunction ActiveSummary({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { filters, update } = useProductFilters(\"ProductFiltersActiveSummary\");\n  const count = countActiveFilters(filters);\n  return (\n    <div\n      className={cn(\"flex h-8 items-center justify-between\", className)}\n      {...rest}\n    >\n      <span className=\"text-sm font-medium\">\n        Filters\n        {count > 0 ? <span className=\"ml-1.5 text-muted-foreground\">({count})</span> : null}\n      </span>\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        onClick={() => update(() => EMPTY_FILTERS)}\n        className={cn(!count && \"pointer-events-none invisible\")}\n        aria-hidden={count === 0}\n        tabIndex={count === 0 ? -1 : undefined}\n      >\n        Clear all\n      </Button>\n    </div>\n  );\n}\n\nfunction FacetPopover({\n  label,\n  count,\n  contentClassName,\n  children,\n}: {\n  label: string;\n  count: number;\n  contentClassName?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <Popover>\n      <PopoverTrigger\n        type=\"button\"\n        className={cn(\n          buttonVariants({ variant: \"outline\", size: \"sm\" }),\n          \"gap-1.5\",\n          count > 0 && \"border-foreground/40\",\n        )}\n      >\n        {label}\n        {count > 0 ? (\n          <Badge variant=\"secondary\" className=\"size-5 justify-center rounded-full px-0 tabular-nums\">\n            {count}\n          </Badge>\n        ) : null}\n        <ChevronDown className=\"size-3.5 text-muted-foreground\" aria-hidden />\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className={cn(\"w-auto max-w-sm p-3\", contentClassName)}\n      >\n        {children}\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport interface ProductFiltersBarProps extends React.ComponentProps<\"div\"> {\n  priceMax?: number;\n  priceStep?: number;\n}\n\nfunction Bar({ priceMax, priceStep, className, ...rest }: ProductFiltersBarProps) {\n  const { filters, update, searchBrands, searchWebsites, searchCategories } = useProductFilters(\"ProductFiltersBar\");\n  const counts = facetCounts(filters);\n  const total = countActiveFilters(filters);\n  const attributeGroups = categoryAttributeGroups(filters);\n\n  return (\n    <div\n      data-slot=\"product-filters-bar\"\n      className={cn(\"flex flex-wrap items-center justify-center gap-2\", className)}\n      {...rest}\n    >\n      <FacetPopover label=\"Price\" count={counts.price} contentClassName=\"w-64\">\n        <PriceControl max={priceMax} step={priceStep} />\n      </FacetPopover>\n      <FacetPopover label=\"Gender\" count={counts.gender}>\n        <GenderControl />\n      </FacetPopover>\n      <FacetPopover label=\"Age\" count={counts.age}>\n        <AgeControl />\n      </FacetPopover>\n      <FacetPopover label=\"Condition\" count={counts.condition}>\n        <ConditionControl />\n      </FacetPopover>\n      <FacetPopover label=\"Availability\" count={counts.availability}>\n        <AvailabilityControl />\n      </FacetPopover>\n      <FacetPopover label=\"Dimensions\" count={counts.dimensions} contentClassName=\"w-72\">\n        <DimensionsControl />\n      </FacetPopover>\n      <FacetPopover label=\"Color\" count={counts.colors} contentClassName=\"min-w-[14rem]\">\n        <ColorsControl />\n      </FacetPopover>\n      {searchBrands ? (\n        <FacetPopover label=\"Brands\" count={counts.brands} contentClassName=\"min-w-[14rem]\">\n          <BrandsControl inline autoFocus />\n        </FacetPopover>\n      ) : null}\n      {searchWebsites ? (\n        <FacetPopover label=\"Websites\" count={counts.websites} contentClassName=\"min-w-[14rem]\">\n          <WebsitesControl inline autoFocus />\n        </FacetPopover>\n      ) : null}\n      {searchCategories ? (\n        <FacetPopover label=\"Category\" count={counts.categories} contentClassName=\"min-w-[14rem]\">\n          <CategoryControl inline autoFocus />\n        </FacetPopover>\n      ) : null}\n      {attributeGroups.map(({ category, attributes }) => (\n        <FacetPopover\n          key={category.slug}\n          label={category.title}\n          count={countCategoryAttributes(filters, attributes)}\n        >\n          <div className=\"flex flex-col gap-3\">\n            {attributes.map((attribute) => (\n              <AttributeField key={attribute.slug} attribute={attribute} showLabel={false} />\n            ))}\n          </div>\n        </FacetPopover>\n      ))}\n      {total > 0 ? (\n        <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => update(() => EMPTY_FILTERS)}>\n          Clear all\n        </Button>\n      ) : null}\n    </div>\n  );\n}\n\nfunction FacetSection({\n  value,\n  label,\n  count,\n  children,\n}: {\n  value: string;\n  label: React.ReactNode;\n  count: number;\n  children: React.ReactNode;\n}) {\n  return (\n    <AccordionItem value={value}>\n      <AccordionTrigger>\n        <span className=\"flex items-center gap-1.5\">\n          {label}\n          {count > 0 ? (\n            <Badge\n              variant=\"secondary\"\n              className=\"size-5 justify-center rounded-full px-0 tabular-nums\"\n            >\n              {count}\n            </Badge>\n          ) : null}\n        </span>\n      </AccordionTrigger>\n      {/* Slight inset so focus rings/shadows aren't clipped by the content's\n          overflow-hidden (needed for the open/close height animation). */}\n      <AccordionContent className=\"px-1 pt-1\">{children}</AccordionContent>\n    </AccordionItem>\n  );\n}\n\nfunction DefaultLayout() {\n  const { filters, searchBrands, searchWebsites, searchCategories } = useProductFilters(\"ProductFilters\");\n  const counts = facetCounts(filters);\n  const attributeGroups = categoryAttributeGroups(filters);\n\n  // Seed from the initial filters only — later edits must not reopen sections.\n  const [defaultOpen] = React.useState<string[]>(() => {\n    const initial = facetCounts(filters);\n    const entries: Array<[string, number]> = [\n      [\"price\", initial.price],\n      [\"gender\", initial.gender],\n      [\"age\", initial.age],\n      [\"condition\", initial.condition],\n      [\"availability\", initial.availability],\n      [\"dimensions\", initial.dimensions],\n      [\"colors\", initial.colors],\n      [\"brands\", initial.brands],\n      [\"websites\", initial.websites],\n      [\"categories\", initial.categories],\n    ];\n    const open = entries.filter(([, count]) => count > 0).map(([key]) => key);\n    for (const { category } of categoryAttributeGroups(filters)) {\n      open.push(`attr-${category.slug}`);\n    }\n    return open;\n  });\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <ActiveSummary />\n      <Accordion type=\"multiple\" defaultValue={defaultOpen} className=\"w-full\">\n        <FacetSection value=\"price\" label=\"Price\" count={counts.price}>\n          <PriceControl />\n        </FacetSection>\n        <FacetSection value=\"gender\" label=\"Gender\" count={counts.gender}>\n          <GenderControl />\n        </FacetSection>\n        <FacetSection value=\"age\" label=\"Age\" count={counts.age}>\n          <AgeControl />\n        </FacetSection>\n        <FacetSection value=\"condition\" label=\"Condition\" count={counts.condition}>\n          <ConditionControl />\n        </FacetSection>\n        <FacetSection value=\"availability\" label=\"Availability\" count={counts.availability}>\n          <AvailabilityControl />\n        </FacetSection>\n        <FacetSection value=\"dimensions\" label=\"Dimensions\" count={counts.dimensions}>\n          <DimensionsControl />\n        </FacetSection>\n        <FacetSection value=\"colors\" label=\"Color\" count={counts.colors}>\n          <ColorsControl />\n        </FacetSection>\n        {searchBrands ? (\n          <FacetSection value=\"brands\" label=\"Brands\" count={counts.brands}>\n            <BrandsControl inline />\n          </FacetSection>\n        ) : null}\n        {searchWebsites ? (\n          <FacetSection value=\"websites\" label=\"Websites\" count={counts.websites}>\n            <WebsitesControl inline />\n          </FacetSection>\n        ) : null}\n        {searchCategories ? (\n          <FacetSection value=\"categories\" label=\"Category\" count={counts.categories}>\n            <CategoryControl inline />\n          </FacetSection>\n        ) : null}\n        {attributeGroups.map(({ category, attributes }) => (\n          <FacetSection\n            key={category.slug}\n            value={`attr-${category.slug}`}\n            label={category.title}\n            count={countCategoryAttributes(filters, attributes)}\n          >\n            <div className=\"flex flex-col gap-3\">\n              {attributes.map((attribute) => (\n                <AttributeField key={attribute.slug} attribute={attribute} showLabel={false} />\n              ))}\n            </div>\n          </FacetSection>\n        ))}\n      </Accordion>\n    </div>\n  );\n}\n\nexport function ProductFilters({ className, ...props }: ProductFiltersProps) {\n  return (\n    <Root className={cn(\"w-full\", className)} {...props}>\n      <DefaultLayout />\n    </Root>\n  );\n}\n\nexport {\n  Root as ProductFiltersRoot,\n  Bar as ProductFiltersBar,\n  ActiveSummary as ProductFiltersActiveSummary,\n  Price as ProductFiltersPrice,\n  Gender as ProductFiltersGender,\n  Age as ProductFiltersAge,\n  Condition as ProductFiltersCondition,\n  Availability as ProductFiltersAvailability,\n  DimensionsField as ProductFiltersDimensions,\n  Colors as ProductFiltersColors,\n  Brands as ProductFiltersBrands,\n  Websites as ProductFiltersWebsites,\n  CategoryField as ProductFiltersCategory,\n  Attributes as ProductFiltersAttributes,\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}
