{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-panel",
  "title": "Filter Panel",
  "author": "ilinxa",
  "description": "Schema-driven filter panel — checkbox lists, toggles, text, and custom filter types with AND composition and debounced input.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "checkbox",
    "input",
    "switch",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/components/forms/filter-panel/filter-panel.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n} from \"react\";\nimport type {\n  FilterPanelHandle,\n  FilterPanelProps,\n  FilterValue,\n} from \"./types\";\nimport { isCategoryEmpty } from \"./lib/default-is-empty\";\nimport { validateCategorySchema } from \"./lib/validate-schema\";\nimport { useFilteredItems } from \"./hooks/use-filtered-items\";\nimport { useTextBuffer } from \"./hooks/use-text-buffer\";\nimport { FilterSection } from \"./parts/filter-section\";\nimport { FilterPanelFooter } from \"./parts/filter-panel-footer\";\nimport { cn } from \"@/lib/utils\";\n\nconst SCHEMA_INSTABILITY_THRESHOLD = 5;\n\nexport function FilterPanel<T = unknown>(props: FilterPanelProps<T>) {\n  const {\n    items,\n    categories,\n    values,\n    onChange,\n    onFilteredChange,\n    showClearAll = true,\n    clearAllLabel = \"Clear all\",\n    ariaLabel,\n    className,\n    ref,\n  } = props;\n\n  const valuesRef = useRef(values);\n  const categoriesRef = useRef(categories);\n  const onChangeRef = useRef(onChange);\n\n  useEffect(() => {\n    valuesRef.current = values;\n    categoriesRef.current = categories;\n    onChangeRef.current = onChange;\n  });\n\n  useEffect(() => {\n    validateCategorySchema(\n      categories as ReadonlyArray<import(\"./types\").FilterCategory<unknown>>,\n    );\n  }, [categories]);\n\n  const schemaInstabilityRef = useRef({\n    last: categories,\n    count: 0,\n    warned: false,\n  });\n  useEffect(() => {\n    const tracker = schemaInstabilityRef.current;\n    if (tracker.last === categories) {\n      tracker.count = 0;\n      return;\n    }\n    tracker.last = categories;\n    tracker.count++;\n    if (\n      tracker.count > SCHEMA_INSTABILITY_THRESHOLD &&\n      !tracker.warned &&\n      process.env.NODE_ENV !== \"production\"\n    ) {\n      tracker.warned = true;\n      console.warn(\n        \"[filter-panel] `categories` prop is changing every render. \" +\n          \"Wrap with `useMemo` or hoist to module scope to avoid re-computation.\",\n      );\n    }\n  }, [categories]);\n\n  const commitText = useCallback((id: string, value: string) => {\n    const current = valuesRef.current;\n    onChangeRef.current({ ...current, [id]: value });\n  }, []);\n\n  const textBuffer = useTextBuffer(commitText);\n\n  useFilteredItems(items, categories, values, onFilteredChange);\n\n  const isEmpty = useCallback((): boolean => {\n    for (const cat of categoriesRef.current) {\n      if (!isCategoryEmpty(cat, valuesRef.current[cat.id])) return false;\n    }\n    return true;\n  }, []);\n\n  const clear = useCallback(\n    (categoryId: string) => {\n      const cat = categoriesRef.current.find((c) => c.id === categoryId);\n      if (!cat) {\n        if (process.env.NODE_ENV !== \"production\") {\n          console.warn(\n            `[filter-panel] clear(\"${categoryId}\") — unknown category id; no-op.`,\n          );\n        }\n        return;\n      }\n      if (cat.type === \"text\") textBuffer.cancel(categoryId);\n      const next = { ...valuesRef.current };\n      delete next[categoryId];\n      if (cat.type === \"checkbox-list\" && cat.modeToggle) {\n        delete next[`${categoryId}__mode`];\n      }\n      onChangeRef.current(next);\n    },\n    [textBuffer],\n  );\n\n  const clearAll = useCallback(() => {\n    textBuffer.cancelAll();\n    const current = valuesRef.current;\n    const next: Record<string, FilterValue> = {};\n    for (const key of Object.keys(current)) {\n      if (key.endsWith(\"__mode\")) next[key] = current[key];\n    }\n    onChangeRef.current(next);\n  }, [textBuffer]);\n\n  useImperativeHandle(\n    ref,\n    (): FilterPanelHandle => ({ clearAll, clear, isEmpty }),\n    [clearAll, clear, isEmpty],\n  );\n\n  let allEmpty = true;\n  for (const cat of categories) {\n    if (!isCategoryEmpty(cat, values[cat.id])) {\n      allEmpty = false;\n      break;\n    }\n  }\n\n  return (\n    <form\n      noValidate\n      role=\"region\"\n      aria-label={ariaLabel}\n      className={cn(\"flex flex-col gap-3\", className)}\n      onSubmit={(e) => e.preventDefault()}\n    >\n      <div className=\"flex flex-col gap-3\">\n        {categories.map((cat) => (\n          <FilterSection<T>\n            key={cat.id}\n            category={cat}\n            values={values}\n            onChange={onChange}\n            items={items}\n            textBuffer={textBuffer}\n          />\n        ))}\n      </div>\n      {showClearAll ? (\n        <FilterPanelFooter\n          onClearAll={clearAll}\n          disabled={allEmpty}\n          label={clearAllLabel}\n        />\n      ) : null}\n    </form>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/filter-panel.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/index.ts",
      "content": "export { FilterPanel } from \"./filter-panel\";\nexport { defaultIsEmpty } from \"./lib/default-is-empty\";\nexport type {\n  CheckboxListFilter,\n  CustomFilter,\n  CustomFilterRenderProps,\n  FilterCategory,\n  FilterMode,\n  FilterOption,\n  FilterPanelHandle,\n  FilterPanelProps,\n  FilterValue,\n  TextFilter,\n  ToggleFilter,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/filter-panel/index.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/types.ts",
      "content": "import type { ReactNode, Ref } from \"react\";\n\nexport type FilterValue = unknown;\nexport type FilterMode = \"union\" | \"intersection\";\n\nexport interface FilterOption {\n  value: string;\n  label: string;\n}\n\ninterface BaseFilterCategory<T> {\n  id: string;\n  label: string;\n  description?: string;\n  predicate: (item: T, value: FilterValue) => boolean;\n}\n\nexport interface CheckboxListFilter<T> extends BaseFilterCategory<T> {\n  type: \"checkbox-list\";\n  options: ReadonlyArray<FilterOption>;\n  modeToggle?: boolean;\n  defaultMode?: FilterMode;\n  showSoloButtons?: boolean;\n  isEmpty?: (value: FilterValue) => boolean;\n}\n\nexport interface ToggleFilter<T> extends BaseFilterCategory<T> {\n  type: \"toggle\";\n  isEmpty: (value: FilterValue) => boolean;\n}\n\nexport interface TextFilter<T> extends BaseFilterCategory<T> {\n  type: \"text\";\n  placeholder?: string;\n  debounceMs?: number;\n  isEmpty?: (value: FilterValue) => boolean;\n}\n\nexport interface CustomFilterRenderProps<T> {\n  value: unknown;\n  onChange: (value: unknown) => void;\n  items: ReadonlyArray<T>;\n  fieldId: string;\n}\n\nexport interface CustomFilter<T> extends BaseFilterCategory<T> {\n  type: \"custom\";\n  render: (props: CustomFilterRenderProps<T>) => ReactNode;\n  isEmpty: (value: FilterValue) => boolean;\n}\n\nexport type FilterCategory<T> =\n  | CheckboxListFilter<T>\n  | ToggleFilter<T>\n  | TextFilter<T>\n  | CustomFilter<T>;\n\nexport interface FilterPanelProps<T = unknown> {\n  items: ReadonlyArray<T>;\n  categories: ReadonlyArray<FilterCategory<T>>;\n\n  values: Record<string, FilterValue>;\n  onChange: (values: Record<string, FilterValue>) => void;\n\n  onFilteredChange?: (filtered: ReadonlyArray<T>) => void;\n\n  showClearAll?: boolean;\n  clearAllLabel?: string;\n\n  ariaLabel?: string;\n  className?: string;\n\n  ref?: Ref<FilterPanelHandle>;\n}\n\nexport interface FilterPanelHandle {\n  clearAll(): void;\n  clear(categoryId: string): void;\n  isEmpty(): boolean;\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/types.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/hooks/use-filtered-items.ts",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\nimport type { FilterCategory, FilterValue } from \"../types\";\nimport { applyFilters } from \"../lib/apply-filters\";\n\nexport function useFilteredItems<T>(\n  items: ReadonlyArray<T>,\n  categories: ReadonlyArray<FilterCategory<T>>,\n  values: Record<string, FilterValue>,\n  onFilteredChange?: (filtered: ReadonlyArray<T>) => void,\n): ReadonlyArray<T> {\n  const filtered = useMemo(\n    () => applyFilters(items, categories, values),\n    [items, categories, values],\n  );\n\n  const onFilteredChangeRef = useRef(onFilteredChange);\n  useEffect(() => {\n    onFilteredChangeRef.current = onFilteredChange;\n  });\n\n  useEffect(() => {\n    const cb = onFilteredChangeRef.current;\n    if (!cb) return;\n    try {\n      cb(filtered);\n    } catch (err) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.error(\"[filter-panel] onFilteredChange threw:\", err);\n      }\n    }\n  }, [filtered]);\n\n  return filtered;\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/hooks/use-filtered-items.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/hooks/use-text-buffer.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface TextBufferApi {\n  buffer: Record<string, string>;\n  setBuffer: (id: string, value: string, debounceMs: number) => void;\n  flush: (id: string) => void;\n  cancel: (id: string) => void;\n  flushAll: () => void;\n  cancelAll: () => void;\n}\n\ntype CommitText = (id: string, value: string) => void;\n\nexport function useTextBuffer(commitText: CommitText): TextBufferApi {\n  const [buffer, setBufferState] = useState<Record<string, string>>({});\n  const timersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});\n  const pendingRef = useRef<Record<string, string>>({});\n  const commitTextRef = useRef(commitText);\n\n  useEffect(() => {\n    commitTextRef.current = commitText;\n  });\n\n  useEffect(() => {\n    const timers = timersRef.current;\n    return () => {\n      for (const id of Object.keys(timers)) clearTimeout(timers[id]);\n    };\n  }, []);\n\n  const clearBufferEntry = useCallback((id: string) => {\n    setBufferState((prev) => {\n      if (!(id in prev)) return prev;\n      const next = { ...prev };\n      delete next[id];\n      return next;\n    });\n  }, []);\n\n  const cancel = useCallback(\n    (id: string) => {\n      const timer = timersRef.current[id];\n      if (timer) {\n        clearTimeout(timer);\n        delete timersRef.current[id];\n      }\n      delete pendingRef.current[id];\n      clearBufferEntry(id);\n    },\n    [clearBufferEntry],\n  );\n\n  const flush = useCallback((id: string) => {\n    const timer = timersRef.current[id];\n    if (timer) {\n      clearTimeout(timer);\n      delete timersRef.current[id];\n    }\n    const value = pendingRef.current[id];\n    if (value !== undefined) {\n      delete pendingRef.current[id];\n      commitTextRef.current(id, value);\n    }\n  }, []);\n\n  const setBuffer = useCallback(\n    (id: string, value: string, debounceMs: number) => {\n      pendingRef.current[id] = value;\n      setBufferState((prev) => ({ ...prev, [id]: value }));\n      const existing = timersRef.current[id];\n      if (existing) clearTimeout(existing);\n      timersRef.current[id] = setTimeout(() => {\n        delete timersRef.current[id];\n        const pending = pendingRef.current[id];\n        if (pending === undefined) return;\n        delete pendingRef.current[id];\n        commitTextRef.current(id, pending);\n      }, debounceMs);\n    },\n    [],\n  );\n\n  const flushAll = useCallback(() => {\n    for (const id of Object.keys(timersRef.current)) flush(id);\n  }, [flush]);\n\n  const cancelAll = useCallback(() => {\n    for (const id of Object.keys(timersRef.current)) {\n      clearTimeout(timersRef.current[id]);\n      delete timersRef.current[id];\n    }\n    pendingRef.current = {};\n    setBufferState({});\n  }, []);\n\n  return { buffer, setBuffer, flush, cancel, flushAll, cancelAll };\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/hooks/use-text-buffer.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/lib/apply-filters.ts",
      "content": "import type { FilterCategory, FilterValue } from \"../types\";\nimport { isCategoryEmpty } from \"./default-is-empty\";\n\nexport function applyFilters<T>(\n  items: ReadonlyArray<T>,\n  categories: ReadonlyArray<FilterCategory<T>>,\n  values: Record<string, FilterValue>,\n): ReadonlyArray<T> {\n  const activePredicates: Array<(item: T) => boolean> = [];\n  for (const cat of categories) {\n    const value = values[cat.id];\n    if (isCategoryEmpty(cat, value)) continue;\n    activePredicates.push((item) => {\n      try {\n        return cat.predicate(item, value);\n      } catch (err) {\n        if (process.env.NODE_ENV !== \"production\") {\n          console.error(\n            `[filter-panel] predicate threw for category \"${cat.id}\":`,\n            err,\n          );\n        }\n        return false;\n      }\n    });\n  }\n  if (activePredicates.length === 0) return items;\n  return items.filter((item) => activePredicates.every((p) => p(item)));\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/lib/apply-filters.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/lib/default-is-empty.ts",
      "content": "import type { FilterCategory, FilterValue } from \"../types\";\n\nexport function defaultCheckboxListIsEmpty(value: FilterValue): boolean {\n  if (!Array.isArray(value)) return true;\n  return value.length === 0;\n}\n\nexport function defaultTextIsEmpty(value: FilterValue): boolean {\n  if (typeof value !== \"string\") return true;\n  return value.length === 0;\n}\n\nconst TRUE = (): boolean => false;\n\nexport function isCategoryEmpty<T>(\n  category: FilterCategory<T>,\n  value: FilterValue,\n): boolean {\n  try {\n    switch (category.type) {\n      case \"checkbox-list\":\n        return (category.isEmpty ?? defaultCheckboxListIsEmpty)(value);\n      case \"text\":\n        return (category.isEmpty ?? defaultTextIsEmpty)(value);\n      case \"toggle\":\n      case \"custom\":\n        return category.isEmpty(value);\n    }\n  } catch (err) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(\n        `[filter-panel] isEmpty threw for category \"${category.id}\":`,\n        err,\n      );\n    }\n    return TRUE();\n  }\n}\n\nexport const defaultIsEmpty = {\n  checkboxList: defaultCheckboxListIsEmpty,\n  text: defaultTextIsEmpty,\n};\n",
      "type": "registry:component",
      "target": "components/filter-panel/lib/default-is-empty.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/lib/validate-schema.ts",
      "content": "import type { FilterCategory } from \"../types\";\n\nexport const RESERVED_SUFFIXES = [\"__mode\"] as const;\n\nexport function validateCategorySchema(\n  categories: ReadonlyArray<FilterCategory<unknown>>,\n): void {\n  if (process.env.NODE_ENV === \"production\") return;\n\n  const seen = new Set<string>();\n  for (const cat of categories) {\n    for (const suffix of RESERVED_SUFFIXES) {\n      if (cat.id.endsWith(suffix)) {\n        console.error(\n          `[filter-panel] category id \"${cat.id}\" ends in reserved suffix \"${suffix}\". ` +\n            `Pick a different id; \"${suffix}\" is used internally for mode storage.`,\n        );\n      }\n    }\n    if (seen.has(cat.id)) {\n      console.error(`[filter-panel] duplicate category id \"${cat.id}\".`);\n    }\n    seen.add(cat.id);\n    if (\n      cat.type === \"checkbox-list\" &&\n      (!cat.options || cat.options.length === 0)\n    ) {\n      console.warn(\n        `[filter-panel] checkbox-list category \"${cat.id}\" has no options.`,\n      );\n    }\n    if (\n      cat.type === \"checkbox-list\" &&\n      cat.modeToggle === false &&\n      cat.defaultMode !== undefined\n    ) {\n      console.warn(\n        `[filter-panel] category \"${cat.id}\" has \\`defaultMode\\` but \\`modeToggle\\` is false; defaultMode will be ignored.`,\n      );\n    }\n  }\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/lib/validate-schema.ts"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/clear-button.tsx",
      "content": "\"use client\";\n\nimport { X } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ClearButtonProps {\n  onClick: () => void;\n  ariaLabel: string;\n  variant?: \"icon\" | \"text\";\n  label?: string;\n  disabled?: boolean;\n  className?: string;\n}\n\nexport function ClearButton({\n  onClick,\n  ariaLabel,\n  variant = \"icon\",\n  label = \"Clear all\",\n  disabled,\n  className,\n}: ClearButtonProps) {\n  if (variant === \"icon\") {\n    return (\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        onClick={onClick}\n        disabled={disabled}\n        aria-label={ariaLabel}\n        className={cn(\n          \"size-7 p-0 text-muted-foreground hover:text-foreground\",\n          className,\n        )}\n      >\n        <X aria-hidden=\"true\" className=\"size-3.5\" />\n      </Button>\n    );\n  }\n  return (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      size=\"sm\"\n      onClick={onClick}\n      disabled={disabled}\n      aria-label={ariaLabel}\n      aria-disabled={disabled || undefined}\n      className={className}\n    >\n      <X aria-hidden=\"true\" className=\"size-3\" />\n      {label}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/clear-button.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/filter-checkbox-list.tsx",
      "content": "\"use client\";\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport type {\n  CheckboxListFilter,\n  FilterMode,\n  FilterValue,\n} from \"../types\";\nimport { SoloButton } from \"./solo-button\";\n\ninterface FilterCheckboxListProps<T> {\n  category: CheckboxListFilter<T>;\n  values: Record<string, FilterValue>;\n  onChange: (values: Record<string, FilterValue>) => void;\n  fieldId: string;\n}\n\nfunction asStringArray(value: FilterValue): string[] {\n  if (!Array.isArray(value)) return [];\n  return value.filter((v): v is string => typeof v === \"string\");\n}\n\nexport function FilterCheckboxList<T>({\n  category,\n  values,\n  onChange,\n  fieldId,\n}: FilterCheckboxListProps<T>) {\n  const selected = asStringArray(values[category.id]);\n  const selectedSet = new Set(selected);\n  const showSolo = category.showSoloButtons === true;\n\n  const handleToggle = (optionValue: string, checked: boolean) => {\n    const next = checked\n      ? [...selected, optionValue]\n      : selected.filter((v) => v !== optionValue);\n    onChange({ ...values, [category.id]: next });\n  };\n\n  const handleSolo = (optionValue: string) => {\n    if (category.modeToggle) {\n      const existing = values[`${category.id}__mode`];\n      const fallbackMode: FilterMode = category.defaultMode ?? \"union\";\n      const mode: FilterMode =\n        existing === \"union\" || existing === \"intersection\"\n          ? existing\n          : fallbackMode;\n      onChange({\n        ...values,\n        [category.id]: [optionValue],\n        [`${category.id}__mode`]: mode,\n      });\n    } else {\n      onChange({ ...values, [category.id]: [optionValue] });\n    }\n  };\n\n  return (\n    <ul\n      role=\"group\"\n      aria-labelledby={fieldId}\n      className=\"flex flex-col gap-1\"\n    >\n      {category.options.map((opt) => {\n        const optionId = `${fieldId}-opt-${opt.value}`;\n        const checked = selectedSet.has(opt.value);\n        return (\n          <li\n            key={opt.value}\n            className=\"group flex items-center justify-between gap-2 rounded-md px-1 py-0.5 hover:bg-muted/40\"\n          >\n            <label\n              htmlFor={optionId}\n              className=\"flex flex-1 cursor-pointer items-center gap-2 text-sm\"\n            >\n              <Checkbox\n                id={optionId}\n                checked={checked}\n                onCheckedChange={(state) => handleToggle(opt.value, state === true)}\n              />\n              <span className=\"truncate text-foreground\">{opt.label}</span>\n            </label>\n            {showSolo ? (\n              <span className=\"opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100\">\n                <SoloButton\n                  optionLabel={opt.label}\n                  onClick={() => handleSolo(opt.value)}\n                />\n              </span>\n            ) : null}\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/filter-checkbox-list.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/filter-custom.tsx",
      "content": "\"use client\";\n\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport type { CustomFilter, FilterValue } from \"../types\";\n\ninterface FilterCustomProps<T> {\n  category: CustomFilter<T>;\n  values: Record<string, FilterValue>;\n  onChange: (values: Record<string, FilterValue>) => void;\n  items: ReadonlyArray<T>;\n  fieldId: string;\n}\n\nclass FilterErrorBoundary extends Component<\n  { categoryId: string; children: ReactNode },\n  { hasError: boolean }\n> {\n  override state = { hasError: false };\n  static getDerivedStateFromError() {\n    return { hasError: true };\n  }\n  override componentDidCatch(error: Error, info: ErrorInfo) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(\n        `[filter-panel] custom render crashed for category \"${this.props.categoryId}\":`,\n        error,\n        info,\n      );\n    }\n  }\n  override render() {\n    if (this.state.hasError) {\n      return (\n        <p role=\"alert\" className=\"text-xs text-destructive\">\n          Custom filter crashed — see console.\n        </p>\n      );\n    }\n    return this.props.children;\n  }\n}\n\nexport function FilterCustom<T>({\n  category,\n  values,\n  onChange,\n  items,\n  fieldId,\n}: FilterCustomProps<T>) {\n  return (\n    <FilterErrorBoundary categoryId={category.id}>\n      {category.render({\n        value: values[category.id],\n        onChange: (next) => onChange({ ...values, [category.id]: next }),\n        items,\n        fieldId,\n      })}\n    </FilterErrorBoundary>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/filter-custom.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/filter-section.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport type {\n  FilterCategory,\n  FilterMode,\n  FilterValue,\n} from \"../types\";\nimport { isCategoryEmpty } from \"../lib/default-is-empty\";\nimport { ClearButton } from \"./clear-button\";\nimport { ModeToggle } from \"./mode-toggle\";\nimport { FilterCheckboxList } from \"./filter-checkbox-list\";\nimport { FilterToggle } from \"./filter-toggle\";\nimport { FilterText } from \"./filter-text\";\nimport { FilterCustom } from \"./filter-custom\";\nimport type { TextBufferApi } from \"../hooks/use-text-buffer\";\nimport { defaultTextIsEmpty } from \"../lib/default-is-empty\";\n\ninterface FilterSectionProps<T> {\n  category: FilterCategory<T>;\n  values: Record<string, FilterValue>;\n  onChange: (values: Record<string, FilterValue>) => void;\n  items: ReadonlyArray<T>;\n  textBuffer: TextBufferApi;\n}\n\nfunction readMode<T>(\n  category: FilterCategory<T>,\n  values: Record<string, FilterValue>,\n): FilterMode {\n  if (category.type !== \"checkbox-list\" || !category.modeToggle) return \"union\";\n  const stored = values[`${category.id}__mode`];\n  if (stored === \"union\" || stored === \"intersection\") return stored;\n  return category.defaultMode ?? \"union\";\n}\n\nexport function FilterSection<T>({\n  category,\n  values,\n  onChange,\n  items,\n  textBuffer,\n}: FilterSectionProps<T>) {\n  const reactId = useId();\n  const fieldId = `${reactId}-${category.id}`;\n  const descriptionId = category.description ? `${fieldId}-desc` : undefined;\n  const empty = isCategoryEmpty(category, values[category.id]);\n  const showClear = !empty;\n\n  const handleClear = () => {\n    if (category.type === \"text\") textBuffer.cancel(category.id);\n    const next = { ...values };\n    delete next[category.id];\n    if (category.type === \"checkbox-list\" && category.modeToggle) {\n      delete next[`${category.id}__mode`];\n    }\n    onChange(next);\n  };\n\n  return (\n    <fieldset className=\"flex flex-col gap-2 rounded-md border border-border bg-card/40 p-3\">\n      <div className=\"flex flex-wrap items-center justify-between gap-2\">\n        <legend\n          id={fieldId}\n          className=\"px-1 text-sm font-medium text-foreground\"\n        >\n          {category.label}\n        </legend>\n        <div className=\"flex items-center gap-2\">\n          {category.type === \"checkbox-list\" && category.modeToggle ? (\n            <ModeToggle\n              value={readMode(category, values)}\n              ariaLabel={`${category.label} mode`}\n              onChange={(next) =>\n                onChange({ ...values, [`${category.id}__mode`]: next })\n              }\n            />\n          ) : null}\n          {showClear ? (\n            <ClearButton\n              onClick={handleClear}\n              ariaLabel={`Clear ${category.label}`}\n            />\n          ) : null}\n        </div>\n      </div>\n      {category.description ? (\n        <p id={descriptionId} className=\"px-1 text-xs text-muted-foreground\">\n          {category.description}\n        </p>\n      ) : null}\n      {(() => {\n        switch (category.type) {\n          case \"checkbox-list\":\n            return (\n              <FilterCheckboxList\n                category={category}\n                values={values}\n                onChange={onChange}\n                fieldId={fieldId}\n              />\n            );\n          case \"toggle\":\n            return (\n              <FilterToggle\n                category={category}\n                values={values}\n                onChange={onChange}\n                fieldId={fieldId}\n              />\n            );\n          case \"text\": {\n            const bufferValue = textBuffer.buffer[category.id];\n            const debounceMs = category.debounceMs ?? 250;\n            return (\n              <FilterText\n                category={category}\n                values={values}\n                onChange={onChange}\n                fieldId={fieldId}\n                descriptionId={descriptionId}\n                bufferValue={bufferValue}\n                onBufferChange={(v) =>\n                  textBuffer.setBuffer(category.id, v, debounceMs)\n                }\n                onFlush={() => {\n                  textBuffer.flush(category.id);\n                  if (\n                    bufferValue !== undefined &&\n                    defaultTextIsEmpty(bufferValue) &&\n                    typeof values[category.id] === \"string\" &&\n                    !defaultTextIsEmpty(values[category.id])\n                  ) {\n                    onChange({ ...values, [category.id]: \"\" });\n                  }\n                }}\n                onCancel={() => textBuffer.cancel(category.id)}\n              />\n            );\n          }\n          case \"custom\":\n            return (\n              <FilterCustom\n                category={category}\n                values={values}\n                onChange={onChange}\n                items={items}\n                fieldId={fieldId}\n              />\n            );\n        }\n      })()}\n    </fieldset>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/filter-section.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/filter-panel-footer.tsx",
      "content": "\"use client\";\n\nimport { ClearButton } from \"./clear-button\";\n\ninterface FilterPanelFooterProps {\n  onClearAll: () => void;\n  disabled: boolean;\n  label: string;\n}\n\nexport function FilterPanelFooter({\n  onClearAll,\n  disabled,\n  label,\n}: FilterPanelFooterProps) {\n  return (\n    <div role=\"toolbar\" className=\"flex justify-end pt-1\">\n      <ClearButton\n        onClick={onClearAll}\n        ariaLabel=\"Clear all filters\"\n        variant=\"text\"\n        label={label}\n        disabled={disabled}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/filter-panel-footer.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/filter-text.tsx",
      "content": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport type { FilterValue, TextFilter } from \"../types\";\n\ninterface FilterTextProps<T> {\n  category: TextFilter<T>;\n  values: Record<string, FilterValue>;\n  onChange: (values: Record<string, FilterValue>) => void;\n  fieldId: string;\n  bufferValue: string | undefined;\n  onBufferChange: (value: string) => void;\n  onFlush: () => void;\n  onCancel: () => void;\n  descriptionId?: string;\n}\n\nexport function FilterText<T>({\n  category,\n  values,\n  onChange,\n  fieldId,\n  bufferValue,\n  onBufferChange,\n  onFlush,\n  onCancel,\n  descriptionId,\n}: FilterTextProps<T>) {\n  const propValue =\n    typeof values[category.id] === \"string\"\n      ? (values[category.id] as string)\n      : \"\";\n  const inputValue = bufferValue !== undefined ? bufferValue : propValue;\n\n  return (\n    <Input\n      id={fieldId}\n      type=\"text\"\n      value={inputValue}\n      placeholder={category.placeholder}\n      aria-describedby={descriptionId}\n      onChange={(e) => onBufferChange(e.target.value)}\n      onBlur={onFlush}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          e.preventDefault();\n          e.stopPropagation();\n          onCancel();\n          if (propValue.length > 0) {\n            onChange({ ...values, [category.id]: \"\" });\n          }\n        }\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/filter-text.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/filter-toggle.tsx",
      "content": "\"use client\";\n\nimport { Switch } from \"@/components/ui/switch\";\nimport type { FilterValue, ToggleFilter } from \"../types\";\n\ninterface FilterToggleProps<T> {\n  category: ToggleFilter<T>;\n  values: Record<string, FilterValue>;\n  onChange: (values: Record<string, FilterValue>) => void;\n  fieldId: string;\n}\n\nexport function FilterToggle<T>({\n  category,\n  values,\n  onChange,\n  fieldId,\n}: FilterToggleProps<T>) {\n  const checked = values[category.id] === true;\n  return (\n    <div className=\"flex items-center justify-between gap-2 px-1\">\n      <span id={`${fieldId}-state`} className=\"text-sm text-foreground\">\n        {checked ? \"On\" : \"Off\"}\n      </span>\n      <Switch\n        id={fieldId}\n        checked={checked}\n        onCheckedChange={(next) =>\n          onChange({ ...values, [category.id]: next })\n        }\n        aria-labelledby={fieldId}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/filter-toggle.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/mode-toggle.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { FilterMode } from \"../types\";\n\ninterface ModeToggleProps {\n  value: FilterMode;\n  onChange: (mode: FilterMode) => void;\n  ariaLabel: string;\n}\n\nconst MODES: Array<{ mode: FilterMode; label: string }> = [\n  { mode: \"union\", label: \"Union\" },\n  { mode: \"intersection\", label: \"Intersection\" },\n];\n\n/**\n * Plain-button segmented control. Deliberately NOT shadcn ToggleGroup — its\n * value model diverges between Radix (single: string) and Base UI (string[]),\n * an F-cross-13 carrier that breaks Base-UI consumers (event-calendar v0.2.1\n * precedent: same swap, toggle-group dep dropped).\n */\nexport function ModeToggle({ value, onChange, ariaLabel }: ModeToggleProps) {\n  return (\n    <div\n      role=\"group\"\n      aria-label={ariaLabel}\n      className=\"inline-flex overflow-hidden rounded-md border border-input\"\n    >\n      {MODES.map(({ mode, label }) => (\n        <button\n          key={mode}\n          type=\"button\"\n          aria-pressed={value === mode}\n          onClick={() => onChange(mode)}\n          className={cn(\n            \"px-2 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            value === mode\n              ? \"bg-accent text-accent-foreground\"\n              : \"bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground\",\n          )}\n        >\n          {label}\n        </button>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/mode-toggle.tsx"
    },
    {
      "path": "src/registry/components/forms/filter-panel/parts/solo-button.tsx",
      "content": "\"use client\";\n\nimport { Crosshair } from \"lucide-react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SoloButtonProps {\n  optionLabel: string;\n  onClick: () => void;\n}\n\nexport function SoloButton({ optionLabel, onClick }: SoloButtonProps) {\n  const ariaLabel = `Show only ${optionLabel}`;\n  // No delayDuration — Radix-only prop; Base UI's TooltipProvider rejects it (F-cross-13).\n  // No `asChild` either — the trigger IS the button (styled via buttonVariants).\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          aria-label={ariaLabel}\n          onClick={onClick}\n          className={cn(\n            buttonVariants({ variant: \"ghost\", size: \"sm\" }),\n            \"size-6 p-0 text-muted-foreground/70 hover:text-foreground\",\n          )}\n        >\n          <Crosshair aria-hidden=\"true\" className=\"size-3.5\" />\n        </TooltipTrigger>\n        <TooltipContent side=\"left\">Solo: {optionLabel}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/filter-panel/parts/solo-button.tsx"
    }
  ],
  "categories": [
    "forms",
    "graph-system"
  ],
  "type": "registry:block"
}