{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filters",
  "title": "Filters",
  "description": "A filter bar of sentence-like chips with per-field operators and a two-step add menu.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/button",
    "@cubby-ui/dropdown-menu",
    "@cubby-ui/scroll-area",
    "@cubby-ui/text-morph",
    "@cubby-ui/tooltip",
    "@cubby-ui/kbd",
    "@cubby-ui/combobox",
    "@cubby-ui/use-controllable-state",
    "@cubby-ui/elevated",
    "@cubby-ui/popup-search"
  ],
  "files": [
    {
      "path": "registry/default/filters/filters.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/default/button/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n} from \"@/registry/default/dropdown-menu/dropdown-menu\";\nimport { ScrollArea } from \"@/registry/default/scroll-area/scroll-area\";\nimport { TextMorph } from \"@/registry/default/text-morph/text-morph\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/registry/default/tooltip/tooltip\";\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\";\n\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { Cancel01Icon, FilterRemoveIcon } from \"@hugeicons/core-free-icons\";\n\nimport {\n  FieldIcon,\n  FilterActionsAdd,\n  FilterAddButton,\n} from \"./filters-add-menu\";\nimport {\n  FilterChipContext,\n  FiltersActionsContext,\n  FiltersAnnouncementContext,\n  FiltersStateContext,\n  useFilterChip,\n  useFiltersActions,\n  useFiltersState,\n} from \"./filters-context\";\nimport { FilterChipValue } from \"./filters-value-controls\";\nimport {\n  CLICK_MORPH_MS,\n  describeFilter,\n  isFilterComplete,\n  operatorLabel,\n  FILTER_ENDS,\n  FILTER_SEGMENT,\n  FILTER_SEGMENT_INTERACTIVE,\n  FILTER_SEGMENT_SQUARE,\n  FILTER_SIZES,\n  FILTER_TINTS,\n  FILTER_VARIANTS,\n  patchFilter,\n  resolveOperators,\n} from \"./lib/filters-utils\";\nimport { useFlowPresence, useFlowRow } from \"./lib/flow-presence\";\nimport type {\n  FilterChipProps,\n  FiltersBarProps,\n  FiltersLabels,\n  FiltersOverflow,\n  FiltersProps,\n  FiltersProviderProps,\n  FilterValue,\n} from \"./lib/filters-types\";\n\nconst DEFAULT_LABELS: FiltersLabels = {\n  add: \"Filter\",\n  addTooltip: \"Add filter\",\n  clear: \"Clear filters\",\n  searchFields: \"Filter by...\",\n  searchFieldValues: (field) => `Search ${field.label.toLowerCase()}...`,\n  noFields: \"No matches.\",\n  noResults: \"No results found.\",\n  selectValue: \"Select...\",\n  enterValue: \"Enter value\",\n  value: \"Value\",\n  min: \"Min\",\n  max: \"Max\",\n  and: \"and\",\n  fields: \"Fields\",\n  values: \"Values\",\n  back: \"Back to fields\",\n  done: \"Done\",\n  invalidNumber: \"Enter a number\",\n  filtersCleared: \"Cleared all filters\",\n  operator: \"operator\",\n  operators: {},\n  activeCount: (count) => `${count} active`,\n  selectedCount: (count) => `${count} selected`,\n  removeFilter: (fieldLabel) => `Remove ${fieldLabel} filter`,\n  filterAdded: (description) => `Added ${description}`,\n  filterRemoved: (description) => `Removed ${description}`,\n};\n\nconst LABEL_KEYS = Object.keys(DEFAULT_LABELS) as (keyof FiltersLabels)[];\n\n/** Moves focus to the add button first: the chips and clear control are about to unmount. */\nfunction clearFromBar(from: HTMLElement, clearAll: () => void) {\n  from\n    .closest('[data-slot=\"filters\"]')\n    ?.querySelector<HTMLElement>('[data-slot=\"filter-add\"]')\n    ?.focus();\n  clearAll();\n}\n\n/** Refocuses a neighbouring chip (or the add button) before removal so focus never falls to `<body>`. */\nfunction focusAdjacentChip(chip: HTMLElement | null) {\n  const bar = chip?.closest<HTMLElement>('[data-slot=\"filters\"]');\n  if (!chip || !bar) return;\n  const chips = Array.from(\n    bar.querySelectorAll<HTMLElement>('[data-slot=\"filter-chip\"]'),\n  );\n  const index = chips.indexOf(chip);\n  const neighbor = chips[index + 1] ?? chips[index - 1];\n  const target =\n    neighbor?.querySelector<HTMLElement>('[data-slot=\"filter-chip-remove\"]') ??\n    bar.querySelector<HTMLElement>('[data-slot=\"filter-add\"]');\n  target?.focus();\n}\n\n/** Owns filter state with no layout; wrap a `FiltersBar` plus any external UI that reads `useFilters`. */\nfunction FiltersProvider({\n  fields,\n  value,\n  defaultValue = [],\n  onValueChange,\n  size = \"default\",\n  variant = \"outline\",\n  allowDuplicateFields = false,\n  labels: labelsProp,\n  children,\n}: FiltersProviderProps) {\n  const [filters, setFilters] = useControllableState<FilterValue[]>({\n    value,\n    defaultValue,\n    onValueChange,\n  });\n\n  const labels = React.useMemo(\n    () => ({ ...DEFAULT_LABELS, ...labelsProp }),\n    // Value-level deps so an inline `labels` object doesn't churn the actions\n    // context. Inline function labels still churn; hoist those.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    LABEL_KEYS.map((key) => labelsProp?.[key]),\n  );\n  const fieldsById = React.useMemo(\n    () => new Map(fields.map((field) => [field.id, field])),\n    [fields],\n  );\n  // Keyed by content (not the filters array identity) so the Set stays\n  // referentially stable while a filter's value is being typed.\n  const usedFieldsKey = filters\n    .map((filter) => filter.field)\n    .sort()\n    .join(\"\\u0000\");\n  const usedFieldIds = React.useMemo(\n    () => new Set(usedFieldsKey ? usedFieldsKey.split(\"\\u0000\") : []),\n    [usedFieldsKey],\n  );\n\n  // Adds/removes/clears are announced because focus lands elsewhere; edits\n  // aren't, since their own controls already say what changed.\n  const [announcement, setAnnouncement] = React.useState({\n    text: \"\",\n    count: 0,\n  });\n  const announce = React.useCallback(\n    (text: string) =>\n      setAnnouncement((previous) => ({ text, count: previous.count + 1 })),\n    [],\n  );\n  // The filters as of the last commit, for describing the one removed.\n  const filtersRef = React.useRef(filters);\n  React.useLayoutEffect(() => {\n    filtersRef.current = filters;\n  }, [filters]);\n  const describe = React.useCallback(\n    (filter: FilterValue | undefined) => {\n      const field = filter && fieldsById.get(filter.field);\n      return field && filter\n        ? describeFilter(field, filter, labels.operators)\n        : \"\";\n    },\n    [fieldsById, labels],\n  );\n\n  const addFilter = React.useCallback(\n    (filter: FilterValue) => {\n      setFilters((prev) => [...prev, filter]);\n      announce(labels.filterAdded(describe(filter)));\n    },\n    [setFilters, announce, labels, describe],\n  );\n  const removeFilter = React.useCallback(\n    (id: string) => {\n      const removed = filtersRef.current.find((filter) => filter.id === id);\n      setFilters((prev) => prev.filter((f) => f.id !== id));\n      if (removed) announce(labels.filterRemoved(describe(removed)));\n    },\n    [setFilters, announce, labels, describe],\n  );\n  const getFilters = React.useCallback(() => filtersRef.current, []);\n  const clearAll = React.useCallback(() => {\n    setFilters([]);\n    announce(labels.filtersCleared);\n  }, [setFilters, announce, labels]);\n  const updateFilter = React.useCallback(\n    (id: string, patch: Partial<Omit<FilterValue, \"id\">>) => {\n      setFilters((prev) =>\n        prev.map((filter) =>\n          filter.id === id\n            ? patchFilter(fieldsById.get(filter.field), filter, patch)\n            : filter,\n        ),\n      );\n    },\n    [setFilters, fieldsById],\n  );\n\n  // Split so useFiltersActions leaves don't re-render per keystroke.\n  const stateContext = React.useMemo(() => ({ filters }), [filters]);\n  const actionsContext = React.useMemo(\n    () => ({\n      fields,\n      size,\n      variant,\n      labels,\n      fieldsById,\n      usedFieldIds,\n      allowDuplicateFields,\n      addFilter,\n      updateFilter,\n      removeFilter,\n      clearAll,\n      getFilters,\n    }),\n    [\n      fields,\n      size,\n      variant,\n      labels,\n      fieldsById,\n      usedFieldIds,\n      allowDuplicateFields,\n      addFilter,\n      updateFilter,\n      removeFilter,\n      clearAll,\n      getFilters,\n    ],\n  );\n\n  return (\n    <FiltersStateContext.Provider value={stateContext}>\n      <FiltersActionsContext.Provider value={actionsContext}>\n        <FiltersAnnouncementContext value={announcement}>\n          {children}\n        </FiltersAnnouncementContext>\n      </FiltersActionsContext.Provider>\n    </FiltersStateContext.Provider>\n  );\n}\n\n/** The flex row; renders the default chips + actions unless `children` is passed. */\nfunction FiltersBar({\n  shortcut,\n  overflow = \"scroll\",\n  className,\n  children,\n  ref,\n  ...props\n}: FiltersBarProps) {\n  const rowRef = useFlowRow(ref);\n  return (\n    <FiltersOverflowContext value={overflow}>\n      {/* Shared so moving between the add and clear tooltips skips the delay. */}\n      <TooltipProvider>\n        <div\n          ref={rowRef}\n          data-slot=\"filters\"\n          data-overflow={overflow}\n          className={cn(\n            \"flex max-w-full items-center gap-2\",\n            overflow === \"wrap\" && \"flex-wrap\",\n            className,\n          )}\n          {...props}\n        >\n          {children ?? (\n            <>\n              <FilterChips />\n              <FilterActions shortcut={shortcut} />\n            </>\n          )}\n          <FiltersAnnouncer />\n        </div>\n      </TooltipProvider>\n    </FiltersOverflowContext>\n  );\n}\n\n// Always mounted so screen readers are listening before the first change; the\n// alternating nbsp makes a repeated message still count as a change.\nfunction FiltersAnnouncer() {\n  const { text, count } = React.use(FiltersAnnouncementContext);\n  return (\n    <span role=\"status\" className=\"sr-only\">\n      {text}\n      {count % 2 === 1 ? \"\\u00a0\" : \"\"}\n    </span>\n  );\n}\n\n/** `FiltersProvider` + `FiltersBar` in one component, for the common case. */\nfunction Filters({\n  fields,\n  value,\n  defaultValue,\n  onValueChange,\n  size,\n  variant,\n  allowDuplicateFields,\n  labels,\n  ...barProps\n}: FiltersProps) {\n  return (\n    <FiltersProvider\n      fields={fields}\n      value={value}\n      defaultValue={defaultValue}\n      onValueChange={onValueChange}\n      size={size}\n      variant={variant}\n      allowDuplicateFields={allowDuplicateFields}\n      labels={labels}\n    >\n      <FiltersBar {...barProps} />\n    </FiltersProvider>\n  );\n}\n\n/** The enclosing bar's overflow mode; chips outside a bar just lay out flat. */\nconst FiltersOverflowContext = React.createContext<FiltersOverflow>(\"wrap\");\n\n// Vertical wheel scrolls the strip sideways, but only while it can move that\n// way, so at either end the page scrolls as usual.\nfunction scrollSidewaysOnWheel(node: HTMLDivElement | null) {\n  if (!node) return;\n  const handleWheel = (event: WheelEvent) => {\n    if (event.ctrlKey || Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {\n      return;\n    }\n    const range = node.scrollWidth - node.clientWidth;\n    if (range <= 1) return;\n    // RTL scrolls from 0 towards negative values.\n    const direction = getComputedStyle(node).direction === \"rtl\" ? -1 : 1;\n    const position = node.scrollLeft * direction;\n    const delta = event.deltaY * (event.deltaMode === 1 ? 16 : 1);\n    if ((delta < 0 && position <= 0) || (delta > 0 && position >= range - 1)) {\n      return;\n    }\n    event.preventDefault();\n    node.scrollLeft += delta * direction;\n  };\n  node.addEventListener(\"wheel\", handleWheel, { passive: false });\n  return () => node.removeEventListener(\"wheel\", handleWheel);\n}\n\n/** A `FilterChip` per active filter; in a scrolling bar, inside a sideways-scrolling strip. */\nfunction FilterChips({\n  className,\n}: {\n  /** Merged into the scroll strip (a scrolling bar only). */\n  className?: string;\n}) {\n  const { filters } = useFiltersState();\n  const { fieldsById } = useFiltersActions();\n  const overflow = React.use(FiltersOverflowContext);\n  const chips = filters.map((filter) => {\n    const field = fieldsById.get(filter.field);\n    if (!field) return null;\n    return <FilterChip key={filter.id} filter={filter} field={field} />;\n  });\n\n  if (overflow === \"wrap\") return <>{chips}</>;\n\n  return (\n    <ScrollArea\n      nativeScroll\n      hideScrollbar\n      fadeEdges=\"x\"\n      viewportRef={scrollSidewaysOnWheel}\n      // Chips are tab stops and get scrolled into view, so the strip isn't one.\n      tabIndex={-1}\n      // 2px inset keeps chip borders off the clip line (fractional DPR shaves\n      // them); the negative margin cancels it. `empty:` hides the strip's gap.\n      className={cn(\n        \"-m-0.5 flex size-auto min-w-0 items-center gap-2 overflow-y-hidden rounded-none p-0.5 *:max-w-none empty:not-data-flow-busy:hidden\",\n        className,\n      )}\n    >\n      {chips}\n    </ScrollArea>\n  );\n}\n\n// Memoized so typing in one chip doesn't re-render the others (untouched\n// `filter` objects keep their identity; only the stable actions context is read).\nconst FilterChip = React.memo(function FilterChip({\n  filter,\n  field: fieldProp,\n  className,\n  children,\n  ref,\n  onKeyDown,\n  ...props\n}: FilterChipProps) {\n  const { size, variant, removeFilter, fieldsById, labels } =\n    useFiltersActions();\n  const presenceRef = useFlowPresence(ref);\n  const field = fieldProp ?? fieldsById.get(filter.field);\n\n  const chipContext = React.useMemo(\n    () => (field ? { filter, field, size } : null),\n    [filter, field, size],\n  );\n\n  if (!field || !chipContext) return null;\n\n  const incomplete = !isFilterComplete(field, filter);\n\n  return (\n    <FilterChipContext.Provider value={chipContext}>\n      <div\n        ref={presenceRef}\n        role=\"group\"\n        data-slot=\"filter-chip\"\n        data-size={size}\n        data-variant={variant}\n        data-filter-id={filter.id}\n        data-incomplete={incomplete ? \"\" : undefined}\n        aria-label={describeFilter(field, filter, labels.operators)}\n        className={cn(\n          \"text-foreground inline-flex max-w-full shrink-0 items-center rounded-lg\",\n          FILTER_VARIANTS[variant],\n          FILTER_ENDS,\n          FILTER_TINTS,\n          \"transition-[outline-color,border-color] duration-150\",\n          FILTER_SIZES[size],\n          className,\n        )}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented) return;\n          if (event.key !== \"Backspace\" && event.key !== \"Delete\") return;\n          const target = event.target;\n          // Remove button only: elsewhere the key means editing that part.\n          if (\n            !(target instanceof HTMLButtonElement) ||\n            target.dataset.slot !== \"filter-chip-remove\"\n          ) {\n            return;\n          }\n          event.preventDefault();\n          focusAdjacentChip(event.currentTarget);\n          removeFilter(filter.id);\n        }}\n        {...props}\n      >\n        {children ?? (\n          <>\n            <FilterChipField />\n            <FilterChipOperator />\n            <FilterChipValue />\n            <FilterChipRemove />\n          </>\n        )}\n      </div>\n    </FilterChipContext.Provider>\n  );\n});\n\nfunction FilterChipField({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  const { field } = useFilterChip();\n  return (\n    <span\n      data-slot=\"filter-chip-field\"\n      className={cn(\n        \"text-muted-foreground inline-flex h-full shrink-0 items-center gap-1.5 border border-y-2 border-transparent ps-(--seg-x) pe-1 whitespace-nowrap\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <FieldIcon>{field.icon}</FieldIcon>\n          {field.label}\n        </>\n      )}\n    </span>\n  );\n}\n\nfunction FilterChipOperator({\n  className,\n  ...props\n}: React.ComponentProps<\"button\">) {\n  const { filter, field } = useFilterChip();\n  const { updateFilter, labels } = useFiltersActions();\n  const operators = resolveOperators(field);\n  const current = operators.find((operator) => operator.id === filter.operator);\n  const label = current\n    ? operatorLabel(current, labels.operators)\n    : filter.operator;\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <button\n            type=\"button\"\n            data-slot=\"filter-chip-operator\"\n            aria-label={`${field.label} ${labels.operator}: ${label}`}\n            className={cn(\n              FILTER_SEGMENT,\n              FILTER_SEGMENT_INTERACTIVE,\n              \"text-muted-foreground hover:text-foreground data-popup-open:text-foreground px-[calc(var(--seg-x)*0.6)]\",\n              className,\n            )}\n            {...props}\n          />\n        }\n      >\n        <TextMorph value={label} split=\"word\" duration={CLICK_MORPH_MS} />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n        <DropdownMenuRadioGroup\n          value={filter.operator}\n          onValueChange={(next) => updateFilter(filter.id, { operator: next })}\n        >\n          {operators.map((operator) => (\n            <DropdownMenuRadioItem key={operator.id} value={operator.id}>\n              {operatorLabel(operator, labels.operators)}\n            </DropdownMenuRadioItem>\n          ))}\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction FilterChipRemove({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<\"button\">) {\n  const { filter, field } = useFilterChip();\n  const { removeFilter, labels } = useFiltersActions();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"filter-chip-remove\"\n      aria-label={labels.removeFilter(field.label)}\n      className={cn(\n        FILTER_SEGMENT,\n        FILTER_SEGMENT_INTERACTIVE,\n        \"text-muted-foreground hover:text-foreground [&_svg]:size-3.5\",\n        FILTER_SEGMENT_SQUARE,\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        focusAdjacentChip(\n          event.currentTarget.closest<HTMLElement>('[data-slot=\"filter-chip\"]'),\n        );\n        removeFilter(filter.id);\n      }}\n      {...props}\n    >\n      <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />\n    </button>\n  );\n}\n\n/** Clears every filter. Renders nothing while no filters are active. */\nfunction FilterClearButton({\n  className,\n  children,\n  ref,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { clearAll, labels, size } = useFiltersActions();\n  const { filters } = useFiltersState();\n  const presenceRef = useFlowPresence(ref);\n  if (filters.length === 0) return null;\n  return (\n    <Button\n      ref={presenceRef}\n      data-slot=\"filter-clear\"\n      variant=\"ghost\"\n      size={size}\n      className={cn(\"text-muted-foreground\", className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        clearFromBar(event.currentTarget, clearAll);\n      }}\n      {...props}\n    >\n      {children ?? labels.clear}\n    </Button>\n  );\n}\n\n/** The add and clear actions as one chip-like capsule. */\nfunction FilterActions({\n  shortcut,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** Key that opens the add menu from the keyboard (e.g. `\"f\"`). */\n  shortcut?: string;\n}) {\n  const { size, variant } = useFiltersActions();\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"filter-actions\"\n      data-size={size}\n      data-variant={variant}\n      className={cn(\n        // No gap: a folded clear segment would still get one; it brings its own.\n        \"text-foreground inline-flex shrink-0 items-center rounded-lg\",\n        FILTER_VARIANTS[variant],\n        FILTER_ENDS,\n        FILTER_TINTS,\n        FILTER_SIZES[size],\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <FilterActionsAdd shortcut={shortcut} />\n          <FilterActionsClear />\n        </>\n      )}\n    </div>\n  );\n}\n\n/** The capsule's clear segment; stays mounted, folded and inert while no filters exist. */\nfunction FilterActionsClear({\n  className,\n  children,\n  onClick,\n  ...props\n}: React.ComponentProps<\"button\">) {\n  const { clearAll, labels, usedFieldIds } = useFiltersActions();\n  const active = usedFieldIds.size > 0;\n  return (\n    <div\n      data-slot=\"filter-clear-segment\"\n      data-active={active ? \"\" : undefined}\n      inert={!active}\n      className=\"grid h-full grid-cols-[minmax(0,0fr)] overflow-hidden opacity-0 transition-[grid-template-columns,opacity] duration-220 ease-[cubic-bezier(0.22,1,0.36,1)] data-active:grid-cols-[minmax(0,1fr)] data-active:opacity-100 motion-reduce:transition-none\"\n    >\n      <div className=\"flex h-full min-w-0 items-center\">\n        {/* Air and line are all borders, so both sides snap alike. */}\n        <span\n          aria-hidden\n          className=\"after:border-s-border pointer-events-none flex shrink-0 self-stretch border-e-[3px] border-transparent before:border-s-[3px] before:border-transparent before:content-[''] after:h-5 after:self-center after:rounded-full after:border-s-[1.5px] after:border-transparent after:content-['']\"\n        />\n        <Tooltip>\n          <TooltipTrigger\n            render={\n              <button\n                type=\"button\"\n                data-slot=\"filter-clear\"\n                aria-label={labels.clear}\n                className={cn(\n                  FILTER_SEGMENT,\n                  FILTER_SEGMENT_INTERACTIVE,\n                  \"text-muted-foreground hover:text-foreground [&_svg]:size-4\",\n                  FILTER_SEGMENT_SQUARE,\n                  className,\n                )}\n                {...props}\n                onClick={(event) => {\n                  onClick?.(event);\n                  if (event.defaultPrevented) return;\n                  clearFromBar(event.currentTarget, clearAll);\n                }}\n              />\n            }\n          >\n            {children ?? (\n              <HugeiconsIcon icon={FilterRemoveIcon} strokeWidth={2} />\n            )}\n          </TooltipTrigger>\n          <TooltipContent>{labels.clear}</TooltipContent>\n        </Tooltip>\n      </div>\n    </div>\n  );\n}\n\n/** Muted \"3 active\" text counting complete filters only; renders nothing at zero. */\nfunction FilterActiveCount({\n  className,\n  ref,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  const { filters } = useFiltersState();\n  const { labels, fieldsById } = useFiltersActions();\n  const presenceRef = useFlowPresence(ref);\n  // Skips filters whose field is gone (a stale URL, say).\n  const count = filters.filter((filter) => {\n    const field = fieldsById.get(filter.field);\n    return field !== undefined && isFilterComplete(field, filter);\n  }).length;\n  if (count === 0) return null;\n  return (\n    <span\n      ref={presenceRef}\n      data-slot=\"filter-active-count\"\n      className={cn(\n        \"text-muted-foreground shrink-0 text-sm whitespace-nowrap tabular-nums\",\n        className,\n      )}\n      {...props}\n    >\n      <TextMorph value={labels.activeCount(count)} duration={CLICK_MORPH_MS} />\n    </span>\n  );\n}\n\nexport {\n  Filters,\n  FiltersProvider,\n  FiltersBar,\n  FilterChips,\n  FilterChip,\n  FilterChipField,\n  FilterChipOperator,\n  FilterChipValue,\n  FilterChipRemove,\n  FilterAddButton,\n  FilterActions,\n  FilterActionsAdd,\n  FilterActionsClear,\n  FilterClearButton,\n  FilterActiveCount,\n};\nexport {\n  useFilters,\n  useFiltersState,\n  useFiltersActions,\n  useFilterChip,\n} from \"./filters-context\";\nexport {\n  allowsDuplicates,\n  isOptionField,\n  nameFromPlaceholder,\n  createFilter,\n  patchFilter,\n  resolveOperators,\n  scalarOperatorFor,\n  withOption,\n  defaultOperatorsFor,\n  operatorShape,\n  operatorShapeFor,\n  emptyValueFor,\n  formatFilterValue,\n  describeFilter,\n  isFilterComplete,\n  operatorLabel,\n  asFilterValues,\n  FILTER_SEGMENT,\n  FILTER_SEGMENT_INTERACTIVE,\n} from \"./lib/filters-utils\";\nexport type {\n  FilterField,\n  FilterFieldType,\n  FilterOption,\n  FilterOperator,\n  FilterOperatorShape,\n  FilterValue,\n  FilterSize,\n  FilterVariant,\n  FiltersLabels,\n  FiltersOverflow,\n  FiltersProps,\n  FiltersProviderProps,\n  FiltersBarProps,\n  FilterChipProps,\n  FilterValueControlProps,\n  NumberRange,\n  SelectFilterField,\n  MultiSelectFilterField,\n  TextFilterField,\n  NumberFilterField,\n  CustomFilterField,\n} from \"./lib/filters-types\";\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\";\n\n/** Size of the filter bar and its chips. */\nexport type FilterSize = \"sm\" | \"default\" | \"lg\";\n\n/** Shell style for chips and the add/clear capsule. */\nexport type FilterVariant = \"outline\" | \"soft\" | \"elevated\";\n\n/** Built-in field types. `custom` renders its own value control. */\nexport type FilterFieldType =\n  | \"select\"\n  | \"multiselect\"\n  | \"text\"\n  | \"number\"\n  | \"custom\";\n\n/** A selectable option for `select` / `multiselect` fields. */\nexport interface FilterOption {\n  value: string;\n  label: string;\n  icon?: React.ReactNode;\n}\n\n/** Value shape an operator expects: `\"none\"` (no value), `\"scalar\"` (one control), `\"range\"` (number min/max). */\nexport type FilterOperatorShape = \"none\" | \"scalar\" | \"range\";\n\n/** An operator shown in the middle segment of a pill (`is`, `contains`, ...). */\nexport interface FilterOperator {\n  id: string;\n  label: string;\n  /** Value shape this operator expects. Defaults to `\"scalar\"`. */\n  shape?: FilterOperatorShape;\n  /** Sugar for `shape: \"none\"`, e.g. `is empty` / `is not empty`. */\n  valueless?: boolean;\n}\n\ninterface FilterFieldBase {\n  /** Stable key stored on each filter as `FilterValue.field`. */\n  id: string;\n  label: string;\n  icon?: React.ReactNode;\n  /** Override the default operators for this field's type. */\n  operators?: FilterOperator[];\n  /** Hide specific default operators by id. */\n  disabledOperators?: string[];\n}\n\n/**\n * Fields whose filters can repeat: text, number and custom. Option fields\n * (select, multiselect) always keep one filter, which the add menu edits.\n */\ninterface RepeatableFieldBase extends FilterFieldBase {\n  /**\n   * Lets this field have more than one filter, e.g. \"contains\" one word and\n   * \"does not contain\" another. Defaults to the bar's `allowDuplicateFields`.\n   */\n  allowDuplicates?: boolean;\n}\n\nexport interface SelectFilterField extends FilterFieldBase {\n  type: \"select\";\n  options: FilterOption[];\n  placeholder?: string;\n}\n\nexport interface MultiSelectFilterField extends FilterFieldBase {\n  type: \"multiselect\";\n  options: FilterOption[];\n  placeholder?: string;\n  /** Maximum number of options that can be selected. */\n  maxSelections?: number;\n}\n\nexport interface TextFilterField extends RepeatableFieldBase {\n  type: \"text\";\n  placeholder?: string;\n  /** Content shown before the input, e.g. `@` or an icon. */\n  prefix?: React.ReactNode;\n  /** Content shown after the input, e.g. a unit label. */\n  suffix?: React.ReactNode;\n}\n\nexport interface NumberFilterField extends RepeatableFieldBase {\n  type: \"number\";\n  placeholder?: string;\n  step?: number;\n  /** Content shown before the input, e.g. `$`. */\n  prefix?: React.ReactNode;\n  /** Content shown after the input, e.g. `%` or `hrs`. */\n  suffix?: React.ReactNode;\n}\n\nexport interface CustomFilterField extends RepeatableFieldBase {\n  type: \"custom\";\n  /** Renders the value segment and reports changes back to the filter. */\n  renderValue: (props: FilterValueControlProps) => React.ReactNode;\n  /** Seed value for a fresh filter of this field. */\n  defaultValue?: unknown;\n  /**\n   * Value as text, for the accessible name and completeness (`\"\"` = no value).\n   * Defaults handle only strings, numbers and booleans; required for objects.\n   */\n  formatValue?: (value: unknown) => string;\n}\n\nexport type FilterField =\n  | SelectFilterField\n  | MultiSelectFilterField\n  | TextFilterField\n  | NumberFilterField\n  | CustomFilterField;\n\n/** Props passed to a `custom` field's `renderValue`. */\nexport interface FilterValueControlProps {\n  value: unknown;\n  operator: string;\n  onValueChange: (value: unknown) => void;\n  size: FilterSize;\n  field: CustomFilterField;\n}\n\n/** Value shape for a `number` field when the operator is `between`. */\nexport interface NumberRange {\n  min: number | null;\n  max: number | null;\n}\n\n/**\n * A single active filter. `value` by field type: `select → string | null`,\n * `multiselect → string[]`, `text → string`,\n * `number → number | null | NumberRange`, `custom → unknown`.\n */\nexport interface FilterValue {\n  id: string;\n  field: string;\n  operator: string;\n  value: unknown;\n}\n\n/** Copy overrides for the bar's chrome. */\nexport interface FiltersLabels {\n  add: string;\n  /** Tooltip on the add button once it has folded down to its icon. */\n  addTooltip: string;\n  clear: string;\n  searchFields: string;\n  /** Placeholder of a field's value search (add menu and chip), e.g. \"Search labels...\". */\n  searchFieldValues: (field: FilterField) => string;\n  noFields: string;\n  noResults: string;\n  selectValue: string;\n  enterValue: string;\n  value: string;\n  min: string;\n  max: string;\n  /** Joins the two bounds of a range, as in \"between 2 and 8\". */\n  and: string;\n  /** Group heading for fields in the add menu while searching. */\n  fields: string;\n  /** Group heading for matching values in the add menu while searching. */\n  values: string;\n  /** @deprecated The add menu has no back button. Kept so existing overrides type-check. */\n  back: string;\n  /** Closes the add menu after picking several values of a multiselect. */\n  done: string;\n  /** Hint under a number the add menu can't read, e.g. \"12a\". */\n  invalidNumber: string;\n  /** Announced to screen readers when every filter is cleared. */\n  filtersCleared: string;\n  /** Word used in the operator trigger's accessible name. */\n  operator: string;\n  /** Operator label overrides by id, e.g. `{ is_not: \"n'est pas\" }`. */\n  operators: Partial<Record<string, string>>;\n  /** The active-filter count, e.g. \"3 active\". */\n  activeCount: (count: number) => string;\n  /** Summary for a multiselect value with two or more options picked. */\n  selectedCount: (count: number, field: FilterField) => string;\n  /** Builds the accessible label for a pill's remove button. */\n  removeFilter: (fieldLabel: string) => string;\n  /** Announced when a filter is added; gets e.g. \"Status is Done\". */\n  filterAdded: (description: string) => string;\n  /** Announced when a filter is removed; gets e.g. \"Status is Done\". */\n  filterRemoved: (description: string) => string;\n}\n\nexport interface FiltersProviderProps {\n  /** Field definitions the bar can filter on. */\n  fields: FilterField[];\n  /** Controlled list of active filters. */\n  value?: FilterValue[];\n  /** Initial filters in uncontrolled mode. */\n  defaultValue?: FilterValue[];\n  onValueChange?: (value: FilterValue[]) => void;\n  size?: FilterSize;\n  /** Shell style for chips and the add/clear capsule. Defaults to `\"outline\"`. */\n  variant?: FilterVariant;\n  /**\n   * Default for each text, number and custom field's `allowDuplicates`.\n   * Option fields always keep one filter. Defaults to `false`.\n   */\n  allowDuplicateFields?: boolean;\n  labels?: Partial<FiltersLabels>;\n  children?: React.ReactNode;\n}\n\nexport interface FiltersBarProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\" | \"defaultValue\"\n> {\n  /** Key that opens the add-filter menu, forwarded to the default `FilterAddButton`. */\n  shortcut?: string;\n  /** Chip overflow: `\"scroll\"` (default) scrolls one row behind edge fades; `\"wrap\"` adds rows. */\n  overflow?: FiltersOverflow;\n}\n\n/** How a bar handles more chips than fit on one row. */\nexport type FiltersOverflow = \"scroll\" | \"wrap\";\n\nexport interface FiltersProps\n  extends Omit<FiltersProviderProps, \"children\">, FiltersBarProps {}\n\nexport interface FilterChipProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\"\n> {\n  filter: FilterValue;\n  /** Field definition. Resolved from the `Filters` context when omitted. */\n  field?: FilterField;\n}\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/filters/lib/filters-types.ts"
    },
    {
      "path": "registry/default/filters/lib/filters-utils.ts",
      "content": "import { cn } from \"@/lib/utils\";\nimport { solidSurface } from \"@/registry/default/lib/elevated\";\n\nimport type {\n  FilterField,\n  FilterFieldType,\n  FilterOperator,\n  FilterOperatorShape,\n  FilterSize,\n  FilterValue,\n  FilterVariant,\n  MultiSelectFilterField,\n  NumberRange,\n  SelectFilterField,\n} from \"./filters-types\";\n\n/** TextMorph duration for click-driven labels; snappier than the default 240ms. */\nexport const CLICK_MORPH_MS = 209;\n\n/**\n * Shared segment classes. Insets are segment borders, 2px facing the chip's\n * edge and 1px facing a neighbour (FILTER_ENDS sets the ends). The plate is\n * ::before, so it scales on press while the label holds still; its corners\n * are the chip's minus 2px. Hover is instant; the press tint still fades.\n */\nexport const FILTER_SEGMENT = cn(\n  \"relative isolate inline-flex h-full shrink-0 items-center gap-1.5 whitespace-nowrap border border-y-2 border-transparent bg-clip-padding rounded-lg px-(--seg-x) outline-0 outline-transparent outline-solid transition-[background-color,color,outline-color] duration-100 ease-out [transition-duration:100ms,0ms,100ms] focus-visible:before:outline-2 focus-visible:before:-outline-offset-2 focus-visible:before:outline-ring/50\",\n  // A square hit area over the whole box, so hover never drops between\n  // segments.\n  \"after:absolute after:-inset-x-px after:-inset-y-0.5 after:content-['']\",\n  \"before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-lg)-2px)] before:content-[''] before:transition-[background-color,scale] before:duration-100 before:ease-out before:[transition-duration:0ms,100ms] active:before:[transition-duration:100ms]\",\n);\n\n/**\n * Ends of a chip or the actions capsule get the 2px border. `of` counts\n * segments only, so Base UI's guard spans can't move them. In the capsule,\n * add is the end until clear unfolds, which add reads from its sibling to\n * keep `:has()` cheap.\n */\nexport const FILTER_ENDS = cn(\n  \"[&>:nth-child(1_of_[data-slot^=filter-chip-])]:border-s-2 [&>:nth-last-child(1_of_[data-slot^=filter-chip-])]:border-e-2\",\n  // With clear unfolded, add and clear reach to the separator's line.\n  \"[&_[data-slot=filter-add]:has(~[data-slot=filter-clear-segment][data-active])]:after:-end-[5px] [&_[data-slot=filter-clear]]:after:-start-[5px]\",\n  \"[&_[data-slot=filter-add]]:border-s-2 [&_[data-slot=filter-clear-segment][data-active]_[data-slot=filter-clear]]:border-e-2 [&_[data-slot=filter-add]:not(:has(~[data-slot=filter-clear-segment][data-active]))]:border-e-2\",\n);\n\n/** Icon-only segments: the plate (content box) is square. */\nexport const FILTER_SEGMENT_SQUARE =\n  \"box-content aspect-square h-auto self-stretch justify-center px-0\";\n\n/**\n * Hover and press as in Button Group: the label color at 10% / 14%, and the\n * plate scales to 0.98. Menu triggers (aria-haspopup) skip the press and hold\n * the hover plate while open.\n */\nexport const FILTER_SEGMENT_INTERACTIVE =\n  \"cursor-pointer hover:before:bg-(--filter-highlight) active:not-aria-[haspopup]:before:bg-(--filter-press) active:not-aria-[haspopup]:before:scale-[0.98] data-popup-open:before:bg-(--filter-highlight) motion-reduce:active:before:scale-100\";\n\n/**\n * Shell classes per variant. The chip has no border; its segments carry the\n * inset. Outline draws its line just outside, and an incomplete filter\n * dashes it. `--filter-chip-bg` lets stacked option icons ring in the fill.\n */\nexport const FILTER_VARIANTS: Record<FilterVariant, string> = {\n  outline:\n    \"bg-card outline outline-border data-incomplete:outline-dashed [--filter-chip-bg:var(--card)]\",\n  soft: \"bg-secondary data-incomplete:outline data-incomplete:outline-border data-incomplete:outline-dashed [--filter-chip-bg:var(--secondary)]\",\n  elevated: cn(\n    solidSurface(3, 3),\n    \"[--filter-chip-bg:var(--surface-3)]\",\n    // Incomplete: the drop shadow stays; a dashed outline takes the ring's place.\n    \"data-incomplete:shadow-[0_1px_1px_-0.5px_var(--surface-shadow-near),0_3px_3px_-1.5px_var(--surface-shadow-mid)] data-incomplete:outline data-incomplete:outline-border data-incomplete:outline-dashed\",\n  ),\n};\n\n/** Tints for segment hover and press, set on each shell. */\nexport const FILTER_TINTS =\n  \"[--filter-highlight:color-mix(in_oklab,var(--foreground)_10%,transparent)] [--filter-press:color-mix(in_oklab,var(--foreground)_14%,transparent)]\";\n\n/**\n * Per-size chip classes; heights match the `Button` ramp. `--seg-x` is segment\n * side padding; `--seg-icon-x` squares an icon-only segment:\n * (height - 2*2px segment border - 16px icon) / 2.\n */\nexport const FILTER_SIZES: Record<FilterSize, string> = {\n  sm: \"h-9 sm:not-pointer-coarse:h-8 [--seg-icon-x:8px] sm:not-pointer-coarse:[--seg-icon-x:6px] text-sm [--seg-x:0.5rem] [&_[data-slot=filter-chip-field]_svg]:size-3.5\",\n  default:\n    \"h-10 sm:not-pointer-coarse:h-9 [--seg-icon-x:10px] sm:not-pointer-coarse:[--seg-icon-x:8px] text-sm [--seg-x:0.625rem] [&_[data-slot=filter-chip-field]_svg]:size-3.5\",\n  lg: \"h-11 sm:not-pointer-coarse:h-10 [--seg-icon-x:12px] sm:not-pointer-coarse:[--seg-icon-x:10px] text-base [--seg-x:0.75rem] [&_[data-slot=filter-chip-field]_svg]:size-4\",\n};\n\n// `FilterValue.value` is `unknown` and may come from untrusted input (URL\n// state, `JSON.parse`), so reads go through these coercers, never a cast.\n\nexport function asString(value: unknown): string {\n  return typeof value === \"string\" ? value : \"\";\n}\n\nexport function asStringArray(value: unknown): string[] {\n  return Array.isArray(value)\n    ? value.filter((item): item is string => typeof item === \"string\")\n    : [];\n}\n\nexport function asNumberOrNull(value: unknown): number | null {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nexport function asNumberRange(value: unknown): NumberRange {\n  if (typeof value === \"object\" && value !== null) {\n    const candidate = value as Partial<Record<\"min\" | \"max\", unknown>>;\n    return {\n      min: asNumberOrNull(candidate.min),\n      max: asNumberOrNull(candidate.max),\n    };\n  }\n  return { min: null, max: null };\n}\n\n/** Coerces unknown JSON into `FilterValue[]`, dropping entries without string `id`/`field`/`operator`. */\nexport function asFilterValues(value: unknown): FilterValue[] {\n  if (!Array.isArray(value)) return [];\n  return value\n    .filter(\n      (item): item is Record<string, unknown> =>\n        typeof item === \"object\" && item !== null,\n    )\n    .filter(\n      (item) =>\n        typeof item.id === \"string\" &&\n        typeof item.field === \"string\" &&\n        typeof item.operator === \"string\",\n    )\n    .map((item) => ({\n      id: item.id as string,\n      field: item.field as string,\n      operator: item.operator as string,\n      value: item.value,\n    }));\n}\n\nconst OPERATOR_LABELS: Record<string, string> = {\n  is: \"is\",\n  is_not: \"is not\",\n  is_empty: \"is empty\",\n  is_not_empty: \"is not empty\",\n  is_any_of: \"is any of\",\n  is_not_any_of: \"is not any of\",\n  includes_all: \"includes all\",\n  contains: \"contains\",\n  not_contains: \"does not contain\",\n  starts_with: \"starts with\",\n  ends_with: \"ends with\",\n  eq: \"=\",\n  neq: \"≠\",\n  gt: \">\",\n  lt: \"<\",\n  between: \"between\",\n};\n\nfunction op(id: string, shape: FilterOperatorShape = \"scalar\"): FilterOperator {\n  return { id, label: OPERATOR_LABELS[id] ?? id, shape };\n}\n\n/** The default operator set for a field type, in display order. */\nexport function defaultOperatorsFor(type: FilterFieldType): FilterOperator[] {\n  switch (type) {\n    case \"select\":\n      return [\n        op(\"is\"),\n        op(\"is_not\"),\n        op(\"is_empty\", \"none\"),\n        op(\"is_not_empty\", \"none\"),\n      ];\n    case \"multiselect\":\n      return [\n        op(\"is_any_of\"),\n        op(\"is_not_any_of\"),\n        op(\"includes_all\"),\n        op(\"is_empty\", \"none\"),\n      ];\n    case \"text\":\n      return [\n        op(\"contains\"),\n        op(\"not_contains\"),\n        op(\"starts_with\"),\n        op(\"ends_with\"),\n        op(\"is\"),\n        op(\"is_empty\", \"none\"),\n      ];\n    case \"number\":\n      return [op(\"eq\"), op(\"neq\"), op(\"gt\"), op(\"lt\"), op(\"between\", \"range\")];\n    case \"custom\":\n    default:\n      return [op(\"is\")];\n  }\n}\n\n/** Resolves the operators available for a field, honoring overrides. */\nexport function resolveOperators(field: FilterField): FilterOperator[] {\n  const base = field.operators ?? defaultOperatorsFor(field.type);\n  if (!field.disabledOperators?.length) return base;\n  const disabled = new Set(field.disabledOperators);\n  return base.filter((operator) => !disabled.has(operator.id));\n}\n\n/** The value shape an operator declares (`valueless` is sugar for `\"none\"`). */\nexport function operatorShape(operator: FilterOperator): FilterOperatorShape {\n  return operator.shape ?? (operator.valueless ? \"none\" : \"scalar\");\n}\n\n/** Resolves the value shape for a field's operator by id. */\nexport function operatorShapeFor(\n  field: FilterField,\n  operatorId: string,\n): FilterOperatorShape {\n  const operator = resolveOperators(field).find((o) => o.id === operatorId);\n  return operator ? operatorShape(operator) : \"scalar\";\n}\n\n/**\n * Whether `field` can have more than one filter. Option fields never can (one\n * chip holds every value); others use `allowDuplicates` or the bar's default.\n */\nexport function allowsDuplicates(\n  field: FilterField,\n  barDefault: boolean,\n): boolean {\n  if (isOptionField(field)) return false;\n  return field.allowDuplicates ?? barDefault;\n}\n\n/** A search field's accessible name: its placeholder without the trailing \"...\" or \"…\". */\nexport function nameFromPlaceholder(placeholder: string): string {\n  return placeholder.replace(/\\s*(\\.{3}|…)$/, \"\");\n}\n\n/** Whether `field` picks from a list of options (select or multiselect). */\nexport function isOptionField(\n  field: FilterField,\n): field is SelectFilterField | MultiSelectFilterField {\n  return field.type === \"select\" || field.type === \"multiselect\";\n}\n\n/** A typed empty value for a fresh filter of `field` with `operatorId`. */\nexport function emptyValueFor(field: FilterField, operatorId: string): unknown {\n  const shape = operatorShapeFor(field, operatorId);\n  if (shape === \"none\") return null;\n  if (shape === \"range\") {\n    return { min: null, max: null } satisfies NumberRange;\n  }\n  if (field.type === \"multiselect\") return [] as string[];\n  if (field.type === \"text\") return \"\";\n  if (field.type === \"custom\") return field.defaultValue ?? null;\n  return null;\n}\n\nfunction generateId(): string {\n  // `crypto.randomUUID` is absent in non-secure contexts (plain-HTTP LAN dev);\n  // ids only need list-key uniqueness.\n  if (\n    typeof crypto !== \"undefined\" &&\n    typeof crypto.randomUUID === \"function\"\n  ) {\n    return crypto.randomUUID();\n  }\n  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/** Creates a `FilterValue`, defaulting to the field's first operator and a typed empty value. */\nexport function createFilter(\n  field: FilterField,\n  partial?: Partial<Omit<FilterValue, \"field\">>,\n): FilterValue {\n  const operators = resolveOperators(field);\n  const operator = partial?.operator ?? operators[0]?.id ?? \"is\";\n  return {\n    id: partial?.id ?? generateId(),\n    field: field.id,\n    operator,\n    value:\n      partial && \"value\" in partial\n        ? partial.value\n        : emptyValueFor(field, operator),\n  };\n}\n\n/** Applies a patch; an operator change to a different value shape reseeds the value. */\nexport function patchFilter(\n  field: FilterField | undefined,\n  filter: FilterValue,\n  patch: Partial<Omit<FilterValue, \"id\">>,\n): FilterValue {\n  const next: FilterValue = { ...filter, ...patch };\n  const operatorChanged =\n    patch.operator !== undefined && patch.operator !== filter.operator;\n  if (\n    operatorChanged &&\n    !(\"value\" in patch) &&\n    field &&\n    operatorShapeFor(field, filter.operator) !==\n      operatorShapeFor(field, next.operator)\n  ) {\n    next.value = emptyValueFor(field, next.operator);\n  }\n  return next;\n}\n\n/** Human-readable value, shared by the visible controls and the aria summary so they match. */\nexport function formatFilterValue(\n  field: FilterField,\n  filter: FilterValue,\n): string {\n  const shape = operatorShapeFor(field, filter.operator);\n  if (shape === \"none\") return \"\";\n  if (shape === \"range\") {\n    const { min, max } = asNumberRange(filter.value);\n    if (min === null && max === null) return \"\";\n    if (min === null) return `up to ${max}`;\n    if (max === null) return `from ${min}`;\n    return `${min} to ${max}`;\n  }\n  switch (field.type) {\n    case \"select\":\n      return (\n        field.options.find((option) => option.value === filter.value)?.label ??\n        \"\"\n      );\n    case \"multiselect\": {\n      const values = asStringArray(filter.value);\n      return field.options\n        .filter((option) => values.includes(option.value))\n        .map((option) => option.label)\n        .join(\", \");\n    }\n    case \"text\":\n      return asString(filter.value);\n    case \"number\": {\n      const numeric = asNumberOrNull(filter.value);\n      return numeric === null ? \"\" : String(numeric);\n    }\n    case \"custom\": {\n      if (field.formatValue) return field.formatValue(filter.value);\n      const value = filter.value;\n      return typeof value === \"string\" ||\n        typeof value === \"number\" ||\n        typeof value === \"boolean\"\n        ? String(value)\n        : \"\";\n    }\n    default:\n      return \"\";\n  }\n}\n\n/** The field's first single-value operator, so a value picked in the add menu has somewhere to go. */\nexport function scalarOperatorFor(\n  field: FilterField,\n): FilterOperator | undefined {\n  return resolveOperators(field).find(\n    (operator) => operatorShape(operator) === \"scalar\",\n  );\n}\n\n/** Folds a picked option into the value: appended (once) for multiselect, else replaces it. */\nexport function withOption(\n  field: FilterField,\n  current: unknown,\n  optionValue: string,\n): unknown {\n  if (field.type !== \"multiselect\") return optionValue;\n  const values = asStringArray(current);\n  if (values.includes(optionValue)) return values;\n  if (field.maxSelections != null && values.length >= field.maxSelections) {\n    return values;\n  }\n  return [...values, optionValue];\n}\n\n/** Display name: the `labels.operators` translation, else the operator's own `label`. */\nexport function operatorLabel(\n  operator: FilterOperator,\n  names?: Partial<Record<string, string>>,\n): string {\n  return names?.[operator.id] ?? operator.label;\n}\n\n/** Whether a filter narrows anything yet (valueless operator, or a value set). */\nexport function isFilterComplete(\n  field: FilterField,\n  filter: FilterValue,\n): boolean {\n  return (\n    operatorShapeFor(field, filter.operator) === \"none\" ||\n    formatFilterValue(field, filter) !== \"\"\n  );\n}\n\n/** Plain-language summary for screen readers; `operatorNames` is `labels.operators`. */\nexport function describeFilter(\n  field: FilterField,\n  filter: FilterValue,\n  operatorNames?: Partial<Record<string, string>>,\n): string {\n  const operator = resolveOperators(field).find(\n    (candidate) => candidate.id === filter.operator,\n  );\n  const operatorText = operator\n    ? operatorLabel(operator, operatorNames)\n    : filter.operator;\n  const summary = formatFilterValue(field, filter);\n  return summary\n    ? `${field.label} ${operatorText} ${summary}`\n    : `${field.label} ${operatorText}`;\n}\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/filters/lib/filters-utils.ts"
    },
    {
      "path": "registry/default/filters/lib/flow-presence.ts",
      "content": "import * as React from \"react\";\n\n// Chips enter and leave by easing a negative `margin-inline-end` (their width\n// plus the row gap) so neighbours slide instead of jumping. It is real layout,\n// so it wraps and retargets without measuring the siblings.\n\n// One clock for opening and closing; only the exit fade is quicker, so\n// removal feels immediate.\nconst FLOW_MS = 220;\nconst EXIT_FADE_MS = 130;\nconst EASE = \"cubic-bezier(0.22, 1, 0.36, 1)\";\nconst ANIMATION_ID = \"filters-flow\";\n\n/** Marks the bar ready once its first render has painted. */\nconst READY_ATTR = \"data-flow-ready\";\n\nfunction prefersReducedMotion(): boolean {\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** Exit copies to drop if their element remounts (Strict Mode), before it measures the row. */\nconst pendingGhosts = new WeakMap<HTMLElement, HTMLElement>();\n\n/** Fractional width: a rounded `offsetWidth` leaves a sub-pixel snap when motion ends. */\nfunction widthOf(el: HTMLElement): number {\n  return el.getBoundingClientRect().width;\n}\n\n/**\n * Holds the element at its current width and returns the undo. Chips are\n * `max-w-full`, so a shrink-to-fit row narrowing as space closes would shrink\n * the chip and break a margin measured from its full width.\n */\nfunction pinWidth(el: HTMLElement): () => void {\n  const previous = el.style.maxWidth;\n  el.style.maxWidth = `${widthOf(el)}px`;\n  // Safe unconditionally: a replacing entrance releases the old pin first.\n  return () => {\n    el.style.maxWidth = previous;\n  };\n}\n\nfunction isLiveFlowItem(node: Element): node is HTMLElement {\n  return (\n    node instanceof HTMLElement &&\n    !node.hasAttribute(\"data-flow-ghost\") &&\n    ![\"absolute\", \"fixed\"].includes(getComputedStyle(node).position)\n  );\n}\n\nfunction columnGap(el: Element | null): number {\n  return el ? parseFloat(getComputedStyle(el).columnGap) || 0 : 0;\n}\n\n/**\n * Width plus the gap that goes with it. A lone item has no gap, which also\n * keeps a cleared row from closing one gap more than it had.\n */\nfunction flowSpan(el: HTMLElement): number {\n  return widthOf(el) + (isAlone(el) ? 0 : columnGap(el.parentElement));\n}\n\nfunction isAlone(el: HTMLElement): boolean {\n  const parent = el.parentElement;\n  return (\n    !parent ||\n    !Array.from(parent.children).some(\n      (node) => node !== el && isLiveFlowItem(node),\n    )\n  );\n}\n\nfunction asScroller(row: HTMLElement | null): HTMLElement | null {\n  if (!row) return null;\n  const { overflowX } = getComputedStyle(row);\n  return overflowX === \"auto\" || overflowX === \"scroll\" ? row : null;\n}\n\nfunction inlineScroller(el: HTMLElement): HTMLElement | null {\n  return asScroller(el.parentElement);\n}\n\nfunction overflows(scroller: HTMLElement): boolean {\n  return scroller.scrollWidth > scroller.clientWidth + 1;\n}\n\n/** Scrolls the element's sideways-scrolling parent until it clears the edge fades. */\nfunction revealInScroller(el: HTMLElement): void {\n  const scroller = inlineScroller(el);\n  if (!scroller || !overflows(scroller)) return;\n  // Matches the edge fade's depth: min(12%, 2.5rem).\n  const inset = Math.min(scroller.clientWidth * 0.12, 40);\n  const bounds = scroller.getBoundingClientRect();\n  const rect = el.getBoundingClientRect();\n  const delta =\n    rect.right > bounds.right - inset\n      ? rect.right - (bounds.right - inset)\n      : rect.left < bounds.left + inset\n        ? rect.left - (bounds.left + inset)\n        : 0;\n  if (Math.abs(delta) < 1) return;\n  scroller.scrollBy({\n    left: delta,\n    behavior: prefersReducedMotion() ? \"auto\" : \"smooth\",\n  });\n}\n\nfunction trailingFlow(el: HTMLElement): HTMLElement[] {\n  const nodes = [el];\n  for (let node = el.nextElementSibling; node; node = node.nextElementSibling) {\n    if (isLiveFlowItem(node)) nodes.push(node);\n  }\n  return nodes;\n}\n\n/**\n * Whether `span` of space at `el` would move anything to another row. Easing\n * across a wrap makes neighbours hop lines mid-motion, so those fade instead.\n */\nfunction crossesRows(el: HTMLElement, span: number): boolean {\n  const nodes = trailingFlow(el);\n  const rows = nodes.map((node) => node.offsetTop);\n  const unpin = pinWidth(el);\n  const previous = el.style.marginInlineEnd;\n  el.style.marginInlineEnd = `${-span}px`;\n  const moved = nodes.some((node, index) => node.offsetTop !== rows[index]);\n  el.style.marginInlineEnd = previous;\n  unpin();\n  return moved;\n}\n\n/**\n * Entrance pin undos, released synchronously on replacement: the async cancel\n * event would otherwise fire after the new pin and remove it.\n */\nconst entrancePins = new WeakMap<HTMLElement, () => void>();\n\nfunction enterInFlow(el: HTMLElement): void {\n  pendingGhosts.get(el)?.remove();\n  pendingGhosts.delete(el);\n  for (const animation of el.getAnimations()) {\n    if (animation.id !== ANIMATION_ID) continue;\n    animation.onfinish = null;\n    animation.oncancel = null;\n    animation.cancel();\n  }\n  entrancePins.get(el)?.();\n  entrancePins.delete(el);\n  if (prefersReducedMotion()) {\n    const fade = el.animate({ opacity: [0, 1] }, { duration: 150 });\n    fade.id = ANIMATION_ID;\n    return;\n  }\n  // An overflowing scroller has nothing in view to slide; fade and reveal.\n  const scroller = inlineScroller(el);\n  if (scroller && overflows(scroller)) {\n    const animation = el.animate(\n      {\n        opacity: [0, 1],\n      },\n      { duration: FLOW_MS, easing: EASE },\n    );\n    animation.id = ANIMATION_ID;\n    revealInScroller(el);\n    return;\n  }\n  // The first chip into a hidden empty strip also brings back its outer gap.\n  if (scroller) {\n    // Start from the strip as it is now if it was still narrowing (undo a clear).\n    cancelAnimations(scroller, SCROLLER_WIDTH_ID);\n    holdFade(scroller);\n    if (isAlone(el)) animateOuterGap(scroller, \"open\");\n  }\n  const fullSpan = flowSpan(el);\n  const span = crossesRows(el, fullSpan) ? 0 : fullSpan;\n  const unpin = pinWidth(el);\n  entrancePins.set(el, unpin);\n  const animation = el.animate(\n    { marginInlineEnd: [`${-span}px`, \"0px\"] },\n    { duration: FLOW_MS, easing: EASE },\n  );\n  animation.id = ANIMATION_ID;\n  // Gentler curve than the space, so little of the chip shows over its neighbour.\n  el.animate(\n    { opacity: [0, 1] },\n    { duration: FLOW_MS, easing: \"ease-out\" },\n  ).id = ANIMATION_ID;\n  animation.onfinish = () => {\n    unpin();\n    if (entrancePins.get(el) === unpin) entrancePins.delete(el);\n    revealInScroller(el);\n  };\n  animation.oncancel = () => {\n    unpin();\n    if (entrancePins.get(el) === unpin) entrancePins.delete(el);\n  };\n}\n\nconst fadeHolds = new WeakMap<HTMLElement, number>();\n\n/**\n * Turns a strip's edge fade off during motion: the moving chip's full width\n * counts toward scroll width early, so a fitting strip reads as overflowing.\n * Restored once every hold has run out.\n */\nfunction holdFade(scroller: HTMLElement) {\n  const holds = fadeHolds.get(scroller) ?? 0;\n  fadeHolds.set(scroller, holds + 1);\n  if (holds === 0) {\n    scroller.style.maskImage = \"none\";\n    scroller.style.setProperty(\"-webkit-mask-image\", \"none\");\n  }\n  setTimeout(() => {\n    const left = (fadeHolds.get(scroller) ?? 1) - 1;\n    fadeHolds.set(scroller, left);\n    if (left > 0) return;\n    scroller.style.removeProperty(\"mask-image\");\n    scroller.style.removeProperty(\"-webkit-mask-image\");\n  }, FLOW_MS);\n}\n\n/** Keeps an emptied strip shown while its exit is still settling. */\nconst BUSY_ATTR = \"data-flow-busy\";\n\nconst SCROLLER_GAP_ID = \"filters-flow-gap\";\nconst SCROLLER_WIDTH_ID = \"filters-flow-width\";\n\n/**\n * Eases the strip's outer gap (present only while it has chips) via a negative\n * margin. Closing holds its end state until `settleScroller` releases it.\n */\nfunction animateOuterGap(scroller: HTMLElement, direction: \"open\" | \"close\") {\n  for (const animation of scroller.getAnimations()) {\n    if (animation.id === SCROLLER_GAP_ID) animation.cancel();\n  }\n  // The resting margin isn't necessarily zero.\n  const rest = parseFloat(getComputedStyle(scroller).marginInlineEnd) || 0;\n  const open = `${rest}px`;\n  const closed = `${rest - columnGap(scroller.parentElement)}px`;\n  scroller.animate(\n    {\n      marginInlineEnd: direction === \"open\" ? [closed, open] : [open, closed],\n    },\n    {\n      duration: FLOW_MS,\n      easing: EASE,\n      fill: direction === \"close\" ? \"forwards\" : \"none\",\n    },\n  ).id = SCROLLER_GAP_ID;\n}\n\nfunction cancelAnimations(el: HTMLElement, id: string) {\n  for (const animation of el.getAnimations()) {\n    if (animation.id === id) animation.cancel();\n  }\n}\n\n/**\n * In-flight exit batches per strip. Cleanup waits for the last, or an earlier\n * batch's cleanup would cut a later batch's motion short.\n */\nconst scrollerExits = new WeakMap<HTMLElement, number>();\n\nfunction settleScroller(scroller: HTMLElement) {\n  const left = (scrollerExits.get(scroller) ?? 1) - 1;\n  scrollerExits.set(scroller, left);\n  if (left > 0) return;\n  scroller.removeAttribute(BUSY_ATTR);\n  cancelAnimations(scroller, SCROLLER_GAP_ID);\n  cancelAnimations(scroller, SCROLLER_WIDTH_ID);\n}\n\ninterface Exit {\n  el: HTMLElement;\n  ghost: HTMLElement;\n  span: number;\n  crosses: boolean;\n  /** Progress of an interrupted entrance. */\n  opacity: number;\n  margin: number;\n}\n\n/** Exits from one row in the same commit, played together. */\nconst exitBatches = new Map<\n  HTMLElement,\n  { exits: Exit[]; width: number; overflowed: boolean }\n>();\n\n/**\n * Leaves an inert copy of `el` in its place (just before React removes it)\n * and closes its space. One commit's copies play together a microtask later.\n */\nfunction exitInFlow(el: HTMLElement): void {\n  const parent = el.parentElement;\n  if (!parent || prefersReducedMotion()) return;\n  // Copies carry no animations, so capture an interrupted entrance's progress\n  // (or the exit snaps), then cancel it so measurements see resting layout.\n  const current = getComputedStyle(el);\n  const opacity = parseFloat(current.opacity);\n  const margin = parseFloat(current.marginInlineEnd) || 0;\n  for (const animation of el.getAnimations()) {\n    if (animation.id !== ANIMATION_ID) continue;\n    animation.onfinish = null;\n    animation.oncancel = null;\n    animation.cancel();\n  }\n  entrancePins.get(el)?.();\n  entrancePins.delete(el);\n  const span = flowSpan(el);\n  const crosses = crossesRows(el, span);\n  const ghost = el.cloneNode(true) as HTMLElement;\n  ghost.removeAttribute(\"id\");\n  for (const node of ghost.querySelectorAll(\"[id]\")) node.removeAttribute(\"id\");\n  // A distinct slot so lookups for live chips skip the copy.\n  ghost.setAttribute(\"data-slot\", `${el.dataset.slot ?? \"flow\"}-ghost`);\n  ghost.setAttribute(\"data-flow-ghost\", \"\");\n  ghost.inert = true;\n  // Fixed width, for the reason `pinWidth` gives.\n  Object.assign(ghost.style, {\n    width: `${widthOf(el)}px`,\n    maxWidth: \"none\",\n  });\n\n  let batch = exitBatches.get(parent);\n  if (!batch) {\n    const created = {\n      exits: [] as Exit[],\n      width: widthOf(parent),\n      overflowed: overflows(parent),\n    };\n    exitBatches.set(parent, created);\n    queueMicrotask(() => {\n      exitBatches.delete(parent);\n      playExits(parent, created);\n    });\n    batch = created;\n  }\n  el.before(ghost);\n  pendingGhosts.set(el, ghost);\n  batch.exits.push({ el, ghost, span, crosses, opacity, margin });\n}\n\nfunction liftOut(ghost: HTMLElement) {\n  const { offsetLeft, offsetTop } = ghost;\n  Object.assign(ghost.style, {\n    position: \"absolute\",\n    left: `${offsetLeft}px`,\n    top: `${offsetTop}px`,\n    margin: \"0px\",\n  });\n}\n\nfunction fadeOut({ ghost, opacity }: Exit): Animation {\n  return ghost.animate(\n    {\n      opacity: [opacity, 0],\n    },\n    { duration: EXIT_FADE_MS, easing: EASE, fill: \"forwards\" },\n  );\n}\n\nfunction playExits(\n  parent: HTMLElement,\n  batch: { exits: Exit[]; width: number; overflowed: boolean },\n) {\n  const { width, overflowed } = batch;\n  const exits = batch.exits.filter(({ el, ghost }) => {\n    if (pendingGhosts.get(el) === ghost) pendingGhosts.delete(el);\n    // Still attached: Strict Mode's rehearsal. Row gone: nothing to show.\n    if (el.isConnected || !parent.isConnected) {\n      ghost.remove();\n      return false;\n    }\n    return true;\n  });\n  if (exits.length === 0) return;\n\n  const scroller = asScroller(parent);\n  const emptying =\n    scroller !== null && !Array.from(parent.children).some(isLiveFlowItem);\n\n  // The strip's final width. Stop an earlier batch's width motion first, or\n  // it would be measured as the destination.\n  let settledWidth = width;\n  if (scroller) {\n    cancelAnimations(scroller, SCROLLER_WIDTH_ID);\n    scrollerExits.set(scroller, (scrollerExits.get(scroller) ?? 0) + 1);\n    // Hide every copy, earlier batches' too, or one still collapsing counts.\n    const ghosts = Array.from(\n      scroller.querySelectorAll<HTMLElement>(\":scope > [data-flow-ghost]\"),\n    );\n    for (const ghost of ghosts) ghost.style.display = \"none\";\n    settledWidth = emptying ? 0 : widthOf(scroller);\n    for (const ghost of ghosts) ghost.style.display = \"\";\n  }\n\n  if (scroller && overflowed && settledWidth !== width) {\n    // Closing space inside an overflowing strip happens mostly out of view,\n    // then the bar jumps. Fade copies in place and ease the strip's width.\n    scroller.style.position ||= \"relative\";\n    for (const exit of exits) liftOut(exit.ghost);\n    scroller.animate(\n      { width: [`${width}px`, `${settledWidth}px`] },\n      { duration: FLOW_MS, easing: EASE },\n    ).id = SCROLLER_WIDTH_ID;\n    if (emptying) animateOuterGap(scroller, \"close\");\n    // A lifted copy still widens the scroll range (keeping the edge fade on),\n    // so each goes once faded; busy keeps an emptied strip shown meanwhile.\n    scroller.setAttribute(BUSY_ATTR, \"\");\n    // It ends up fitting, so drop the fade now rather than mid-motion.\n    holdFade(scroller);\n    for (const exit of exits) {\n      fadeOut(exit).finished.then(\n        () => exit.ghost.remove(),\n        () => exit.ghost.remove(),\n      );\n    }\n  } else {\n    // A fitting strip only looks overflowing while space closes.\n    if (scroller && !overflowed) holdFade(scroller);\n    if (emptying && scroller) animateOuterGap(scroller, \"close\");\n    for (const exit of exits) {\n      if (exit.crosses) {\n        liftOut(exit.ghost);\n      } else {\n        collapse(exit);\n      }\n      fadeOut(exit);\n    }\n  }\n\n  // Copies stay until the space closes, so an emptied strip doesn't hide mid-motion.\n  setTimeout(() => {\n    for (const { ghost } of exits) ghost.remove();\n    if (scroller) settleScroller(scroller);\n  }, FLOW_MS);\n}\n\n/**\n * Shrinks a copy's box to nothing and pulls it over the preceding gap. A\n * negative margin alone leaves the box in the scroll range, so the edge fade\n * would linger and the range snap on removal.\n */\nfunction collapse({ ghost, span, margin }: Exit) {\n  const styles = getComputedStyle(ghost);\n  const box = widthOf(ghost);\n  const gap = span - box;\n  ghost.style.overflow = \"hidden\";\n  ghost.animate(\n    {\n      width: [`${box}px`, \"0px\"],\n      paddingInline: [\n        `${styles.paddingInlineStart} ${styles.paddingInlineEnd}`,\n        \"0px\",\n      ],\n      borderInlineWidth: [\n        `${styles.borderInlineStartWidth} ${styles.borderInlineEndWidth}`,\n        \"0px\",\n      ],\n      marginInlineStart: [\"0px\", `${-Math.max(0, gap)}px`],\n      // Non-zero if it left mid-entrance.\n      marginInlineEnd: [`${margin}px`, \"0px\"],\n    },\n    { duration: FLOW_MS, easing: EASE, fill: \"forwards\" },\n  );\n}\n\nfunction assignRef<T>(ref: React.Ref<T> | undefined, value: T | null): void {\n  if (typeof ref === \"function\") ref(value);\n  else if (ref) ref.current = value;\n}\n\n/**\n * Callback ref that animates the element into and out of its flex row; only\n * mounts after the row's first paint animate in. Kept stable (forwarded ref\n * synced separately), or an inline caller ref would replay exit and entrance\n * every render.\n */\nfunction useFlowPresence<T extends HTMLElement>(\n  forwardedRef?: React.Ref<T>,\n): React.RefCallback<T> {\n  const nodeRef = React.useRef<T | null>(null);\n  const forwarded = React.useRef(forwardedRef);\n  React.useLayoutEffect(() => {\n    if (forwarded.current === forwardedRef) return;\n    assignRef(forwarded.current, null);\n    forwarded.current = forwardedRef;\n    assignRef(forwardedRef, nodeRef.current);\n  });\n  return React.useCallback((node: T | null) => {\n    nodeRef.current = node;\n    assignRef(forwarded.current, node);\n    if (!node) return;\n    const row = node.closest(`[${READY_ATTR}]`);\n    if (row) enterInFlow(node);\n    return () => {\n      nodeRef.current = null;\n      assignRef(forwarded.current, null);\n      exitInFlow(node);\n    };\n  }, []);\n}\n\n/** Callback ref for the row: marks it ready after first paint so later children animate in. */\nfunction useFlowRow<T extends HTMLElement>(\n  forwardedRef?: React.Ref<T>,\n): React.RefCallback<T> {\n  return React.useCallback(\n    (node: T | null) => {\n      assignRef(forwardedRef, node);\n      if (!node) return;\n      const frame = requestAnimationFrame(() =>\n        node.setAttribute(READY_ATTR, \"\"),\n      );\n      return () => {\n        cancelAnimationFrame(frame);\n        assignRef(forwardedRef, null);\n      };\n    },\n    [forwardedRef],\n  );\n}\n\nexport { revealInScroller, useFlowPresence, useFlowRow };\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/filters/lib/flow-presence.ts"
    },
    {
      "path": "registry/default/filters/filters-add-menu.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Menu as BaseMenu } from \"@base-ui/react/menu\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/default/button/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuEmpty,\n  DropdownMenuFilter,\n  DropdownMenuGroup,\n  DropdownMenuGroupLabel,\n  DropdownMenuInput,\n  DropdownMenuItem,\n  DropdownMenuList,\n  DropdownMenuPopup,\n  DropdownMenuPortal,\n  DropdownMenuPositioner,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/registry/default/dropdown-menu/dropdown-menu\";\nimport { Kbd } from \"@/registry/default/kbd/kbd\";\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\";\nimport {\n  POPUP_SEARCH_FIELD,\n  POPUP_SEARCH_INPUT,\n  POPUP_SEARCH_MENU_FIELD,\n} from \"@/registry/default/lib/popup-search\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/registry/default/tooltip/tooltip\";\n\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { PlusSignIcon } from \"@hugeicons/core-free-icons\";\n\nimport { useFiltersActions, useFiltersState } from \"./filters-context\";\nimport { revealInScroller } from \"./lib/flow-presence\";\nimport {\n  allowsDuplicates,\n  asStringArray,\n  createFilter,\n  isOptionField,\n  nameFromPlaceholder,\n  operatorLabel,\n  FILTER_SEGMENT,\n  FILTER_SEGMENT_INTERACTIVE,\n  operatorShapeFor,\n  scalarOperatorFor,\n} from \"./lib/filters-utils\";\nimport type {\n  FilterField,\n  FilterOption,\n  MultiSelectFilterField,\n  NumberFilterField,\n  SelectFilterField,\n  TextFilterField,\n} from \"./lib/filters-types\";\n\ntype OptionsField = SelectFilterField | MultiSelectFilterField;\n\nfunction FieldIcon({\n  className,\n  children,\n}: {\n  className?: string;\n  children?: React.ReactNode;\n}) {\n  if (!children) return null;\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"text-muted-foreground flex shrink-0 items-center [&_svg]:size-3.5\",\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\nfunction OptionContent({ option }: { option: FilterOption }) {\n  return (\n    <span className=\"flex min-w-0 items-center gap-2.5\">\n      {option.icon}\n      <span className=\"truncate\">{option.label}</span>\n    </span>\n  );\n}\n\nfunction hasValueStep(field: FilterField): boolean {\n  return field.type !== \"custom\" && scalarOperatorFor(field) !== undefined;\n}\n\n/** An option field with a value step: its submenu edits its one chip. */\nfunction editsInPlace(field: FilterField): field is OptionsField {\n  return isOptionField(field) && hasValueStep(field);\n}\n\nconst DECIMAL = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/;\n\n// Submenu positioners don't glide: submenus switch as the pointer sweeps down\n// the list, and each sits beside its own row.\nconst SUBMENU_POSITIONER_STATIC = \"transition-none\";\n\n/** Closes the menu, optionally handing focus to a newly added filter. */\ntype OnDone = (focusFilterId?: string) => void;\n\ninterface SubmenuProps<F extends FilterField> {\n  field: F;\n  disabled: boolean;\n  onDone: OnDone;\n}\n\ninterface OptionFilter {\n  /** The field's chip, if it has one. */\n  filterId: string | undefined;\n  /** The checked option values. */\n  values: string[];\n  /** Adds the chip, edits it, or removes it once nothing is checked. */\n  write: (next: string[]) => void;\n  /** Whether a multiselect has reached its `maxSelections`. */\n  atMax: boolean;\n}\n\n/**\n * An option field's one filter and how to write it, shared by the submenus and\n * search so they agree on what is checked.\n */\nfunction useOptionFilter(field: OptionsField): OptionFilter {\n  const { addFilter, updateFilter, removeFilter } = useFiltersActions();\n  const { filters } = useFiltersState();\n  const filter = filters.find((candidate) => candidate.field === field.id);\n  // An \"is empty\" chip holds no value; writing one switches it to \"is\".\n  const valueless =\n    filter !== undefined && operatorShapeFor(field, filter.operator) === \"none\";\n  const values =\n    filter === undefined || valueless\n      ? []\n      : field.type === \"multiselect\"\n        ? asStringArray(filter.value)\n        : typeof filter.value === \"string\"\n          ? [filter.value]\n          : [];\n\n  const write = (next: string[]) => {\n    const scalar = scalarOperatorFor(field);\n    if (!scalar) return;\n    const value = field.type === \"multiselect\" ? next : next[0];\n    if (!filter) {\n      if (next.length > 0) {\n        addFilter(createFilter(field, { operator: scalar.id, value }));\n      }\n      return;\n    }\n    if (next.length === 0) {\n      removeFilter(filter.id);\n      return;\n    }\n    updateFilter(filter.id, {\n      ...(valueless && { operator: scalar.id }),\n      value,\n    });\n  };\n\n  const atMax =\n    field.type === \"multiselect\" &&\n    field.maxSelections != null &&\n    values.length >= field.maxSelections;\n\n  return { filterId: filter?.id, values, write, atMax };\n}\n\nfunction toggled(values: string[], value: string, checked: boolean): string[] {\n  return checked\n    ? [...values, value]\n    : values.filter((existing) => existing !== value);\n}\n\nfunction ValueSubmenuTrigger({\n  field,\n  disabled,\n}: {\n  field: FilterField;\n  disabled: boolean;\n}) {\n  return (\n    // Submenus have no exit animation: without the delay a fast sweep closes\n    // the old one before the next renders, leaving a blank frame. Opening one\n    // closes its sibling at once, so two never stay open.\n    <DropdownMenuSubTrigger\n      disabled={disabled}\n      label={field.label}\n      closeDelay={1}\n    >\n      <FieldIcon>{field.icon}</FieldIcon>\n      <span className=\"truncate\">{field.label}</span>\n    </DropdownMenuSubTrigger>\n  );\n}\n\n// A field's searchable value submenu. Memoised so a filter change skips the\n// text and number submenus; option submenus read filter state regardless.\nconst ValueSubmenu = React.memo(function ValueSubmenu(\n  props: SubmenuProps<FilterField>,\n) {\n  const { field } = props;\n  switch (field.type) {\n    case \"select\":\n    case \"multiselect\":\n      return <OptionsSubmenu {...props} field={field} />;\n    case \"text\":\n    case \"number\":\n      return <InputSubmenu {...props} field={field} />;\n    default:\n      return null;\n  }\n});\n\n// An option field's values as menu items: radios for a single choice,\n// checkboxes for several. A single choice closes the menu on any pick;\n// checkboxes close it only in search, which jumps straight to a value.\nfunction OptionItems({\n  field,\n  inSearch = false,\n  trailing,\n  onEdited,\n}: {\n  field: OptionsField;\n  inSearch?: boolean;\n  /** Shown at the end of each row, e.g. the field's name in search. */\n  trailing?: React.ReactNode;\n  /** Called with the chip's id when a pick changed an existing chip. */\n  onEdited?: (filterId: string) => void;\n}): React.ReactNode {\n  const { filterId, values, write, atMax } = useOptionFilter(field);\n  const pick = (next: string[]) => {\n    write(next);\n    if (filterId) onEdited?.(filterId);\n  };\n\n  if (field.type === \"select\") {\n    return (\n      <DropdownMenuRadioGroup\n        value={values[0] ?? null}\n        onValueChange={(value: string) => pick([value])}\n      >\n        {field.options.map((option) => (\n          <DropdownMenuRadioItem\n            key={option.value}\n            value={option.value}\n            label={option.label}\n            closeOnClick\n          >\n            <OptionContent option={option} />\n            {trailing}\n          </DropdownMenuRadioItem>\n        ))}\n      </DropdownMenuRadioGroup>\n    );\n  }\n\n  return field.options.map((option) => {\n    const checked = values.includes(option.value);\n    return (\n      <DropdownMenuCheckboxItem\n        key={option.value}\n        label={option.label}\n        checked={checked}\n        disabled={atMax && !checked}\n        closeOnClick={inSearch}\n        onCheckedChange={(next) => pick(toggled(values, option.value, next))}\n      >\n        <OptionContent option={option} />\n        {trailing}\n      </DropdownMenuCheckboxItem>\n    );\n  });\n}\n\n// A select or multiselect field's searchable submenu. Multiselect adds a\n// footer with the count and a Done button, since its checks keep it open.\nfunction OptionsSubmenu({\n  field,\n  disabled,\n  onDone,\n}: SubmenuProps<OptionsField>): React.ReactElement {\n  const { labels } = useFiltersActions();\n  const { values } = useOptionFilter(field);\n\n  return (\n    <DropdownMenuFilter>\n      <DropdownMenuSub>\n        <ValueSubmenuTrigger field={field} disabled={disabled} />\n        <DropdownMenuSubContent\n          className=\"w-60\"\n          viewportClassName=\"p-0\"\n          positionerClassName={SUBMENU_POSITIONER_STATIC}\n        >\n          <DropdownMenuInput\n            aria-label={nameFromPlaceholder(labels.searchFieldValues(field))}\n            placeholder={labels.searchFieldValues(field)}\n          />\n          <DropdownMenuEmpty>{labels.noResults}</DropdownMenuEmpty>\n          <DropdownMenuList>\n            <OptionItems field={field} />\n          </DropdownMenuList>\n          {field.type === \"multiselect\" && (\n            // Edge to edge; the list's own 4px padding sits above it. The same\n            // 4px gutter sizes the band, and the count lines up with the labels.\n            <div className=\"flex items-center justify-between gap-2 border-t p-1 ps-3.5\">\n              <span className=\"text-muted-foreground text-xs tabular-nums\">\n                {values.length > 0\n                  ? labels.selectedCount(values.length, field)\n                  : \"\"}\n              </span>\n              <Button variant=\"ghost\" size=\"xs\" onClick={() => onDone()}>\n                {labels.done}\n              </Button>\n            </div>\n          )}\n        </DropdownMenuSubContent>\n      </DropdownMenuSub>\n    </DropdownMenuFilter>\n  );\n}\n\n// The value field, then the filter it would add as a row. Matching is off\n// since the field holds a value, and the row always renders so the submenu\n// keeps its height while typing.\nfunction InputSubmenu({\n  field,\n  disabled,\n  onDone,\n}: SubmenuProps<TextFilterField | NumberFilterField>) {\n  const { labels, addFilter } = useFiltersActions();\n  const [text, setText] = React.useState(\"\");\n  const inputRef = React.useRef<HTMLInputElement | null>(null);\n  const hintId = React.useId();\n  const operator = scalarOperatorFor(field);\n  const isNumber = field.type === \"number\";\n  const decimal = text.trim().replace(\",\", \".\");\n  // Plain decimals only: `Number` would also take \"0x10\" or \"1e3\".\n  const parsed = DECIMAL.test(decimal) ? Number(decimal) : NaN;\n  const valid = text.trim() !== \"\" && (!isNumber || Number.isFinite(parsed));\n  const invalid = text.trim() !== \"\" && !valid;\n  const prefix = typeof field.prefix === \"string\" ? field.prefix : null;\n  const suffix = typeof field.suffix === \"string\" ? field.suffix : null;\n  const operatorText = operator\n    ? operatorLabel(operator, labels.operators)\n    : \"\";\n\n  return (\n    <DropdownMenuFilter filter={null} value={text} onValueChange={setText}>\n      <DropdownMenuSub\n        onOpenChange={(open, details) => {\n          // A tap or click puts the caret in the field (on touch, Base UI\n          // leaves focus alone). Hover-opens don't, so a sweep never steals\n          // the root search.\n          if (open && details.reason === \"trigger-press\") {\n            requestAnimationFrame(() => inputRef.current?.focus());\n          }\n        }}\n      >\n        <ValueSubmenuTrigger field={field} disabled={disabled} />\n        <DropdownMenuSubContent\n          className=\"w-64\"\n          viewportClassName=\"p-0\"\n          positionerClassName={SUBMENU_POSITIONER_STATIC}\n        >\n          <div\n            data-slot=\"filter-add-input\"\n            className={cn(POPUP_SEARCH_FIELD, POPUP_SEARCH_MENU_FIELD)}\n          >\n            {prefix && (\n              // Pulls in 4px to match the chip's affix spacing.\n              <span className=\"text-muted-foreground -me-1 shrink-0 text-sm\">\n                {prefix}\n              </span>\n            )}\n            <BaseMenu.Input\n              ref={inputRef}\n              aria-label={`${field.label} ${operatorText}`.trim()}\n              aria-invalid={invalid || undefined}\n              aria-describedby={invalid ? hintId : undefined}\n              inputMode={isNumber ? \"decimal\" : undefined}\n              placeholder={field.placeholder ?? labels.enterValue}\n              className={cn(\n                POPUP_SEARCH_INPUT,\n                isNumber && \"tabular-nums\",\n                \"aria-invalid:text-danger-foreground\",\n              )}\n            />\n            {suffix && (\n              <span className=\"text-muted-foreground shrink-0 text-sm\">\n                {suffix}\n              </span>\n            )}\n          </div>\n          <DropdownMenuEmpty\n            id={hintId}\n            className={cn(invalid && \"text-danger-foreground\")}\n          >\n            {invalid ? labels.invalidNumber : `${field.label} ${operatorText}…`}\n          </DropdownMenuEmpty>\n          <DropdownMenuList>\n            {valid && operator && (\n              <DropdownMenuItem\n                onClick={() => {\n                  addFilter(\n                    createFilter(field, {\n                      operator: operator.id,\n                      value: isNumber ? parsed : text,\n                    }),\n                  );\n                  onDone();\n                }}\n              >\n                <span className=\"text-muted-foreground shrink-0\">\n                  {field.label} {operatorText}\n                </span>\n                <span className=\"truncate font-medium\">\n                  {prefix}\n                  {isNumber ? parsed : `“${text}”`}\n                  {suffix && ` ${suffix}`}\n                </span>\n              </DropdownMenuItem>\n            )}\n          </DropdownMenuList>\n        </DropdownMenuSubContent>\n      </DropdownMenuSub>\n    </DropdownMenuFilter>\n  );\n}\n\n// A chip's value control, focused after the menu adds that filter.\nconst CHIP_FOCUS_TARGET =\n  '[data-slot=\"filter-chip-value\"]:is(button, input), [data-slot=\"filter-chip-value\"] :is(button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"]))';\n\ninterface AddMenuOptions {\n  /** Key that opens this menu from the keyboard (e.g. `\"f\"`). */\n  shortcut?: string;\n  /** Whether the menu is open (controlled). */\n  open?: boolean;\n  /** Whether the menu starts open (uncontrolled). */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\ntype AddMenuProps = React.ComponentProps<typeof Button> & AddMenuOptions;\n\n/** `kind=\"segment\"` renders inside the `FilterActions` capsule; `button` standalone. */\nfunction AddMenu({\n  children,\n  shortcut,\n  className,\n  onBlur,\n  kind,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  ...props\n}: AddMenuProps & { kind: \"button\" | \"segment\" }) {\n  const {\n    fields,\n    labels,\n    size,\n    usedFieldIds,\n    allowDuplicateFields,\n    addFilter,\n  } = useFiltersActions();\n  const compact = usedFieldIds.size > 0;\n  const inActions = kind === \"segment\";\n  const [open, setOpen] = useControllableState({\n    value: openProp,\n    defaultValue: defaultOpen,\n    onValueChange: onOpenChange,\n  });\n  // The root's search. Base UI clears it when the menu closes.\n  const [query, setQuery] = React.useState(\"\");\n  // Values join the list only while searching.\n  const searching = query.trim() !== \"\";\n\n  const focusFilterIdRef = React.useRef<string | null>(null);\n  const triggerRef = React.useRef<HTMLButtonElement | null>(null);\n  // Scoped to this bar: two bars can show the same filters.\n  const findChip = React.useCallback((filterId: string) => {\n    const scope =\n      triggerRef.current?.closest('[data-slot=\"filters\"]') ?? document;\n    return scope.querySelector<HTMLElement>(\n      `[data-slot=\"filter-chip\"][data-filter-id=\"${CSS.escape(filterId)}\"]`,\n    );\n  }, []);\n\n  // Closing restores focus to the button; until focus leaves it, focus-driven\n  // tooltip opens are refused (hover still opens it).\n  const [quietTooltip, setQuietTooltip] = React.useState(false);\n  const [tooltipOpen, setTooltipOpen] = React.useState(false);\n  // Derived from `open` itself so it also runs for a controlled `open`.\n  const [shownOpen, setShownOpen] = React.useState(open);\n  if (open !== shownOpen) {\n    setShownOpen(open);\n    if (!open) setQuietTooltip(true);\n  }\n  const changeOpen = React.useCallback(\n    (nextOpen: boolean) => setOpen(nextOpen),\n    [setOpen],\n  );\n  const wakeTooltip = React.useCallback(() => setQuietTooltip(false), []);\n\n  React.useEffect(() => {\n    if (!shortcut) return;\n    const handleKeyDown = (event: KeyboardEvent) => {\n      // Also dedupes multiple Filters instances: the first to accept prevents it.\n      if (event.defaultPrevented) return;\n      if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {\n        return;\n      }\n      // Skip IME composition, key repeat, and keyless synthetic keydowns (autofill).\n      if (event.isComposing || event.repeat || !event.key) return;\n      if (event.key.toLowerCase() !== shortcut.toLowerCase()) return;\n      const target = event.target;\n      if (target instanceof HTMLElement) {\n        if (\n          target instanceof HTMLInputElement ||\n          target instanceof HTMLTextAreaElement ||\n          target instanceof HTMLSelectElement ||\n          target.isContentEditable\n        ) {\n          return;\n        }\n        // Don't steal the key from open popups (menu typeahead, dialogs).\n        if (\n          target.closest(\n            '[role=\"menu\"], [role=\"listbox\"], [role=\"dialog\"], [role=\"alertdialog\"]',\n          )\n        ) {\n          return;\n        }\n      }\n      event.preventDefault();\n      changeOpen(true);\n    };\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [shortcut, changeOpen]);\n\n  const onDone = React.useCallback<OnDone>(\n    (focusFilterId) => {\n      focusFilterIdRef.current = focusFilterId ?? null;\n      changeOpen(false);\n    },\n    [changeOpen],\n  );\n\n  // Option fields whose values join the results while searching.\n  const optionFields = React.useMemo(\n    () => fields.filter(editsInPlace),\n    [fields],\n  );\n\n  // An edited chip may sit outside a scrolling bar; bring it into view.\n  const revealChip = React.useCallback(\n    (filterId: string) => {\n      requestAnimationFrame(() => {\n        const chip = findChip(filterId);\n        if (chip) revealInScroller(chip);\n      });\n    },\n    [findChip],\n  );\n\n  return (\n    <DropdownMenuFilter value={query} onValueChange={setQuery}>\n      {/* Non-modal: a modal menu measures its trigger while rendering, forcing\n          a layout mid chip entrance when a pick closes the menu. */}\n      <DropdownMenu open={open} onOpenChange={changeOpen} modal={false}>\n        {/* Only once folded to \"+\" (the label is hidden) and while closed. */}\n        <Tooltip\n          disabled={!compact || open}\n          open={tooltipOpen}\n          onOpenChange={(nextOpen, details) => {\n            if (\n              nextOpen &&\n              quietTooltip &&\n              details.reason === \"trigger-focus\"\n            ) {\n              return;\n            }\n            setTooltipOpen(nextOpen);\n          }}\n        >\n          <TooltipTrigger\n            render={\n              <DropdownMenuTrigger\n                ref={triggerRef}\n                render={\n                  inActions ? (\n                    <button\n                      type=\"button\"\n                      data-slot=\"filter-add\"\n                      aria-keyshortcuts={shortcut?.toUpperCase()}\n                      data-compact={compact ? \"\" : undefined}\n                      onBlur={(event: React.FocusEvent<HTMLButtonElement>) => {\n                        // Base UI's event type has extra fields; cast through.\n                        onBlur?.(\n                          event as Parameters<NonNullable<typeof onBlur>>[0],\n                        );\n                        wakeTooltip();\n                      }}\n                      className={cn(\n                        FILTER_SEGMENT,\n                        FILTER_SEGMENT_INTERACTIVE,\n                        // Folded, the side padding goes and the \"+\" square\n                        // below sets the width.\n                        \"text-muted-foreground hover:text-foreground data-popup-open:text-foreground gap-0 px-(--seg-icon-x) data-compact:px-0\",\n                        className,\n                      )}\n                      {...(props as React.ComponentProps<\"button\">)}\n                    />\n                  ) : (\n                    <Button\n                      data-slot=\"filter-add\"\n                      aria-keyshortcuts={shortcut?.toUpperCase()}\n                      data-compact={compact ? \"\" : undefined}\n                      onBlur={(event: React.FocusEvent<HTMLButtonElement>) => {\n                        // Base UI's event type has extra fields; cast through.\n                        onBlur?.(\n                          event as Parameters<NonNullable<typeof onBlur>>[0],\n                        );\n                        wakeTooltip();\n                      }}\n                      variant=\"outline\"\n                      size={size}\n                      className={cn(\n                        \"text-muted-foreground hover:text-foreground data-popup-open:text-foreground gap-0\",\n                        // Matches the icon side; the label adds the rest when shown.\n                        size === \"sm\" ? \"pe-2\" : \"pe-2.5\",\n                        className,\n                      )}\n                      leadingIcon={\n                        <HugeiconsIcon icon={PlusSignIcon} strokeWidth={2} />\n                      }\n                      {...props}\n                    />\n                  )\n                }\n              >\n                {inActions && (\n                  // Folded to \"+\", a square as tall as the plate, so the\n                  // segment stays square at any display scale.\n                  <span className=\"flex h-full shrink-0 items-center justify-center in-data-compact:aspect-square\">\n                    <HugeiconsIcon\n                      icon={PlusSignIcon}\n                      strokeWidth={2}\n                      className=\"size-4\"\n                    />\n                  </span>\n                )}\n                {/* A grid column folds it: animates in every browser (auto width\n                    doesn't) and stays readable to screen readers. */}\n                <span\n                  data-slot=\"filter-add-label\"\n                  className=\"grid grid-cols-[minmax(0,1fr)] overflow-hidden transition-[grid-template-columns,opacity] duration-220 ease-[cubic-bezier(0.22,1,0.36,1)] in-data-compact:grid-cols-[minmax(0,0fr)] in-data-compact:opacity-0 motion-reduce:transition-none\"\n                >\n                  <span\n                    // 2px more than the icon side: the \"+\" glyph has ~2.7px of\n                    // internal space, so this balances by ink.\n                    className=\"flex min-w-0 items-center gap-1.5 overflow-hidden ps-1.5 pe-0.5\"\n                  >\n                    {children ?? labels.add}\n                    {shortcut && (\n                      <Kbd size=\"sm\" variant=\"ghost\" aria-hidden>\n                        {shortcut.toUpperCase()}\n                      </Kbd>\n                    )}\n                  </span>\n                </span>\n              </DropdownMenuTrigger>\n            }\n          />\n          <TooltipContent className=\"flex items-center gap-2\">\n            {labels.addTooltip}\n            {shortcut && (\n              <Kbd size=\"sm\" variant=\"ghost\">\n                {shortcut.toUpperCase()}\n              </Kbd>\n            )}\n          </TooltipContent>\n        </Tooltip>\n        <DropdownMenuPortal>\n          {/* Composed without DropdownMenuContent's viewport: this menu never\n              swaps content, and without it Base UI positions with `transform`\n              at whole pixels, following the trigger without a glide. */}\n          <DropdownMenuPositioner\n            side=\"bottom\"\n            align=\"start\"\n            sideOffset={6}\n            collisionPadding={8}\n            className=\"z-50\"\n          >\n            <DropdownMenuPopup\n              aria-label={labels.add}\n              className=\"flex w-64 flex-col\"\n              finalFocus={() => {\n                const id = focusFilterIdRef.current;\n                focusFilterIdRef.current = null;\n                if (!id) return true;\n                return (\n                  findChip(id)?.querySelector<HTMLElement>(CHIP_FOCUS_TARGET) ??\n                  true\n                );\n              }}\n            >\n              <DropdownMenuInput\n                aria-label={nameFromPlaceholder(labels.searchFields)}\n                placeholder={labels.searchFields}\n              />\n              <DropdownMenuEmpty>{labels.noFields}</DropdownMenuEmpty>\n              <DropdownMenuList>\n                <DropdownMenuGroup>\n                  {searching && (\n                    <DropdownMenuGroupLabel>\n                      {labels.fields}\n                    </DropdownMenuGroupLabel>\n                  )}\n                  {fields.map((field) => {\n                    const valueStep = hasValueStep(field);\n                    // Option fields edit their one filter in place, so they\n                    // stay open; the rest close once used unless they repeat.\n                    const disabled =\n                      usedFieldIds.has(field.id) &&\n                      !(valueStep && isOptionField(field)) &&\n                      !allowsDuplicates(field, allowDuplicateFields);\n                    if (!valueStep) {\n                      return (\n                        <DropdownMenuItem\n                          key={field.id}\n                          label={field.label}\n                          disabled={disabled}\n                          onClick={() => {\n                            const filter = createFilter(field);\n                            addFilter(filter);\n                            onDone(filter.id);\n                          }}\n                        >\n                          <FieldIcon>{field.icon}</FieldIcon>\n                          <span className=\"truncate\">{field.label}</span>\n                        </DropdownMenuItem>\n                      );\n                    }\n                    return (\n                      <ValueSubmenu\n                        key={field.id}\n                        field={field}\n                        disabled={disabled}\n                        onDone={onDone}\n                      />\n                    );\n                  })}\n                </DropdownMenuGroup>\n                {searching && <DropdownMenuSeparator />}\n                {searching && (\n                  <DropdownMenuGroup>\n                    <DropdownMenuGroupLabel>\n                      {labels.values}\n                    </DropdownMenuGroupLabel>\n                    {optionFields.map((field) => (\n                      <OptionItems\n                        key={field.id}\n                        field={field}\n                        inSearch\n                        onEdited={revealChip}\n                        trailing={\n                          <span className=\"text-muted-foreground ms-auto shrink-0 text-xs\">\n                            {field.label}\n                          </span>\n                        }\n                      />\n                    ))}\n                  </DropdownMenuGroup>\n                )}\n              </DropdownMenuList>\n            </DropdownMenuPopup>\n          </DropdownMenuPositioner>\n        </DropdownMenuPortal>\n      </DropdownMenu>\n    </DropdownMenuFilter>\n  );\n}\n\n/** Standalone add-filter outline button with its menu. */\nfunction FilterAddButton(props: AddMenuProps) {\n  return <AddMenu {...props} kind=\"button\" />;\n}\n\n/** The add segment of a `FilterActions` capsule, for composing your own. */\nfunction FilterActionsAdd(\n  props: React.ComponentProps<\"button\"> & AddMenuOptions,\n) {\n  return <AddMenu {...(props as AddMenuProps)} kind=\"segment\" />;\n}\n\nexport { FilterAddButton, FilterActionsAdd, FieldIcon, OptionContent };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/filters/filters-add-menu.tsx"
    },
    {
      "path": "registry/default/filters/filters-context.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type {\n  FilterField,\n  FilterSize,\n  FilterValue,\n  FilterVariant,\n  FiltersLabels,\n} from \"./lib/filters-types\";\n\n/** Fast-changing: `filters` gets a new identity on every edit, including keystrokes. */\ninterface FiltersStateContextValue {\n  filters: FilterValue[];\n}\n\n/** Config and actions; stable across value edits, so leaves don't re-render per keystroke. */\ninterface FiltersActionsContextValue {\n  fields: FilterField[];\n  size: FilterSize;\n  variant: FilterVariant;\n  labels: FiltersLabels;\n  fieldsById: Map<string, FilterField>;\n  /** Field ids with at least one active filter. Stable across value edits. */\n  usedFieldIds: Set<string>;\n  allowDuplicateFields: boolean;\n  addFilter: (filter: FilterValue) => void;\n  updateFilter: (id: string, patch: Partial<Omit<FilterValue, \"id\">>) => void;\n  removeFilter: (id: string) => void;\n  clearAll: () => void;\n  /** The filters as of the last render, for handlers that shouldn't subscribe. */\n  getFilters: () => FilterValue[];\n}\n\ntype FiltersContextValue = FiltersStateContextValue &\n  FiltersActionsContextValue;\n\nconst FiltersStateContext =\n  React.createContext<FiltersStateContextValue | null>(null);\nconst FiltersActionsContext =\n  React.createContext<FiltersActionsContextValue | null>(null);\n\n/** The bar's fast-changing state (`filters`). Re-renders on every edit. */\nfunction useFiltersState(): FiltersStateContextValue {\n  const context = React.use(FiltersStateContext);\n  if (!context) {\n    throw new Error(\"useFiltersState must be used within a FiltersProvider.\");\n  }\n  return context;\n}\n\n/** The bar's config and actions. Stable while a filter value is being edited. */\nfunction useFiltersActions(): FiltersActionsContextValue {\n  const context = React.use(FiltersActionsContext);\n  if (!context) {\n    throw new Error(\"useFiltersActions must be used within a FiltersProvider.\");\n  }\n  return context;\n}\n\n/** Both contexts merged; re-renders on every edit (prefer `useFiltersActions` when that matters). */\nfunction useFilters(): FiltersContextValue {\n  const state = useFiltersState();\n  const actions = useFiltersActions();\n  return React.useMemo(() => ({ ...state, ...actions }), [state, actions]);\n}\n\ninterface FilterChipContextValue {\n  filter: FilterValue;\n  field: FilterField;\n  size: FilterSize;\n}\n\n/** `count` bumps per announcement so a repeated message still registers. */\ninterface FiltersAnnouncementValue {\n  text: string;\n  count: number;\n}\n\nconst FiltersAnnouncementContext =\n  React.createContext<FiltersAnnouncementValue>({ text: \"\", count: 0 });\n\nconst FilterChipContext = React.createContext<FilterChipContextValue | null>(\n  null,\n);\n\nfunction useFilterChip(): FilterChipContextValue {\n  const context = React.use(FilterChipContext);\n  if (!context) {\n    throw new Error(\"useFilterChip must be used within a FilterChip.\");\n  }\n  return context;\n}\n\nexport {\n  FiltersStateContext,\n  FiltersActionsContext,\n  useFilters,\n  useFiltersState,\n  useFiltersActions,\n  FilterChipContext,\n  FiltersAnnouncementContext,\n  useFilterChip,\n};\nexport type {\n  FiltersContextValue,\n  FiltersStateContextValue,\n  FiltersActionsContextValue,\n  FilterChipContextValue,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/filters/filters-context.tsx"
    },
    {
      "path": "registry/default/filters/filters-value-controls.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  Combobox,\n  ComboboxEmpty,\n  ComboboxInput,\n  ComboboxItem,\n  ComboboxList,\n  ComboboxPopup,\n  ComboboxTrigger,\n} from \"@/registry/default/combobox/combobox\";\nimport { TextMorph } from \"@/registry/default/text-morph/text-morph\";\nimport { NumberField as BaseNumberField } from \"@base-ui/react/number-field\";\n\nimport { OptionContent } from \"./filters-add-menu\";\nimport { useFilterChip, useFiltersActions } from \"./filters-context\";\nimport {\n  asNumberOrNull,\n  asNumberRange,\n  asString,\n  asStringArray,\n  CLICK_MORPH_MS,\n  FILTER_SEGMENT,\n  FILTER_SEGMENT_INTERACTIVE,\n  nameFromPlaceholder,\n  operatorShapeFor,\n} from \"./lib/filters-utils\";\nimport type {\n  FilterField,\n  FilterOption,\n  FilterValue,\n  FilterValueControlProps,\n  MultiSelectFilterField,\n  NumberFilterField,\n  SelectFilterField,\n  TextFilterField,\n} from \"./lib/filters-types\";\n\n// Padding around the chips' value lists, dropped once nothing matches so the\n// empty message sits evenly.\nconst LIST_PADDING = \"py-1 not-has-[[data-slot=combobox-item]]:py-0\";\n\ninterface ValueControlProps<F extends FilterField> {\n  field: F;\n  filter: FilterValue;\n  onValueChange: (value: unknown) => void;\n  className?: string;\n}\n\n/** A chip's value control for its field type; renders nothing for valueless operators (`is empty`). */\nfunction FilterChipValue({\n  className,\n}: {\n  /** Merged into the value segment (both segments of a `between` range). */\n  className?: string;\n}) {\n  const { field, filter, size } = useFilterChip();\n  const { updateFilter } = useFiltersActions();\n  const onValueChange = React.useCallback(\n    (nextValue: unknown) => updateFilter(filter.id, { value: nextValue }),\n    [updateFilter, filter.id],\n  );\n\n  if (operatorShapeFor(field, filter.operator) === \"none\") return null;\n\n  switch (field.type) {\n    case \"select\":\n    case \"multiselect\":\n      return (\n        <OptionsValueControl\n          field={field}\n          filter={filter}\n          onValueChange={onValueChange}\n          className={className}\n        />\n      );\n    case \"text\":\n      return (\n        <TextValueControl\n          field={field}\n          filter={filter}\n          onValueChange={onValueChange}\n          className={className}\n        />\n      );\n    case \"number\":\n      return (\n        <NumberValueControl\n          field={field}\n          filter={filter}\n          onValueChange={onValueChange}\n          className={className}\n        />\n      );\n    case \"custom\":\n      return (\n        <div\n          data-slot=\"filter-chip-value\"\n          className={cn(\"flex h-full items-stretch\", className)}\n        >\n          <CustomValue\n            value={filter.value}\n            operator={filter.operator}\n            onValueChange={onValueChange}\n            size={size}\n            field={field}\n          />\n        </div>\n      );\n    default:\n      return null;\n  }\n}\n\n// Its own component so `renderValue` can use hooks: called inline they'd belong\n// to the chip and break once a valueless operator stopped calling it.\nfunction CustomValue(props: FilterValueControlProps) {\n  return props.field.renderValue(props);\n}\n\n/** Accessible name for a value input, folding in string affixes (\"$\", \"hrs\"). */\nfunction valueAriaLabel(\n  field: TextFilterField | NumberFilterField,\n  part: string,\n): string {\n  const affixes = [field.prefix, field.suffix].filter(\n    (affix): affix is string => typeof affix === \"string\",\n  );\n  return [`${field.label} ${part}`, ...affixes].join(\" \");\n}\n\n/** Up to three overlapping option icons, ringed in the chip fill to separate them. */\nfunction StackedIcons({ options }: { options: FilterOption[] }) {\n  const withIcons = options.filter((option) => option.icon).slice(0, 3);\n  if (withIcons.length === 0) return null;\n  return (\n    <span aria-hidden className=\"flex shrink-0 items-center\">\n      {withIcons.map((option, index) => (\n        <span\n          key={option.value}\n          style={{ zIndex: withIcons.length - index }}\n          className={cn(\n            \"relative flex items-center justify-center rounded-full bg-(--filter-chip-bg) ring-[1.5px] ring-(--filter-chip-bg)\",\n            index > 0 && \"-ms-0.5\",\n            \"transition-[scale,opacity] duration-150 ease-out motion-reduce:transition-none starting:scale-50 starting:opacity-0\",\n          )}\n        >\n          {option.icon}\n        </span>\n      ))}\n    </span>\n  );\n}\n\nfunction FilterSearchPopup({\n  placeholder,\n  label,\n  empty,\n  className,\n  children,\n}: {\n  placeholder: string;\n  /** The search field's accessible name, e.g. \"Search status\". */\n  label: string;\n  empty: React.ReactNode;\n  className?: string;\n  children: React.ComponentProps<typeof ComboboxList>[\"children\"];\n}) {\n  return (\n    <ComboboxPopup\n      align=\"start\"\n      className={cn(\"flex w-64 flex-col p-0\", className)}\n    >\n      <ComboboxInput\n        showClear={false}\n        variant=\"popup\"\n        aria-label={label}\n        placeholder={placeholder}\n      />\n      <ComboboxEmpty>{empty}</ComboboxEmpty>\n      <ComboboxList className={cn(\"max-h-80\", LIST_PADDING)}>\n        {children}\n      </ComboboxList>\n    </ComboboxPopup>\n  );\n}\n\nfunction OptionsValueControl({\n  field,\n  filter,\n  onValueChange,\n  className,\n}: ValueControlProps<SelectFilterField | MultiSelectFilterField>) {\n  const { labels } = useFiltersActions();\n  const placeholder = field.placeholder ?? labels.selectValue;\n  const multiple = field.type === \"multiselect\";\n\n  const values = multiple\n    ? asStringArray(filter.value)\n    : typeof filter.value === \"string\"\n      ? [filter.value]\n      : [];\n  const selected = field.options.filter((option) =>\n    values.includes(option.value),\n  );\n  const atMax =\n    multiple &&\n    field.maxSelections != null &&\n    values.length >= field.maxSelections;\n  const text =\n    selected.length === 0\n      ? placeholder\n      : selected.length === 1\n        ? selected[0].label\n        : labels.selectedCount(selected.length, field);\n\n  const trigger = (\n    // Render function so the segment's classes replace the stock trigger's\n    // rather than merging.\n    <ComboboxTrigger\n      render={(triggerProps) => (\n        <button\n          {...triggerProps}\n          type=\"button\"\n          data-slot=\"filter-chip-value\"\n          // Leads with the visible text so voice control can target it.\n          aria-label={`${field.label} ${labels.value.toLowerCase()}: ${\n            selected.length > 1\n              ? `${text} (${selected.map((option) => option.label).join(\", \")})`\n              : text\n          }`}\n          className={cn(\n            FILTER_SEGMENT,\n            FILTER_SEGMENT_INTERACTIVE,\n            \"font-medium\",\n            selected.length === 0 && \"text-muted-foreground font-normal\",\n            className,\n          )}\n        >\n          <StackedIcons options={selected} />\n          <TextMorph\n            value={text}\n            split=\"word\"\n            duration={CLICK_MORPH_MS}\n            className=\"max-w-48 overflow-hidden\"\n          />\n        </button>\n      )}\n    />\n  );\n\n  const popup = (\n    <FilterSearchPopup\n      placeholder={labels.searchFieldValues(field)}\n      label={nameFromPlaceholder(labels.searchFieldValues(field))}\n      empty={labels.noResults}\n    >\n      {(option: FilterOption) => (\n        <ComboboxItem\n          key={option.value}\n          value={option}\n          disabled={atMax && !values.includes(option.value)}\n          className=\"my-0!\"\n        >\n          <OptionContent option={option} />\n        </ComboboxItem>\n      )}\n    </FilterSearchPopup>\n  );\n\n  if (multiple) {\n    return (\n      <Combobox<FilterOption, true>\n        items={field.options}\n        multiple\n        autoHighlight\n        value={selected}\n        onValueChange={(next) => {\n          // Reject selections past the cap; the controlled value snaps back.\n          if (\n            field.maxSelections != null &&\n            next.length > field.maxSelections\n          ) {\n            return;\n          }\n          onValueChange(next.map((option) => option.value));\n        }}\n        itemToStringLabel={(option) => option.label}\n      >\n        {trigger}\n        {popup}\n      </Combobox>\n    );\n  }\n\n  return (\n    <Combobox<FilterOption, false>\n      items={field.options}\n      autoHighlight\n      value={selected[0] ?? null}\n      onValueChange={(next) => onValueChange(next ? next.value : null)}\n      itemToStringLabel={(option) => option.label}\n    >\n      {trigger}\n      {popup}\n    </Combobox>\n  );\n}\n\nconst INPUT_SEGMENT = cn(\n  FILTER_SEGMENT,\n  // Inputs can't render ::before, so their plate is their own background,\n  // and their focus ring an inset one (an outline where forced colors drop it).\n  \"hover:bg-(--filter-highlight) focus-visible:bg-(--filter-highlight) cursor-text bg-transparent font-medium [transition-duration:0ms,0ms,100ms]\",\n  \"focus-visible:inset-ring-2 focus-visible:inset-ring-ring/50 forced-colors:focus-visible:outline-2 forced-colors:focus-visible:-outline-offset-2 forced-colors:focus-visible:outline-[Highlight]\",\n  \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground placeholder:font-normal\",\n  \"field-sizing-content max-w-48 min-w-[1ch] not-supports-[field-sizing:content]:w-28\",\n  // 16px on small screens, below which iOS zooms in on a focused input.\n  \"max-md:text-base\",\n);\n\nfunction ValueSegment({\n  prefix,\n  suffix,\n  className,\n  children,\n}: {\n  prefix?: React.ReactNode;\n  suffix?: React.ReactNode;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div\n      data-slot=\"filter-chip-value\"\n      className={cn(\n        \"flex h-full items-center\",\n        prefix != null && \"[&_input]:ps-1\",\n        suffix != null && \"[&_input]:pe-1\",\n        className,\n      )}\n    >\n      {prefix != null && (\n        <span className=\"text-muted-foreground ps-(--seg-x) select-none\">\n          {prefix}\n        </span>\n      )}\n      {children}\n      {suffix != null && (\n        <span className=\"text-muted-foreground pe-(--seg-x) select-none\">\n          {suffix}\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction TextValueControl({\n  field,\n  filter,\n  onValueChange,\n  className,\n}: ValueControlProps<TextFilterField>) {\n  const { labels } = useFiltersActions();\n  return (\n    <ValueSegment\n      prefix={field.prefix}\n      suffix={field.suffix}\n      className={className}\n    >\n      <input\n        data-slot=\"filter-chip-value-input\"\n        type=\"text\"\n        aria-label={valueAriaLabel(field, labels.value.toLowerCase())}\n        value={asString(filter.value)}\n        placeholder={field.placeholder ?? labels.enterValue}\n        className={INPUT_SEGMENT}\n        onChange={(event) => onValueChange(event.target.value)}\n      />\n    </ValueSegment>\n  );\n}\n\nfunction NumberValueField({\n  value,\n  className,\n  step,\n  placeholder,\n  prefix,\n  suffix,\n  \"aria-label\": ariaLabel,\n  onValueChange,\n}: {\n  value: number | null;\n  className?: string;\n  step?: number;\n  placeholder?: string;\n  prefix?: React.ReactNode;\n  suffix?: React.ReactNode;\n  \"aria-label\": string;\n  onValueChange: (value: number | null) => void;\n}) {\n  // `allowWheelScrub` only acts while focused and hovered, so page scroll is safe.\n  return (\n    <ValueSegment prefix={prefix} suffix={suffix} className={className}>\n      <BaseNumberField.Root\n        value={value}\n        step={step}\n        allowWheelScrub\n        onValueChange={onValueChange}\n        className=\"flex h-full items-center\"\n      >\n        <BaseNumberField.Input\n          data-slot=\"filter-chip-value-input\"\n          aria-label={ariaLabel}\n          placeholder={placeholder}\n          className={cn(\n            INPUT_SEGMENT,\n            \"tabular-nums not-supports-[field-sizing:content]:w-16\",\n          )}\n        />\n      </BaseNumberField.Root>\n    </ValueSegment>\n  );\n}\n\nfunction NumberValueControl({\n  field,\n  filter,\n  onValueChange,\n  className,\n}: ValueControlProps<NumberFilterField>) {\n  const { labels } = useFiltersActions();\n\n  if (operatorShapeFor(field, filter.operator) === \"range\") {\n    const range = asNumberRange(filter.value);\n    return (\n      <>\n        <NumberValueField\n          className={className}\n          value={range.min}\n          step={field.step}\n          placeholder={labels.min}\n          prefix={field.prefix}\n          suffix={field.suffix}\n          aria-label={valueAriaLabel(field, labels.min.toLowerCase())}\n          onValueChange={(min) => onValueChange({ ...range, min })}\n        />\n        <span\n          aria-hidden\n          className=\"text-muted-foreground shrink-0 px-0.5 select-none\"\n        >\n          {labels.and}\n        </span>\n        <NumberValueField\n          className={className}\n          value={range.max}\n          step={field.step}\n          placeholder={labels.max}\n          prefix={field.prefix}\n          suffix={field.suffix}\n          aria-label={valueAriaLabel(field, labels.max.toLowerCase())}\n          onValueChange={(max) => onValueChange({ ...range, max })}\n        />\n      </>\n    );\n  }\n\n  return (\n    <NumberValueField\n      className={className}\n      value={asNumberOrNull(filter.value)}\n      step={field.step}\n      placeholder={field.placeholder ?? labels.value}\n      prefix={field.prefix}\n      suffix={field.suffix}\n      aria-label={valueAriaLabel(field, labels.value.toLowerCase())}\n      onValueChange={onValueChange}\n    />\n  );\n}\n\nexport { FilterChipValue };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/filters/filters-value-controls.tsx"
    }
  ],
  "type": "registry:ui"
}