{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filters",
  "title": "Filters",
  "description": "A filter bar of segmented pills with per-field operators and value controls.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/badge",
    "@cubby-ui/button",
    "@cubby-ui/button-group",
    "@cubby-ui/combobox",
    "@cubby-ui/dropdown-menu",
    "@cubby-ui/kbd",
    "@cubby-ui/input",
    "@cubby-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/filters/filters.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Badge } from \"@/registry/default/badge/badge\";\r\nimport { Button } from \"@/registry/default/button/button\";\r\nimport {\r\n  ButtonGroup,\r\n  ButtonGroupText,\r\n} from \"@/registry/default/button-group/button-group\";\r\nimport {\r\n  Combobox,\r\n  ComboboxItem,\r\n  ComboboxTrigger,\r\n} from \"@/registry/default/combobox/combobox\";\r\nimport {\r\n  DropdownMenu,\r\n  DropdownMenuContent,\r\n  DropdownMenuRadioGroup,\r\n  DropdownMenuRadioItem,\r\n  DropdownMenuTrigger,\r\n} from \"@/registry/default/dropdown-menu/dropdown-menu\";\r\nimport { Kbd } from \"@/registry/default/kbd/kbd\";\r\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\";\r\n\r\nimport { HugeiconsIcon } from \"@hugeicons/react\";\r\nimport { Cancel01Icon, PlusSignIcon } from \"@hugeicons/core-free-icons\";\r\n\r\nimport {\r\n  FilterChipContext,\r\n  FiltersActionsContext,\r\n  FiltersAutoOpenContext,\r\n  FiltersStateContext,\r\n  useFilterChip,\r\n  useFiltersActions,\r\n  useFiltersAutoOpen,\r\n  useFiltersState,\r\n} from \"./filters-context\";\r\nimport { FilterChipValue, FilterSearchPopup } from \"./filters-value-controls\";\r\nimport {\r\n  createFilter,\r\n  describeFilter,\r\n  FILTER_SIZES,\r\n  patchFilter,\r\n  resolveOperators,\r\n} from \"./lib/filters-utils\";\r\nimport type {\r\n  FilterChipProps,\r\n  FilterField,\r\n  FiltersBarProps,\r\n  FiltersLabels,\r\n  FiltersProps,\r\n  FiltersProviderProps,\r\n  FilterValue,\r\n} from \"./lib/filters-types\";\r\n\r\nconst DEFAULT_LABELS: FiltersLabels = {\r\n  add: \"Add filter\",\r\n  clear: \"Clear\",\r\n  searchFields: \"Filter...\",\r\n  searchValues: \"Search...\",\r\n  noFields: \"No filters found.\",\r\n  noResults: \"No results found.\",\r\n  selectValue: \"Select...\",\r\n  enterValue: \"Enter value\",\r\n  value: \"Value\",\r\n  min: \"Min\",\r\n  max: \"Max\",\r\n  operator: \"operator\",\r\n  removeFilter: (fieldLabel) => `Remove ${fieldLabel} filter`,\r\n};\r\n\r\nconst LABEL_KEYS = Object.keys(DEFAULT_LABELS) as (keyof FiltersLabels)[];\r\n\r\n/** Small muted wrapper that normalizes field icons to 14px. */\r\nfunction FieldIcon({ children }: { children?: React.ReactNode }) {\r\n  if (!children) return null;\r\n  return (\r\n    <span className=\"text-muted-foreground flex shrink-0 items-center [&_svg]:size-3.5!\">\r\n      {children}\r\n    </span>\r\n  );\r\n}\r\n\r\n/**\r\n * Moves focus to the adjacent chip's remove button (or the add-filter trigger)\r\n * before a chip is removed, so focus never falls to `<body>`.\r\n */\r\nfunction focusAdjacentChip(chip: HTMLElement | null) {\r\n  const bar = chip?.closest<HTMLElement>('[data-slot=\"filters\"]');\r\n  if (!chip || !bar) return;\r\n  const chips = Array.from(\r\n    bar.querySelectorAll<HTMLElement>('[data-slot=\"filter-chip\"]'),\r\n  );\r\n  const index = chips.indexOf(chip);\r\n  const neighbor = chips[index + 1] ?? chips[index - 1];\r\n  const target =\r\n    neighbor?.querySelector<HTMLElement>('[data-slot=\"filter-chip-remove\"]') ??\r\n    bar.querySelector<HTMLElement>('[data-slot=\"filter-add\"]');\r\n  target?.focus();\r\n}\r\n\r\n/**\r\n * Owns filter state and provides it via context, without rendering any layout.\r\n * Wrap it around a `FiltersBar` plus any external UI (a results count, saved\r\n * views, an apply button) that should share the state through `useFilters`.\r\n */\r\nfunction FiltersProvider({\r\n  fields,\r\n  value,\r\n  defaultValue = [],\r\n  onValueChange,\r\n  size = \"default\",\r\n  allowDuplicateFields = false,\r\n  labels: labelsProp,\r\n  children,\r\n}: FiltersProviderProps) {\r\n  const [filters, setFilters] = useControllableState<FilterValue[]>({\r\n    value,\r\n    defaultValue,\r\n    onValueChange,\r\n  });\r\n\r\n  // Tracks the freshly added filter so its value control opens on mount. The\r\n  // chip consumes (clears) the flag once mounted, so later remounts of the\r\n  // value control don't spuriously re-open it.\r\n  const [lastAddedId, setLastAddedId] = React.useState<string | null>(null);\r\n  const clearAutoOpen = React.useCallback(() => setLastAddedId(null), []);\r\n\r\n  const labels = React.useMemo(\r\n    () => ({ ...DEFAULT_LABELS, ...labelsProp }),\r\n    // Value-level deps (constant length — FiltersLabels is a closed shape) so\r\n    // an inline `labels={{ ... }}` object doesn't churn the actions context.\r\n    // Caveat: `removeFilter` is a function, so an inline arrow for it is a\r\n    // new identity every render and still churns; hoist it in that case.\r\n    // eslint-disable-next-line react-hooks/exhaustive-deps\r\n    LABEL_KEYS.map((key) => labelsProp?.[key]),\r\n  );\r\n  const fieldsById = React.useMemo(\r\n    () => new Map(fields.map((field) => [field.id, field])),\r\n    [fields],\r\n  );\r\n  // Keyed by content (not the filters array identity) so the Set stays\r\n  // referentially stable while a filter's value is being typed.\r\n  const usedFieldsKey = filters\r\n    .map((filter) => filter.field)\r\n    .sort()\r\n    .join(\"\\u0000\");\r\n  const usedFieldIds = React.useMemo(\r\n    () => new Set(usedFieldsKey ? usedFieldsKey.split(\"\\u0000\") : []),\r\n    [usedFieldsKey],\r\n  );\r\n\r\n  const addFilter = React.useCallback(\r\n    (filter: FilterValue) => {\r\n      setFilters((prev) => [...prev, filter]);\r\n      setLastAddedId(filter.id);\r\n    },\r\n    [setFilters],\r\n  );\r\n  const removeFilter = React.useCallback(\r\n    (id: string) => setFilters((prev) => prev.filter((f) => f.id !== id)),\r\n    [setFilters],\r\n  );\r\n  const clearAll = React.useCallback(() => setFilters([]), [setFilters]);\r\n  const updateFilter = React.useCallback(\r\n    (id: string, patch: Partial<Omit<FilterValue, \"id\">>) => {\r\n      setFilters((prev) =>\r\n        prev.map((filter) =>\r\n          filter.id === id\r\n            ? patchFilter(fieldsById.get(filter.field), filter, patch)\r\n            : filter,\r\n        ),\r\n      );\r\n    },\r\n    [setFilters, fieldsById],\r\n  );\r\n\r\n  // Split contexts: `state` changes per keystroke, `actions` stays stable\r\n  // (so leaves subscribed via useFiltersActions don't re-render while\r\n  // typing), and the transient auto-open signal is isolated so its set/consume\r\n  // cycle per add doesn't churn the actions context either.\r\n  const stateContext = React.useMemo(() => ({ filters }), [filters]);\r\n  const autoOpenContext = React.useMemo(\r\n    () => ({ lastAddedId, clearAutoOpen }),\r\n    [lastAddedId, clearAutoOpen],\r\n  );\r\n  const actionsContext = React.useMemo(\r\n    () => ({\r\n      fields,\r\n      size,\r\n      labels,\r\n      fieldsById,\r\n      usedFieldIds,\r\n      allowDuplicateFields,\r\n      addFilter,\r\n      updateFilter,\r\n      removeFilter,\r\n      clearAll,\r\n    }),\r\n    [\r\n      fields,\r\n      size,\r\n      labels,\r\n      fieldsById,\r\n      usedFieldIds,\r\n      allowDuplicateFields,\r\n      addFilter,\r\n      updateFilter,\r\n      removeFilter,\r\n      clearAll,\r\n    ],\r\n  );\r\n\r\n  return (\r\n    <FiltersStateContext.Provider value={stateContext}>\r\n      <FiltersActionsContext.Provider value={actionsContext}>\r\n        <FiltersAutoOpenContext.Provider value={autoOpenContext}>\r\n          {children}\r\n        </FiltersAutoOpenContext.Provider>\r\n      </FiltersActionsContext.Provider>\r\n    </FiltersStateContext.Provider>\r\n  );\r\n}\r\n\r\n/**\r\n * The flex row. Renders the default layout unless `children` is passed. Only\r\n * the default leaves subscribe to filter state, so a bar with custom children\r\n * doesn't re-render while a value is being typed.\r\n */\r\nfunction FiltersBar({\r\n  shortcut,\r\n  className,\r\n  children,\r\n  ...props\r\n}: FiltersBarProps) {\r\n  return (\r\n    <div\r\n      data-slot=\"filters\"\r\n      className={cn(\"flex flex-wrap items-center gap-2\", className)}\r\n      {...props}\r\n    >\r\n      {children ?? (\r\n        <>\r\n          <FilterChips />\r\n          <FilterAddButton shortcut={shortcut} />\r\n          <FilterClearButton />\r\n        </>\r\n      )}\r\n    </div>\r\n  );\r\n}\r\n\r\n/** `FiltersProvider` + `FiltersBar` in one component, for the common case. */\r\nfunction Filters({\r\n  fields,\r\n  value,\r\n  defaultValue,\r\n  onValueChange,\r\n  size,\r\n  allowDuplicateFields,\r\n  labels,\r\n  ...barProps\r\n}: FiltersProps) {\r\n  return (\r\n    <FiltersProvider\r\n      fields={fields}\r\n      value={value}\r\n      defaultValue={defaultValue}\r\n      onValueChange={onValueChange}\r\n      size={size}\r\n      allowDuplicateFields={allowDuplicateFields}\r\n      labels={labels}\r\n    >\r\n      <FiltersBar {...barProps} />\r\n    </FiltersProvider>\r\n  );\r\n}\r\n\r\n/** Renders a `FilterChip` for every active filter. */\r\nfunction FilterChips() {\r\n  const { filters } = useFiltersState();\r\n  const { fieldsById } = useFiltersActions();\r\n  return (\r\n    <>\r\n      {filters.map((filter) => {\r\n        const field = fieldsById.get(filter.field);\r\n        if (!field) return null;\r\n        return <FilterChip key={filter.id} filter={filter} field={field} />;\r\n      })}\r\n    </>\r\n  );\r\n}\r\n\r\n// Memoized: chips subscribe only to the stable actions context and untouched\r\n// `filter` objects keep their identity across edits, so typing in one chip's\r\n// value doesn't re-render the others.\r\nconst FilterChip = React.memo(function FilterChip({\r\n  filter,\r\n  field: fieldProp,\r\n  className,\r\n  children,\r\n  ...props\r\n}: FilterChipProps) {\r\n  const { size, removeFilter, fieldsById } = useFiltersActions();\r\n  const { lastAddedId, clearAutoOpen } = useFiltersAutoOpen();\r\n  const field = fieldProp ?? fieldsById.get(filter.field);\r\n  const autoOpen = filter.id === lastAddedId;\r\n\r\n  // Consume the auto-open flag once this chip has mounted, so later remounts\r\n  // of the value control (e.g. an operator shape change) don't re-open it.\r\n  React.useEffect(() => {\r\n    if (autoOpen) clearAutoOpen();\r\n  }, [autoOpen, clearAutoOpen]);\r\n\r\n  const chipContext = React.useMemo(\r\n    () => (field ? { filter, field, size, autoOpen } : null),\r\n    [filter, field, size, autoOpen],\r\n  );\r\n\r\n  if (!field || !chipContext) return null;\r\n\r\n  return (\r\n    <FilterChipContext.Provider value={chipContext}>\r\n      <ButtonGroup\r\n        data-slot=\"filter-chip\"\r\n        aria-label={describeFilter(field, filter)}\r\n        className={cn(\r\n          \"bg-card overflow-hidden rounded-lg border bg-clip-padding\",\r\n          className,\r\n        )}\r\n        onKeyDown={(event) => {\r\n          if (event.key !== \"Backspace\" && event.key !== \"Delete\") return;\r\n          // Only remove when focus is on one of the chip's own button\r\n          // segments; inputs and custom controls keep their editing keys.\r\n          const target = event.target;\r\n          const isChipButton =\r\n            target instanceof HTMLElement &&\r\n            target.tagName === \"BUTTON\" &&\r\n            (target.dataset.slot === \"filter-chip-value\" ||\r\n              target.closest(\r\n                '[data-slot=\"filter-chip-remove\"], [data-slot=\"filter-chip-operator\"]',\r\n              ) !== null);\r\n          if (!isChipButton) return;\r\n          event.preventDefault();\r\n          focusAdjacentChip(event.currentTarget);\r\n          removeFilter(filter.id);\r\n        }}\r\n        {...props}\r\n      >\r\n        {children ?? (\r\n          <>\r\n            <FilterChipField />\r\n            <FilterChipOperator />\r\n            <FilterChipValue />\r\n            <FilterChipRemove />\r\n          </>\r\n        )}\r\n      </ButtonGroup>\r\n    </FilterChipContext.Provider>\r\n  );\r\n});\r\n\r\nfunction FilterChipField({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"div\">) {\r\n  const { field, size } = useFilterChip();\r\n  return (\r\n    <ButtonGroupText\r\n      data-slot=\"filter-chip-field\"\r\n      className={cn(\r\n        \"text-foreground gap-1.5 rounded-none border-0 border-r font-medium\",\r\n        FILTER_SIZES[size].fieldLabel,\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      {children ?? (\r\n        <>\r\n          <FieldIcon>{field.icon}</FieldIcon>\r\n          {field.label}\r\n        </>\r\n      )}\r\n    </ButtonGroupText>\r\n  );\r\n}\r\n\r\nfunction FilterChipOperator({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<typeof Button>) {\r\n  const { filter, field, size } = useFilterChip();\r\n  const { updateFilter, labels } = useFiltersActions();\r\n  const operators = resolveOperators(field);\r\n  const current = operators.find((operator) => operator.id === filter.operator);\r\n\r\n  return (\r\n    <DropdownMenu>\r\n      <DropdownMenuTrigger\r\n        render={\r\n          <Button\r\n            data-slot=\"filter-chip-operator\"\r\n            aria-label={`${field.label} ${labels.operator}: ${current?.label ?? filter.operator}`}\r\n            variant=\"ghost\"\r\n            size={size}\r\n            className={cn(\r\n              \"rounded-none! font-normal focus-visible:-outline-offset-2\",\r\n              className,\r\n            )}\r\n            {...props}\r\n          />\r\n        }\r\n      >\r\n        {current?.label ?? filter.operator}\r\n      </DropdownMenuTrigger>\r\n      <DropdownMenuContent align=\"start\" className=\"min-w-40\">\r\n        <DropdownMenuRadioGroup\r\n          value={filter.operator}\r\n          onValueChange={(next) => updateFilter(filter.id, { operator: next })}\r\n        >\r\n          {operators.map((operator) => (\r\n            <DropdownMenuRadioItem key={operator.id} value={operator.id}>\r\n              {operator.label}\r\n            </DropdownMenuRadioItem>\r\n          ))}\r\n        </DropdownMenuRadioGroup>\r\n      </DropdownMenuContent>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n\r\nfunction FilterChipRemove({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<typeof Button>) {\r\n  const { filter, field } = useFilterChip();\r\n  const { removeFilter, size, labels } = useFiltersActions();\r\n  return (\r\n    <Button\r\n      data-slot=\"filter-chip-remove\"\r\n      aria-label={labels.removeFilter(field.label)}\r\n      variant=\"ghost\"\r\n      size={FILTER_SIZES[size].iconButton}\r\n      className={cn(\r\n        \"text-muted-foreground hover:text-foreground rounded-none focus-visible:-outline-offset-2\",\r\n        className,\r\n      )}\r\n      onClick={(event) => {\r\n        focusAdjacentChip(\r\n          event.currentTarget.closest<HTMLElement>('[data-slot=\"filter-chip\"]'),\r\n        );\r\n        removeFilter(filter.id);\r\n      }}\r\n      {...props}\r\n    >\r\n      <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction FilterAddButton({\r\n  children,\r\n  shortcut,\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<typeof Button> & {\r\n  /** Key that opens this menu from the keyboard (e.g. `\"f\"`). */\r\n  shortcut?: string;\r\n}) {\r\n  const {\r\n    fields,\r\n    usedFieldIds,\r\n    allowDuplicateFields,\r\n    addFilter,\r\n    labels,\r\n    size,\r\n  } = useFiltersActions();\r\n  const [open, setOpen] = React.useState(false);\r\n\r\n  React.useEffect(() => {\r\n    if (!shortcut) return;\r\n    const handleKeyDown = (event: KeyboardEvent) => {\r\n      // `defaultPrevented` also dedupes multiple Filters instances: the first\r\n      // listener to accept the key prevents it for the rest.\r\n      if (event.defaultPrevented) return;\r\n      if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {\r\n        return;\r\n      }\r\n      if (event.key.toLowerCase() !== shortcut.toLowerCase()) return;\r\n      const target = event.target;\r\n      if (target instanceof HTMLElement) {\r\n        if (\r\n          target instanceof HTMLInputElement ||\r\n          target instanceof HTMLTextAreaElement ||\r\n          target.isContentEditable\r\n        ) {\r\n          return;\r\n        }\r\n        // Don't steal the key from open popups (menu typeahead, dialogs).\r\n        if (\r\n          target.closest(\r\n            '[role=\"menu\"], [role=\"listbox\"], [role=\"dialog\"], [role=\"alertdialog\"]',\r\n          )\r\n        ) {\r\n          return;\r\n        }\r\n      }\r\n      event.preventDefault();\r\n      setOpen(true);\r\n    };\r\n    window.addEventListener(\"keydown\", handleKeyDown);\r\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\r\n  }, [shortcut]);\r\n\r\n  return (\r\n    <Combobox<FilterField, false>\r\n      items={fields}\r\n      value={null}\r\n      open={open}\r\n      onOpenChange={setOpen}\r\n      onValueChange={(field) => {\r\n        if (field) addFilter(createFilter(field));\r\n      }}\r\n      itemToStringLabel={(field) => field.label}\r\n    >\r\n      <ComboboxTrigger\r\n        render={(triggerProps) => (\r\n          <Button\r\n            {...triggerProps}\r\n            data-slot=\"filter-add\"\r\n            variant=\"outline\"\r\n            size={size}\r\n            className={cn(\r\n              // The border renders on the button's paint pseudo-element, so\r\n              // border style overrides use before: classes.\r\n              \"text-muted-foreground gap-1.5 before:border-dashed\",\r\n              className,\r\n            )}\r\n            leadingIcon={<HugeiconsIcon icon={PlusSignIcon} strokeWidth={2} />}\r\n            trailingIcon={\r\n              shortcut ? (\r\n                <Kbd size=\"sm\" variant=\"ghost\" className=\"ms-1\">\r\n                  {shortcut.toUpperCase()}\r\n                </Kbd>\r\n              ) : undefined\r\n            }\r\n            {...props}\r\n          >\r\n            {children ?? labels.add}\r\n          </Button>\r\n        )}\r\n      />\r\n      <FilterSearchPopup\r\n        className=\"min-w-56\"\r\n        placeholder={labels.searchFields}\r\n        empty={labels.noFields}\r\n      >\r\n        {(field: FilterField) => (\r\n          <ComboboxItem\r\n            key={field.id}\r\n            value={field}\r\n            disabled={!allowDuplicateFields && usedFieldIds.has(field.id)}\r\n          >\r\n            <span className=\"flex items-center gap-2\">\r\n              <FieldIcon>{field.icon}</FieldIcon>\r\n              <span className=\"truncate\">{field.label}</span>\r\n            </span>\r\n          </ComboboxItem>\r\n        )}\r\n      </FilterSearchPopup>\r\n    </Combobox>\r\n  );\r\n}\r\n\r\n/** Clears every filter. Renders nothing while no filters are active. */\r\nfunction FilterClearButton({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<typeof Button>) {\r\n  const { clearAll, labels, size } = useFiltersActions();\r\n  const { filters } = useFiltersState();\r\n  if (filters.length === 0) return null;\r\n  return (\r\n    <Button\r\n      data-slot=\"filter-clear\"\r\n      variant=\"ghost\"\r\n      size={size}\r\n      className={cn(\"text-muted-foreground gap-1.5\", className)}\r\n      onClick={clearAll}\r\n      leadingIcon={<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />}\r\n      {...props}\r\n    >\r\n      {children ?? labels.clear}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction FilterActiveCount({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<typeof Badge>) {\r\n  const { filters } = useFiltersState();\r\n  return (\r\n    <Badge\r\n      data-slot=\"filter-active-count\"\r\n      variant=\"neutral\"\r\n      className={className}\r\n      {...props}\r\n    >\r\n      {filters.length}\r\n    </Badge>\r\n  );\r\n}\r\n\r\nexport {\r\n  Filters,\r\n  FiltersProvider,\r\n  FiltersBar,\r\n  FilterChips,\r\n  FilterChip,\r\n  FilterChipField,\r\n  FilterChipOperator,\r\n  FilterChipValue,\r\n  FilterChipRemove,\r\n  FilterAddButton,\r\n  FilterClearButton,\r\n  FilterActiveCount,\r\n};\r\nexport {\r\n  useFilters,\r\n  useFiltersState,\r\n  useFiltersActions,\r\n  useFilterChip,\r\n} from \"./filters-context\";\r\nexport {\r\n  createFilter,\r\n  patchFilter,\r\n  resolveOperators,\r\n  defaultOperatorsFor,\r\n  operatorShape,\r\n  operatorShapeFor,\r\n  isValuelessOperator,\r\n  emptyValueFor,\r\n  formatFilterValue,\r\n  describeFilter,\r\n  asFilterValues,\r\n} from \"./lib/filters-utils\";\r\nexport type {\r\n  FilterField,\r\n  FilterFieldType,\r\n  FilterOption,\r\n  FilterOperator,\r\n  FilterOperatorShape,\r\n  FilterValue,\r\n  FilterSize,\r\n  FiltersLabels,\r\n  FiltersProps,\r\n  FiltersProviderProps,\r\n  FiltersBarProps,\r\n  FilterChipProps,\r\n  FilterValueControlProps,\r\n  NumberRange,\r\n  SelectFilterField,\r\n  MultiSelectFilterField,\r\n  TextFilterField,\r\n  NumberFilterField,\r\n  CustomFilterField,\r\n} from \"./lib/filters-types\";\r\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/filters/filters.tsx"
    },
    {
      "path": "registry/default/filters/lib/filters-types.ts",
      "content": "import type * as React from \"react\";\r\n\r\n/** Size of the filter bar and its pills. */\r\nexport type FilterSize = \"sm\" | \"default\" | \"lg\";\r\n\r\n/** Built-in field types. `custom` renders its own value control. */\r\nexport type FilterFieldType =\r\n  | \"select\"\r\n  | \"multiselect\"\r\n  | \"text\"\r\n  | \"number\"\r\n  | \"custom\";\r\n\r\n/** A selectable option for `select` / `multiselect` fields. */\r\nexport interface FilterOption {\r\n  value: string;\r\n  label: string;\r\n  icon?: React.ReactNode;\r\n}\r\n\r\n/**\r\n * The value shape an operator expects: `\"none\"` hides the value segment\r\n * (`is empty`), `\"range\"` renders paired min/max inputs on number fields\r\n * (`between`), and `\"scalar\"` renders the field's normal single control.\r\n */\r\nexport type FilterOperatorShape = \"none\" | \"scalar\" | \"range\";\r\n\r\n/** An operator shown in the middle segment of a pill (`is`, `contains`, ...). */\r\nexport interface FilterOperator {\r\n  id: string;\r\n  label: string;\r\n  /** Value shape this operator expects. Defaults to `\"scalar\"`. */\r\n  shape?: FilterOperatorShape;\r\n  /** Sugar for `shape: \"none\"`, e.g. `is empty` / `is not empty`. */\r\n  valueless?: boolean;\r\n}\r\n\r\ninterface FilterFieldBase {\r\n  /** Stable key stored on each filter as `FilterValue.field`. */\r\n  id: string;\r\n  label: string;\r\n  icon?: React.ReactNode;\r\n  /** Override the default operators for this field's type. */\r\n  operators?: FilterOperator[];\r\n  /** Hide specific default operators by id. */\r\n  disabledOperators?: string[];\r\n}\r\n\r\nexport interface SelectFilterField extends FilterFieldBase {\r\n  type: \"select\";\r\n  options: FilterOption[];\r\n  placeholder?: string;\r\n}\r\n\r\nexport interface MultiSelectFilterField extends FilterFieldBase {\r\n  type: \"multiselect\";\r\n  options: FilterOption[];\r\n  placeholder?: string;\r\n  /** Maximum number of options that can be selected. */\r\n  maxSelections?: number;\r\n}\r\n\r\nexport interface TextFilterField extends FilterFieldBase {\r\n  type: \"text\";\r\n  placeholder?: string;\r\n  /** Content shown before the input, e.g. `@` or an icon. */\r\n  prefix?: React.ReactNode;\r\n  /** Content shown after the input, e.g. a unit label. */\r\n  suffix?: React.ReactNode;\r\n}\r\n\r\nexport interface NumberFilterField extends FilterFieldBase {\r\n  type: \"number\";\r\n  placeholder?: string;\r\n  step?: number;\r\n  /** Content shown before the input, e.g. `$`. */\r\n  prefix?: React.ReactNode;\r\n  /** Content shown after the input, e.g. `%` or `hrs`. */\r\n  suffix?: React.ReactNode;\r\n}\r\n\r\nexport interface CustomFilterField extends FilterFieldBase {\r\n  type: \"custom\";\r\n  /** Renders the value segment and reports changes back to the filter. */\r\n  renderValue: (props: FilterValueControlProps) => React.ReactNode;\r\n  /** Seed value for a fresh filter of this field. */\r\n  defaultValue?: unknown;\r\n}\r\n\r\nexport type FilterField =\r\n  | SelectFilterField\r\n  | MultiSelectFilterField\r\n  | TextFilterField\r\n  | NumberFilterField\r\n  | CustomFilterField;\r\n\r\n/** Props passed to a `custom` field's `renderValue`. */\r\nexport interface FilterValueControlProps {\r\n  value: unknown;\r\n  operator: string;\r\n  onValueChange: (value: unknown) => void;\r\n  size: FilterSize;\r\n  field: FilterField;\r\n}\r\n\r\n/** Value shape for a `number` field when the operator is `between`. */\r\nexport interface NumberRange {\r\n  min: number | null;\r\n  max: number | null;\r\n}\r\n\r\n/**\r\n * A single active filter. `value` is typed by the field:\r\n * `select → string | null`, `multiselect → string[]`, `text → string`,\r\n * `number → number | null | NumberRange`, `custom → unknown`.\r\n */\r\nexport interface FilterValue {\r\n  id: string;\r\n  field: string;\r\n  operator: string;\r\n  value: unknown;\r\n}\r\n\r\n/** Copy overrides for the bar's chrome. */\r\nexport interface FiltersLabels {\r\n  add: string;\r\n  clear: string;\r\n  searchFields: string;\r\n  searchValues: string;\r\n  noFields: string;\r\n  noResults: string;\r\n  selectValue: string;\r\n  enterValue: string;\r\n  value: string;\r\n  min: string;\r\n  max: string;\r\n  /** Word used in the operator trigger's accessible name. */\r\n  operator: string;\r\n  /** Builds the accessible label for a pill's remove button. */\r\n  removeFilter: (fieldLabel: string) => string;\r\n}\r\n\r\nexport interface FiltersProviderProps {\r\n  /** Field definitions the bar can filter on. */\r\n  fields: FilterField[];\r\n  /** Controlled list of active filters. */\r\n  value?: FilterValue[];\r\n  /** Initial filters in uncontrolled mode. */\r\n  defaultValue?: FilterValue[];\r\n  onValueChange?: (value: FilterValue[]) => void;\r\n  size?: FilterSize;\r\n  /** Allow the same field to be added more than once. Defaults to `false`. */\r\n  allowDuplicateFields?: boolean;\r\n  labels?: Partial<FiltersLabels>;\r\n  children?: React.ReactNode;\r\n}\r\n\r\nexport interface FiltersBarProps extends Omit<\r\n  React.ComponentProps<\"div\">,\r\n  \"onChange\" | \"defaultValue\"\r\n> {\r\n  /** Key that opens the add-filter menu, forwarded to the default `FilterAddButton`. */\r\n  shortcut?: string;\r\n}\r\n\r\nexport interface FiltersProps\r\n  extends Omit<FiltersProviderProps, \"children\">, FiltersBarProps {}\r\n\r\nexport interface FilterChipProps extends Omit<\r\n  React.ComponentProps<\"div\">,\r\n  \"onChange\"\r\n> {\r\n  filter: FilterValue;\r\n  /** Field definition. Resolved from the `Filters` context when omitted. */\r\n  field?: FilterField;\r\n}\r\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/filters/lib/filters-types.ts"
    },
    {
      "path": "registry/default/filters/lib/filters-utils.ts",
      "content": "import type {\r\n  FilterField,\r\n  FilterFieldType,\r\n  FilterOperator,\r\n  FilterOperatorShape,\r\n  FilterSize,\r\n  FilterValue,\r\n  NumberRange,\r\n} from \"./filters-types\";\r\n\r\n/**\r\n * Per-size styling contract shared by every segment of a pill. Restyle here\r\n * rather than in the individual components.\r\n */\r\nexport const FILTER_SIZES: Record<\r\n  FilterSize,\r\n  {\r\n    /** `Button` size for icon-only segments (the remove button). */\r\n    iconButton: \"icon_sm\" | \"icon\" | \"icon_lg\";\r\n    /** `Input` size for inline text inputs. */\r\n    input: \"sm\" | \"default\";\r\n    /** Height matching the `Button` size, for non-button segments. */\r\n    height: string;\r\n    /** Padding + text classes for the field-label segment. */\r\n    fieldLabel: string;\r\n  }\r\n> = {\r\n  sm: {\r\n    iconButton: \"icon_sm\",\r\n    input: \"sm\",\r\n    height: \"h-9 sm:h-8\",\r\n    fieldLabel: \"px-2.5 text-xs\",\r\n  },\r\n  default: {\r\n    iconButton: \"icon\",\r\n    input: \"default\",\r\n    height: \"h-10 sm:h-9\",\r\n    fieldLabel: \"px-3\",\r\n  },\r\n  lg: {\r\n    iconButton: \"icon_lg\",\r\n    input: \"default\",\r\n    height: \"h-11 sm:h-10\",\r\n    fieldLabel: \"px-4\",\r\n  },\r\n};\r\n\r\n// ----- Value coercers ---------------------------------------------------\r\n// `FilterValue.value` is `unknown` (it may arrive from URL state or other\r\n// untrusted sources, e.g. `JSON.parse`), so every read goes through a coercer\r\n// that rebuilds the expected shape instead of trusting a cast.\r\n\r\n/** Coerces an unknown filter value to a string (`\"\"` when absent). */\r\nexport function asString(value: unknown): string {\r\n  return typeof value === \"string\" ? value : \"\";\r\n}\r\n\r\n/** Coerces an unknown filter value to a string array. */\r\nexport function asStringArray(value: unknown): string[] {\r\n  return Array.isArray(value)\r\n    ? value.filter((item): item is string => typeof item === \"string\")\r\n    : [];\r\n}\r\n\r\n/** Coerces an unknown filter value to a finite number or `null`. */\r\nexport function asNumberOrNull(value: unknown): number | null {\r\n  return typeof value === \"number\" && Number.isFinite(value) ? value : null;\r\n}\r\n\r\n/** Rebuilds a `NumberRange`, coercing each bound independently. */\r\nexport function asNumberRange(value: unknown): NumberRange {\r\n  if (typeof value === \"object\" && value !== null) {\r\n    const candidate = value as Partial<Record<\"min\" | \"max\", unknown>>;\r\n    return {\r\n      min: asNumberOrNull(candidate.min),\r\n      max: asNumberOrNull(candidate.max),\r\n    };\r\n  }\r\n  return { min: null, max: null };\r\n}\r\n\r\n/**\r\n * Coerces unknown JSON (e.g. a parsed URL param) into a `FilterValue` array,\r\n * dropping entries whose envelope (`id` / `field` / `operator`) is malformed.\r\n * `value` stays `unknown`; the per-field coercers above handle it downstream.\r\n */\r\nexport function asFilterValues(value: unknown): FilterValue[] {\r\n  if (!Array.isArray(value)) return [];\r\n  return value\r\n    .filter(\r\n      (item): item is Record<string, unknown> =>\r\n        typeof item === \"object\" && item !== null,\r\n    )\r\n    .filter(\r\n      (item) =>\r\n        typeof item.id === \"string\" &&\r\n        typeof item.field === \"string\" &&\r\n        typeof item.operator === \"string\",\r\n    )\r\n    .map((item) => ({\r\n      id: item.id as string,\r\n      field: item.field as string,\r\n      operator: item.operator as string,\r\n      value: item.value,\r\n    }));\r\n}\r\n\r\n/** Default English labels for the built-in operators. */\r\nconst OPERATOR_LABELS: Record<string, string> = {\r\n  is: \"is\",\r\n  is_not: \"is not\",\r\n  is_empty: \"is empty\",\r\n  is_not_empty: \"is not empty\",\r\n  is_any_of: \"is any of\",\r\n  is_not_any_of: \"is not any of\",\r\n  includes_all: \"includes all\",\r\n  contains: \"contains\",\r\n  not_contains: \"does not contain\",\r\n  starts_with: \"starts with\",\r\n  ends_with: \"ends with\",\r\n  eq: \"=\",\r\n  neq: \"≠\",\r\n  gt: \">\",\r\n  lt: \"<\",\r\n  between: \"between\",\r\n};\r\n\r\nfunction op(id: string, shape: FilterOperatorShape = \"scalar\"): FilterOperator {\r\n  return { id, label: OPERATOR_LABELS[id] ?? id, shape };\r\n}\r\n\r\n/** The default operator set for a field type, in display order. */\r\nexport function defaultOperatorsFor(type: FilterFieldType): FilterOperator[] {\r\n  switch (type) {\r\n    case \"select\":\r\n      return [\r\n        op(\"is\"),\r\n        op(\"is_not\"),\r\n        op(\"is_empty\", \"none\"),\r\n        op(\"is_not_empty\", \"none\"),\r\n      ];\r\n    case \"multiselect\":\r\n      return [\r\n        op(\"is_any_of\"),\r\n        op(\"is_not_any_of\"),\r\n        op(\"includes_all\"),\r\n        op(\"is_empty\", \"none\"),\r\n      ];\r\n    case \"text\":\r\n      return [\r\n        op(\"contains\"),\r\n        op(\"not_contains\"),\r\n        op(\"starts_with\"),\r\n        op(\"ends_with\"),\r\n        op(\"is\"),\r\n        op(\"is_empty\", \"none\"),\r\n      ];\r\n    case \"number\":\r\n      return [op(\"eq\"), op(\"neq\"), op(\"gt\"), op(\"lt\"), op(\"between\", \"range\")];\r\n    case \"custom\":\r\n    default:\r\n      return [op(\"is\")];\r\n  }\r\n}\r\n\r\n/** Resolves the operators available for a field, honoring overrides. */\r\nexport function resolveOperators(field: FilterField): FilterOperator[] {\r\n  const base = field.operators ?? defaultOperatorsFor(field.type);\r\n  if (!field.disabledOperators?.length) return base;\r\n  const disabled = new Set(field.disabledOperators);\r\n  return base.filter((operator) => !disabled.has(operator.id));\r\n}\r\n\r\n/** The value shape an operator declares (`valueless` is sugar for `\"none\"`). */\r\nexport function operatorShape(operator: FilterOperator): FilterOperatorShape {\r\n  return operator.shape ?? (operator.valueless ? \"none\" : \"scalar\");\r\n}\r\n\r\n/** Resolves the value shape for a field's operator by id. */\r\nexport function operatorShapeFor(\r\n  field: FilterField,\r\n  operatorId: string,\r\n): FilterOperatorShape {\r\n  const operator = resolveOperators(field).find((o) => o.id === operatorId);\r\n  return operator ? operatorShape(operator) : \"scalar\";\r\n}\r\n\r\n/** Whether the given operator hides the value segment. */\r\nexport function isValuelessOperator(\r\n  field: FilterField,\r\n  operatorId: string,\r\n): boolean {\r\n  return operatorShapeFor(field, operatorId) === \"none\";\r\n}\r\n\r\n/** A typed empty value for a fresh filter of `field` with `operatorId`. */\r\nexport function emptyValueFor(field: FilterField, operatorId: string): unknown {\r\n  const shape = operatorShapeFor(field, operatorId);\r\n  if (shape === \"none\") return null;\r\n  if (shape === \"range\") {\r\n    return { min: null, max: null } satisfies NumberRange;\r\n  }\r\n  switch (field.type) {\r\n    case \"multiselect\":\r\n      return [] as string[];\r\n    case \"text\":\r\n      return \"\";\r\n    case \"number\":\r\n      return null;\r\n    case \"custom\":\r\n      return field.defaultValue ?? null;\r\n    case \"select\":\r\n    default:\r\n      return null;\r\n  }\r\n}\r\n\r\n/**\r\n * Classifies the value shape for an operator so a shape change (e.g. `eq` to\r\n * `between`, or entering a valueless operator) can trigger a value reset.\r\n */\r\nexport function valueShape(field: FilterField, operatorId: string): string {\r\n  const shape = operatorShapeFor(field, operatorId);\r\n  if (shape === \"none\") return \"none\";\r\n  if (shape === \"range\") return \"range\";\r\n  return field.type;\r\n}\r\n\r\nfunction generateId(): string {\r\n  // Filter ids only need list-key uniqueness. `crypto.randomUUID` is absent\r\n  // in non-secure contexts (plain-HTTP LAN dev), hence the cheap fallback.\r\n  if (\r\n    typeof crypto !== \"undefined\" &&\r\n    typeof crypto.randomUUID === \"function\"\r\n  ) {\r\n    return crypto.randomUUID();\r\n  }\r\n  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\r\n}\r\n\r\n/**\r\n * Creates a `FilterValue` with a stable id. The operator defaults to the\r\n * field's first resolved operator and the value to a typed empty seed.\r\n */\r\nexport function createFilter(\r\n  field: FilterField,\r\n  partial?: Partial<Omit<FilterValue, \"field\">>,\r\n): FilterValue {\r\n  const operators = resolveOperators(field);\r\n  const operator = partial?.operator ?? operators[0]?.id ?? \"is\";\r\n  return {\r\n    id: partial?.id ?? generateId(),\r\n    field: field.id,\r\n    operator,\r\n    value:\r\n      partial && \"value\" in partial\r\n        ? partial.value\r\n        : emptyValueFor(field, operator),\r\n  };\r\n}\r\n\r\n/**\r\n * Applies a patch to a filter. When only the operator changes and the new\r\n * operator expects a different value shape (e.g. `eq` to `between`, or into a\r\n * valueless operator), the value is reseeded to a typed empty.\r\n */\r\nexport function patchFilter(\r\n  field: FilterField | undefined,\r\n  filter: FilterValue,\r\n  patch: Partial<Omit<FilterValue, \"id\">>,\r\n): FilterValue {\r\n  const next: FilterValue = { ...filter, ...patch };\r\n  const operatorChanged =\r\n    patch.operator !== undefined && patch.operator !== filter.operator;\r\n  if (\r\n    operatorChanged &&\r\n    !(\"value\" in patch) &&\r\n    field &&\r\n    valueShape(field, filter.operator) !== valueShape(field, next.operator)\r\n  ) {\r\n    next.value = emptyValueFor(field, next.operator);\r\n  }\r\n  return next;\r\n}\r\n\r\n/**\r\n * Formats a filter's value as a human-readable string. Shared by the visible\r\n * value controls and the aria summary so the two never diverge.\r\n */\r\nexport function formatFilterValue(\r\n  field: FilterField,\r\n  filter: FilterValue,\r\n): string {\r\n  const shape = operatorShapeFor(field, filter.operator);\r\n  if (shape === \"none\") return \"\";\r\n  if (shape === \"range\") {\r\n    const { min, max } = asNumberRange(filter.value);\r\n    if (min === null && max === null) return \"\";\r\n    if (min === null) return `up to ${max}`;\r\n    if (max === null) return `from ${min}`;\r\n    return `${min} to ${max}`;\r\n  }\r\n  switch (field.type) {\r\n    case \"select\":\r\n      return (\r\n        field.options.find((option) => option.value === filter.value)?.label ??\r\n        \"\"\r\n      );\r\n    case \"multiselect\": {\r\n      const values = asStringArray(filter.value);\r\n      return field.options\r\n        .filter((option) => values.includes(option.value))\r\n        .map((option) => option.label)\r\n        .join(\", \");\r\n    }\r\n    case \"text\":\r\n      return asString(filter.value);\r\n    case \"number\": {\r\n      const numeric = asNumberOrNull(filter.value);\r\n      return numeric === null ? \"\" : String(numeric);\r\n    }\r\n    default:\r\n      return \"\";\r\n  }\r\n}\r\n\r\n/** Builds a plain-language summary of a filter, e.g. for screen readers. */\r\nexport function describeFilter(\r\n  field: FilterField,\r\n  filter: FilterValue,\r\n): string {\r\n  const operatorLabel =\r\n    resolveOperators(field).find((operator) => operator.id === filter.operator)\r\n      ?.label ?? filter.operator;\r\n  const summary = formatFilterValue(field, filter);\r\n  return summary\r\n    ? `${field.label} ${operatorLabel} ${summary}`\r\n    : `${field.label} ${operatorLabel}`;\r\n}\r\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/filters/lib/filters-utils.ts"
    },
    {
      "path": "registry/default/filters/filters-context.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nimport type {\r\n  FilterField,\r\n  FilterSize,\r\n  FilterValue,\r\n  FiltersLabels,\r\n} from \"./lib/filters-types\";\r\n\r\n/**\r\n * Fast-changing state: `filters` gets a new identity on every edit (including\r\n * each keystroke in a text or number filter). Subscribe only when you render\r\n * the filters themselves.\r\n */\r\ninterface FiltersStateContextValue {\r\n  filters: FilterValue[];\r\n}\r\n\r\n/**\r\n * Configuration and actions. Referentially stable across value edits (it only\r\n * changes when the field config changes or a filter is added/removed), so\r\n * leaves like the add/clear buttons and memoized chips can subscribe without\r\n * re-rendering per keystroke.\r\n */\r\ninterface FiltersActionsContextValue {\r\n  fields: FilterField[];\r\n  size: FilterSize;\r\n  labels: FiltersLabels;\r\n  fieldsById: Map<string, FilterField>;\r\n  /** Field ids with at least one active filter. Stable across value edits. */\r\n  usedFieldIds: Set<string>;\r\n  allowDuplicateFields: boolean;\r\n  addFilter: (filter: FilterValue) => void;\r\n  updateFilter: (id: string, patch: Partial<Omit<FilterValue, \"id\">>) => void;\r\n  removeFilter: (id: string) => void;\r\n  clearAll: () => void;\r\n}\r\n\r\n/**\r\n * The transient auto-open signal, isolated in its own context so the two\r\n * invalidations per add (set, then consume) don't churn the actions context.\r\n */\r\ninterface FiltersAutoOpenContextValue {\r\n  /** Id of the most recently added filter, used to auto-open its value control. */\r\n  lastAddedId: string | null;\r\n  /** Clears `lastAddedId`; called by the freshly added chip once it mounts. */\r\n  clearAutoOpen: () => void;\r\n}\r\n\r\ntype FiltersContextValue = FiltersStateContextValue &\r\n  FiltersActionsContextValue &\r\n  FiltersAutoOpenContextValue;\r\n\r\nconst FiltersStateContext =\r\n  React.createContext<FiltersStateContextValue | null>(null);\r\nconst FiltersActionsContext =\r\n  React.createContext<FiltersActionsContextValue | null>(null);\r\nconst FiltersAutoOpenContext =\r\n  React.createContext<FiltersAutoOpenContextValue | null>(null);\r\n\r\n/** The bar's fast-changing state (`filters`). Re-renders on every edit. */\r\nfunction useFiltersState(): FiltersStateContextValue {\r\n  const context = React.use(FiltersStateContext);\r\n  if (!context) {\r\n    throw new Error(\"useFiltersState must be used within a FiltersProvider.\");\r\n  }\r\n  return context;\r\n}\r\n\r\n/** The bar's config and actions. Stable while a filter value is being edited. */\r\nfunction useFiltersActions(): FiltersActionsContextValue {\r\n  const context = React.use(FiltersActionsContext);\r\n  if (!context) {\r\n    throw new Error(\"useFiltersActions must be used within a FiltersProvider.\");\r\n  }\r\n  return context;\r\n}\r\n\r\n/** The auto-open signal for freshly added chips. */\r\nfunction useFiltersAutoOpen(): FiltersAutoOpenContextValue {\r\n  const context = React.use(FiltersAutoOpenContext);\r\n  if (!context) {\r\n    throw new Error(\r\n      \"useFiltersAutoOpen must be used within a FiltersProvider.\",\r\n    );\r\n  }\r\n  return context;\r\n}\r\n\r\n/**\r\n * Everything from all contexts. Convenient, but re-renders on every filter\r\n * edit; subscribe to `useFiltersActions` alone when that matters.\r\n */\r\nfunction useFilters(): FiltersContextValue {\r\n  const state = useFiltersState();\r\n  const actions = useFiltersActions();\r\n  const autoOpen = useFiltersAutoOpen();\r\n  return React.useMemo(\r\n    () => ({ ...state, ...actions, ...autoOpen }),\r\n    [state, actions, autoOpen],\r\n  );\r\n}\r\n\r\ninterface FilterChipContextValue {\r\n  filter: FilterValue;\r\n  field: FilterField;\r\n  size: FilterSize;\r\n  /** True when this chip was just added, so its value control opens itself. */\r\n  autoOpen: boolean;\r\n}\r\n\r\nconst FilterChipContext = React.createContext<FilterChipContextValue | null>(\r\n  null,\r\n);\r\n\r\nfunction useFilterChip(): FilterChipContextValue {\r\n  const context = React.use(FilterChipContext);\r\n  if (!context) {\r\n    throw new Error(\"useFilterChip must be used within a FilterChip.\");\r\n  }\r\n  return context;\r\n}\r\n\r\nexport {\r\n  FiltersStateContext,\r\n  FiltersActionsContext,\r\n  FiltersAutoOpenContext,\r\n  useFilters,\r\n  useFiltersState,\r\n  useFiltersActions,\r\n  useFiltersAutoOpen,\r\n  FilterChipContext,\r\n  useFilterChip,\r\n};\r\nexport type {\r\n  FiltersContextValue,\r\n  FiltersStateContextValue,\r\n  FiltersActionsContextValue,\r\n  FiltersAutoOpenContextValue,\r\n  FilterChipContextValue,\r\n};\r\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/filters/filters-context.tsx"
    },
    {
      "path": "registry/default/filters/filters-value-controls.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/registry/default/button/button\";\r\nimport {\r\n  Combobox,\r\n  ComboboxEmpty,\r\n  ComboboxInput,\r\n  ComboboxItem,\r\n  ComboboxList,\r\n  ComboboxPopup,\r\n  ComboboxTrigger,\r\n} from \"@/registry/default/combobox/combobox\";\r\nimport { Input } from \"@/registry/default/input/input\";\r\nimport { NumberField as BaseNumberField } from \"@base-ui/react/number-field\";\r\n\r\nimport { useFilterChip, useFiltersActions } from \"./filters-context\";\r\nimport {\r\n  asNumberOrNull,\r\n  asNumberRange,\r\n  asString,\r\n  asStringArray,\r\n  FILTER_SIZES,\r\n  operatorShapeFor,\r\n} from \"./lib/filters-utils\";\r\nimport type {\r\n  FilterField,\r\n  FilterOption,\r\n  FilterSize,\r\n  FilterValue,\r\n  MultiSelectFilterField,\r\n  NumberFilterField,\r\n  SelectFilterField,\r\n  TextFilterField,\r\n} from \"./lib/filters-types\";\r\n\r\ninterface ValueControlProps<F extends FilterField> {\r\n  field: F;\r\n  filter: FilterValue;\r\n  size: FilterSize;\r\n  autoOpen: boolean;\r\n  onValueChange: (value: unknown) => void;\r\n}\r\n\r\n/**\r\n * The value segment of a pill. Renders the control matching the field type,\r\n * or nothing when the operator's shape is `\"none\"` (`is empty`), so custom\r\n * chip compositions are correct without re-implementing that rule.\r\n */\r\nfunction FilterChipValue() {\r\n  const { field, filter, size, autoOpen } = useFilterChip();\r\n  const { updateFilter } = useFiltersActions();\r\n  const onValueChange = React.useCallback(\r\n    (nextValue: unknown) => updateFilter(filter.id, { value: nextValue }),\r\n    [updateFilter, filter.id],\r\n  );\r\n\r\n  if (operatorShapeFor(field, filter.operator) === \"none\") return null;\r\n\r\n  switch (field.type) {\r\n    case \"select\":\r\n    case \"multiselect\":\r\n      return (\r\n        <OptionsValueControl\r\n          field={field}\r\n          filter={filter}\r\n          size={size}\r\n          autoOpen={autoOpen}\r\n          onValueChange={onValueChange}\r\n        />\r\n      );\r\n    case \"text\":\r\n      return (\r\n        <TextValueControl\r\n          field={field}\r\n          filter={filter}\r\n          size={size}\r\n          autoOpen={autoOpen}\r\n          onValueChange={onValueChange}\r\n        />\r\n      );\r\n    case \"number\":\r\n      return (\r\n        <NumberValueControl\r\n          field={field}\r\n          filter={filter}\r\n          size={size}\r\n          autoOpen={autoOpen}\r\n          onValueChange={onValueChange}\r\n        />\r\n      );\r\n    case \"custom\":\r\n      return (\r\n        <div data-slot=\"filter-chip-value\" className=\"flex items-stretch\">\r\n          {field.renderValue({\r\n            value: filter.value,\r\n            operator: filter.operator,\r\n            onValueChange,\r\n            size,\r\n            field,\r\n          })}\r\n        </div>\r\n      );\r\n    default:\r\n      return null;\r\n  }\r\n}\r\n\r\n/** Accessible name for a value input, folding in string affixes (\"$\", \"hrs\"). */\r\nfunction valueAriaLabel(\r\n  field: TextFilterField | NumberFilterField,\r\n  part: string,\r\n): string {\r\n  const affixes = [field.prefix, field.suffix].filter(\r\n    (affix): affix is string => typeof affix === \"string\",\r\n  );\r\n  return [`${field.label} ${part}`, ...affixes].join(\" \");\r\n}\r\n\r\n// ----- Select / multiselect ----------------------------------------------\r\n\r\nconst VALUE_TRIGGER_CLASSES =\r\n  \"text-foreground data-popup-open:bg-surface-hover rounded-none font-normal focus-visible:-outline-offset-2\";\r\n\r\nfunction OptionContent({ option }: { option: FilterOption }) {\r\n  return (\r\n    <span className=\"flex items-center gap-2\">\r\n      {option.icon}\r\n      <span className=\"truncate\">{option.label}</span>\r\n    </span>\r\n  );\r\n}\r\n\r\ninterface OptionsTriggerProps {\r\n  size: FilterSize;\r\n  icon?: React.ReactNode;\r\n  text: string;\r\n  isPlaceholder: boolean;\r\n  /** Accessible name with field context, e.g. \"Status value: Done\". */\r\n  \"aria-label\": string;\r\n}\r\n\r\n/** Shared ghost trigger for the single- and multi-select value popups. */\r\nfunction OptionsTrigger({\r\n  size,\r\n  icon,\r\n  text,\r\n  isPlaceholder,\r\n  \"aria-label\": ariaLabel,\r\n}: OptionsTriggerProps) {\r\n  return (\r\n    <ComboboxTrigger\r\n      render={(triggerProps) => (\r\n        <Button\r\n          {...triggerProps}\r\n          data-slot=\"filter-chip-value\"\r\n          aria-label={ariaLabel}\r\n          variant=\"ghost\"\r\n          size={size}\r\n          className={VALUE_TRIGGER_CLASSES}\r\n        >\r\n          <span className=\"flex items-center gap-1.5\">\r\n            {icon}\r\n            <span\r\n              className={cn(\r\n                \"max-w-40 truncate\",\r\n                isPlaceholder && \"text-muted-foreground\",\r\n              )}\r\n            >\r\n              {text}\r\n            </span>\r\n          </span>\r\n        </Button>\r\n      )}\r\n    />\r\n  );\r\n}\r\n\r\n/**\r\n * The searchable popup shell shared by the value pickers and the add-filter\r\n * menu: bordered search header, empty state, and the option list.\r\n */\r\nfunction FilterSearchPopup({\r\n  placeholder,\r\n  empty,\r\n  className,\r\n  children,\r\n}: {\r\n  placeholder: string;\r\n  empty: React.ReactNode;\r\n  className?: string;\r\n  children: React.ComponentProps<typeof ComboboxList>[\"children\"];\r\n}) {\r\n  return (\r\n    <ComboboxPopup className={cn(\"flex min-w-52 flex-col p-0\", className)}>\r\n      <div className=\"border-border border-b p-2\">\r\n        <ComboboxInput\r\n          variant=\"elevated\"\r\n          placeholder={placeholder}\r\n          showTrigger={false}\r\n          showClear={false}\r\n        />\r\n      </div>\r\n      <ComboboxEmpty>{empty}</ComboboxEmpty>\r\n      <ComboboxList>{children}</ComboboxList>\r\n    </ComboboxPopup>\r\n  );\r\n}\r\n\r\nfunction OptionsPopup({\r\n  children,\r\n}: {\r\n  children: React.ComponentProps<typeof ComboboxList>[\"children\"];\r\n}) {\r\n  const { labels } = useFiltersActions();\r\n  return (\r\n    <FilterSearchPopup\r\n      placeholder={labels.searchValues}\r\n      empty={labels.noResults}\r\n    >\r\n      {children}\r\n    </FilterSearchPopup>\r\n  );\r\n}\r\n\r\n/** Value control for `select` and `multiselect` fields. */\r\nfunction OptionsValueControl({\r\n  field,\r\n  filter,\r\n  size,\r\n  autoOpen,\r\n  onValueChange,\r\n}: ValueControlProps<SelectFilterField | MultiSelectFilterField>) {\r\n  const { labels } = useFiltersActions();\r\n  // Capture once at mount so the uncontrolled open state never changes.\r\n  const [initialOpen] = React.useState(autoOpen);\r\n  const placeholder = field.placeholder ?? labels.selectValue;\r\n\r\n  if (field.type === \"multiselect\") {\r\n    const values = asStringArray(filter.value);\r\n    const selected = field.options.filter((option) =>\r\n      values.includes(option.value),\r\n    );\r\n    const atMax =\r\n      field.maxSelections != null && values.length >= field.maxSelections;\r\n    const display =\r\n      selected.length === 0\r\n        ? placeholder\r\n        : selected.length === 1\r\n          ? selected[0].label\r\n          : `${selected[0].label} +${selected.length - 1}`;\r\n\r\n    return (\r\n      <Combobox<FilterOption, true>\r\n        items={field.options}\r\n        multiple\r\n        value={selected}\r\n        defaultOpen={initialOpen}\r\n        onValueChange={(next) => {\r\n          // Reject selections past the cap; the controlled value snaps back.\r\n          if (\r\n            field.maxSelections != null &&\r\n            next.length > field.maxSelections\r\n          ) {\r\n            return;\r\n          }\r\n          onValueChange(next.map((option) => option.value));\r\n        }}\r\n        itemToStringLabel={(option) => option.label}\r\n      >\r\n        <OptionsTrigger\r\n          size={size}\r\n          icon={selected[0]?.icon}\r\n          text={display}\r\n          isPlaceholder={selected.length === 0}\r\n          aria-label={`${field.label} ${labels.value.toLowerCase()}: ${display}`}\r\n        />\r\n        <OptionsPopup>\r\n          {(option: FilterOption) => (\r\n            <ComboboxItem\r\n              key={option.value}\r\n              value={option}\r\n              disabled={atMax && !values.includes(option.value)}\r\n            >\r\n              <OptionContent option={option} />\r\n            </ComboboxItem>\r\n          )}\r\n        </OptionsPopup>\r\n      </Combobox>\r\n    );\r\n  }\r\n\r\n  const selected =\r\n    field.options.find((option) => option.value === filter.value) ?? null;\r\n\r\n  return (\r\n    <Combobox<FilterOption, false>\r\n      items={field.options}\r\n      value={selected}\r\n      defaultOpen={initialOpen}\r\n      onValueChange={(next) => onValueChange(next ? next.value : null)}\r\n      itemToStringLabel={(option) => option.label}\r\n    >\r\n      <OptionsTrigger\r\n        size={size}\r\n        icon={selected?.icon}\r\n        text={selected?.label ?? placeholder}\r\n        isPlaceholder={!selected}\r\n        aria-label={`${field.label} ${labels.value.toLowerCase()}: ${selected?.label ?? placeholder}`}\r\n      />\r\n      <OptionsPopup>\r\n        {(option: FilterOption) => (\r\n          <ComboboxItem key={option.value} value={option}>\r\n            <OptionContent option={option} />\r\n          </ComboboxItem>\r\n        )}\r\n      </OptionsPopup>\r\n    </Combobox>\r\n  );\r\n}\r\n\r\n// ----- Text / number ------------------------------------------------------\r\n\r\n/**\r\n * The value segment wrapper for inline inputs. Owns the `filter-chip-value`\r\n * slot and flanks the input with muted prefix/suffix text (e.g. `$`, `%`).\r\n */\r\nfunction ValueSegment({\r\n  prefix,\r\n  suffix,\r\n  children,\r\n}: {\r\n  prefix?: React.ReactNode;\r\n  suffix?: React.ReactNode;\r\n  children: React.ReactNode;\r\n}) {\r\n  return (\r\n    <div\r\n      data-slot=\"filter-chip-value\"\r\n      className={cn(\r\n        \"flex items-center\",\r\n        prefix && \"[&_input]:pl-1\",\r\n        suffix && \"[&_input]:pr-1\",\r\n      )}\r\n    >\r\n      {prefix ? (\r\n        <span className=\"text-muted-foreground pl-2.5 text-sm select-none\">\r\n          {prefix}\r\n        </span>\r\n      ) : null}\r\n      {children}\r\n      {suffix ? (\r\n        <span className=\"text-muted-foreground pr-2.5 text-sm select-none\">\r\n          {suffix}\r\n        </span>\r\n      ) : null}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction TextValueControl({\r\n  field,\r\n  filter,\r\n  size,\r\n  autoOpen,\r\n  onValueChange,\r\n}: ValueControlProps<TextFilterField>) {\r\n  const { labels } = useFiltersActions();\r\n  return (\r\n    <ValueSegment prefix={field.prefix} suffix={field.suffix}>\r\n      <Input\r\n        data-slot=\"filter-chip-value-input\"\r\n        type=\"text\"\r\n        autoFocus={autoOpen}\r\n        aria-label={valueAriaLabel(field, labels.value.toLowerCase())}\r\n        value={asString(filter.value)}\r\n        placeholder={field.placeholder ?? labels.enterValue}\r\n        size={FILTER_SIZES[size].input}\r\n        className={cn(\r\n          \"w-40 flex-none rounded-none border-0 bg-transparent shadow-none focus-visible:-outline-offset-2 dark:bg-transparent\",\r\n          FILTER_SIZES[size].height,\r\n        )}\r\n        onChange={(event) => onValueChange(event.target.value)}\r\n      />\r\n    </ValueSegment>\r\n  );\r\n}\r\n\r\nfunction NumberValueField({\r\n  value,\r\n  size,\r\n  step,\r\n  placeholder,\r\n  autoFocus,\r\n  prefix,\r\n  suffix,\r\n  \"aria-label\": ariaLabel,\r\n  onValueChange,\r\n}: {\r\n  value: number | null;\r\n  size: FilterSize;\r\n  step?: number;\r\n  placeholder?: string;\r\n  autoFocus?: boolean;\r\n  prefix?: React.ReactNode;\r\n  suffix?: React.ReactNode;\r\n  \"aria-label\": string;\r\n  onValueChange: (value: number | null) => void;\r\n}) {\r\n  // Base UI's NumberField.Input is a text input with numeric semantics (no\r\n  // native spinner) and handles parsing, arrow-key stepping, and clamping.\r\n  // `allowWheelScrub` lets the wheel adjust the value while the input is\r\n  // focused and hovered (it won't hijack ordinary page scrolling).\r\n  return (\r\n    <ValueSegment prefix={prefix} suffix={suffix}>\r\n      <BaseNumberField.Root\r\n        value={value}\r\n        step={step}\r\n        allowWheelScrub\r\n        onValueChange={(next) => onValueChange(next)}\r\n        className={cn(\"flex flex-none items-center\", FILTER_SIZES[size].height)}\r\n      >\r\n        <BaseNumberField.Input\r\n          data-slot=\"filter-chip-value-input\"\r\n          autoFocus={autoFocus}\r\n          aria-label={ariaLabel}\r\n          placeholder={placeholder}\r\n          className={cn(\r\n            \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground\",\r\n            \"h-full w-24 rounded-none border-0 bg-transparent px-2.5 text-base font-normal tabular-nums outline-none md:text-sm\",\r\n            \"focus-visible:outline-ring/50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid\",\r\n          )}\r\n        />\r\n      </BaseNumberField.Root>\r\n    </ValueSegment>\r\n  );\r\n}\r\n\r\nfunction NumberValueControl({\r\n  field,\r\n  filter,\r\n  size,\r\n  autoOpen,\r\n  onValueChange,\r\n}: ValueControlProps<NumberFilterField>) {\r\n  const { labels } = useFiltersActions();\r\n\r\n  if (operatorShapeFor(field, filter.operator) === \"range\") {\r\n    const range = asNumberRange(filter.value);\r\n    return (\r\n      <>\r\n        <NumberValueField\r\n          value={range.min}\r\n          size={size}\r\n          step={field.step}\r\n          placeholder={labels.min}\r\n          prefix={field.prefix}\r\n          suffix={field.suffix}\r\n          aria-label={valueAriaLabel(field, labels.min.toLowerCase())}\r\n          autoFocus={autoOpen}\r\n          onValueChange={(min) => onValueChange({ ...range, min })}\r\n        />\r\n        <NumberValueField\r\n          value={range.max}\r\n          size={size}\r\n          step={field.step}\r\n          placeholder={labels.max}\r\n          prefix={field.prefix}\r\n          suffix={field.suffix}\r\n          aria-label={valueAriaLabel(field, labels.max.toLowerCase())}\r\n          onValueChange={(max) => onValueChange({ ...range, max })}\r\n        />\r\n      </>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <NumberValueField\r\n      value={asNumberOrNull(filter.value)}\r\n      size={size}\r\n      step={field.step}\r\n      placeholder={field.placeholder ?? labels.value}\r\n      prefix={field.prefix}\r\n      suffix={field.suffix}\r\n      aria-label={valueAriaLabel(field, labels.value.toLowerCase())}\r\n      autoFocus={autoOpen}\r\n      onValueChange={onValueChange}\r\n    />\r\n  );\r\n}\r\n\r\nexport { FilterChipValue, FilterSearchPopup };\r\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/filters/filters-value-controls.tsx"
    }
  ],
  "type": "registry:ui"
}