{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter",
  "title": "Filter",
  "description": "Filter components including input search, single select, multi select, and toggle filters for filtering data.",
  "dependencies": [
    "@mdi/js",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://blok.sitecore.com/r/theme.json",
    "https://blok.sitecore.com/r/button.json",
    "https://blok.sitecore.com/r/input.json",
    "https://blok.sitecore.com/r/select.json",
    "https://blok.sitecore.com/r/popover.json",
    "https://blok.sitecore.com/r/badge.json",
    "https://blok.sitecore.com/r/checkbox.json",
    "https://blok.sitecore.com/r/search-input.json"
  ],
  "files": [
    {
      "path": "src/components/ui/filter.tsx",
      "content": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  SearchInput,\n  SearchInputClearButton,\n  SearchInputField,\n  SearchInputLeftElement,\n  SearchInputRightElement,\n} from \"@/components/ui/search-input\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Icon } from \"@/lib/icon\";\nimport { cn } from \"@/lib/utils\";\nimport { mdiCheck, mdiChevronDown, mdiClose, mdiMagnify } from \"@mdi/js\";\nimport * as React from \"react\";\nimport type { ComponentProps } from \"react\";\n\nconst FILTER_DROPDOWN_WIDTH = \"w-54 min-w-54\";\n\nconst FILTER_DROPDOWN_CONTAINER =\n  \"rounded-lg border border-border bg-popover text-popover-foreground shadow-md outline-none\";\n\nconst FILTER_SELECT_TRIGGER_CLASSNAME =\n  \"border-border text-md text-neutral-fg font-semibold outline-none focus-visible:ring-1 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 bg-body-bg hover:bg-neutral-bg dark:bg-input/30 dark:hover:bg-input/50 flex w-fit min-w-0 max-w-full items-center justify-between gap-2 rounded-md border-1 px-3 py-2 whitespace-nowrap transition-[color] h-10 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 data-[state=open]:border-primary data-[state=open]:border-2 data-[state=open]:bg-primary-bg data-[state=open]:text-primary-fg data-[state=open]:hover:bg-primary-bg [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>svg]:!text-current [&>svg]:!opacity-100\";\n\nfunction FilterSingleSelectContent({\n  className,\n  children,\n  position = \"popper\",\n  ...props\n}: React.ComponentProps<typeof SelectContent>) {\n  return (\n    <SelectContent\n      data-slot=\"filter-single-select-content\"\n      className={cn(\n        FILTER_DROPDOWN_WIDTH,\n        FILTER_DROPDOWN_CONTAINER,\n        \"min-w-0 max-h-80 overflow-hidden p-0 [&_[data-slot=select-viewport]]:h-auto [&_[data-slot=select-viewport]]:min-w-0 [&_[data-slot=select-viewport]]:p-2 [&_[data-slot=select-viewport]]:max-h-80\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      {children}\n    </SelectContent>\n  );\n}\n\nfunction getMultiSelectDisplayParts(\n  labels: string[],\n  maxDisplayItems: number,\n): { shownLabels: string[]; overflowCount: number } {\n  if (labels.length === 0) {\n    return { shownLabels: [], overflowCount: 0 };\n  }\n  if (labels.length <= maxDisplayItems) {\n    return { shownLabels: labels, overflowCount: 0 };\n  }\n  return {\n    shownLabels: labels.slice(0, maxDisplayItems),\n    overflowCount: labels.length - maxDisplayItems,\n  };\n}\n\nexport interface FilterOption {\n  value: string;\n  label: string;\n  description?: string;\n  disabled?: boolean;\n}\n\nexport interface FilterInputProps {\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  placeholder?: string;\n  \"aria-label\"?: string;\n  icon?: string;\n  showClear?: boolean;\n  className?: string;\n  width?: string;\n  disabled?: boolean;\n  name?: string;\n  helperText?: string;\n  \"aria-describedby\"?: string;\n}\n\nexport interface FilterSingleSelectGroup {\n  label: string;\n  options: FilterOption[];\n}\n\nexport interface FilterSingleSelectProps {\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  options: FilterOption[];\n  placeholder?: string;\n  groupLabel?: string;\n  groups?: FilterSingleSelectGroup[];\n  searchable?: boolean;\n  showSearch?: boolean;\n  searchPlaceholder?: string;\n  noResultsText?: string;\n  showClear?: boolean;\n  className?: string;\n  disabled?: boolean;\n  name?: string;\n  helperText?: string;\n  ariaLabels?: FilterAriaLabels;\n  \"aria-describedby\"?: string;\n  renderOption?: (option: FilterOption) => React.ReactNode;\n  dropdownClassName?: string;\n}\n\nexport interface FilterMultiSelectProps {\n  value?: string[];\n  defaultValue?: string[];\n  onChange?: (values: string[]) => void;\n  options: FilterOption[];\n  placeholder?: string;\n  groupLabel?: string;\n  groups?: FilterSingleSelectGroup[];\n  searchable?: boolean;\n  showSearch?: boolean;\n  searchPlaceholder?: string;\n  noResultsText?: string;\n  maxButtonWidth?: number;\n  displayMode?: \"text\" | \"badge\";\n  maxDisplayItems?: number;\n  showClear?: boolean;\n  showSelectedCount?: boolean;\n  className?: string;\n  disabled?: boolean;\n  name?: string;\n  helperText?: string;\n  ariaLabels?: FilterAriaLabels;\n  renderOption?: (option: FilterOption) => React.ReactNode;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  dropdownClassName?: string;\n}\n\nexport type FilterDefinition =\n  | { type: \"input\"; key: string; props: FilterInputProps }\n  | { type: \"single-select\"; key: string; props: FilterSingleSelectProps }\n  | { type: \"multi-select\"; key: string; props: FilterMultiSelectProps };\n\nexport interface FilterBarProps {\n  filters: FilterDefinition[];\n  values?: Record<string, unknown>;\n  onChange?: (key: string, value: unknown) => void;\n  onClearAll?: () => void;\n  showClearAll?: boolean;\n  clearAllText?: string;\n  direction?: \"horizontal\" | \"vertical\";\n  gap?: string;\n  className?: string;\n  ariaLabel?: string;\n}\n\nexport type FilterAriaLabels = {\n  popoverTrigger?: string;\n  searchInput?: string;\n  listbox?: string;\n  clearSelection?: string;\n};\n\n// FILTER INPUT COMPONENT\n\nconst FilterInput = React.forwardRef<\n  HTMLInputElement,\n  FilterInputProps & Omit<ComponentProps<\"div\">, \"onChange\" | \"ref\">\n>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = \"\",\n      onChange,\n      placeholder = \"Search...\",\n      \"aria-label\": ariaLabel = \"Search\",\n      icon = mdiMagnify,\n      showClear = true,\n      className,\n      width = \"w-full max-w-sm\",\n      disabled = false,\n      name,\n      helperText,\n      \"aria-describedby\": ariaDescribedBy,\n      ...props\n    },\n    ref,\n  ) => {\n    const [internalValue, setInternalValue] = React.useState(defaultValue);\n    const isControlled = controlledValue !== undefined;\n    const value = isControlled ? controlledValue : internalValue;\n    const helperId = React.useId();\n    const describedBy = helperText\n      ? ariaDescribedBy\n        ? `${helperId} ${ariaDescribedBy}`\n        : helperId\n      : ariaDescribedBy;\n\n    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      if (!isControlled) {\n        setInternalValue(newValue);\n      }\n      onChange?.(newValue);\n    };\n\n    const handleClear = () => {\n      if (!isControlled) {\n        setInternalValue(\"\");\n      }\n      onChange?.(\"\");\n    };\n\n    return (\n      <div className={cn(width, className)} {...props}>\n        <SearchInput>\n          <SearchInputLeftElement>\n            <Icon path={icon} />\n          </SearchInputLeftElement>\n          <SearchInputField\n            ref={ref}\n            name={name}\n            placeholder={placeholder}\n            aria-label={ariaLabel}\n            aria-describedby={describedBy}\n            autoComplete=\"off\"\n            value={value}\n            onChange={handleChange}\n            disabled={disabled}\n          />\n          {showClear && value && !disabled && (\n            <SearchInputRightElement>\n              <SearchInputClearButton\n                onClear={handleClear}\n                tooltipLabel=\"Clear search\"\n              />\n            </SearchInputRightElement>\n          )}\n        </SearchInput>\n        {helperText && (\n          <p id={helperId} className=\"mt-1 text-xs text-muted-foreground\">\n            {helperText}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nFilterInput.displayName = \"FilterInput\";\n\n// FILTER SINGLE SELECT COMPONENT\n\nconst FilterSingleSelect = React.forwardRef<\n  HTMLButtonElement,\n  FilterSingleSelectProps & Omit<ComponentProps<\"div\">, \"onChange\" | \"ref\">\n>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = \"\",\n      onChange,\n      options,\n      placeholder = \"Select an option\",\n      groupLabel,\n      groups,\n      searchable = false,\n      showSearch = true,\n      searchPlaceholder = \"Search\",\n      noResultsText = \"No results found\",\n      showClear = true,\n      className,\n      disabled = false,\n      name,\n      helperText,\n      ariaLabels = {\n        clearSelection: \"Clear selection\",\n      },\n      \"aria-describedby\": ariaDescribedBy,\n      renderOption,\n      dropdownClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    const [internalValue, setInternalValue] = React.useState(defaultValue);\n    const [internalOpen, setInternalOpen] = React.useState(false);\n    const [searchQuery, setSearchQuery] = React.useState(\"\");\n    const isControlled = controlledValue !== undefined;\n    const value = isControlled ? controlledValue : internalValue;\n    const open = internalOpen;\n    const helperId = React.useId();\n    const describedBy = helperText\n      ? ariaDescribedBy\n        ? `${helperId} ${ariaDescribedBy}`\n        : helperId\n      : ariaDescribedBy;\n\n    const allOptions = groups ? groups.flatMap((g) => g.options) : options;\n    const selectedLabel = value\n      ? allOptions.find((opt) => opt.value === value)?.label\n      : \"\";\n\n    const handleChange = (newValue: string) => {\n      if (!isControlled) {\n        setInternalValue(newValue);\n      }\n      onChange?.(newValue);\n    };\n\n    const handleOpenChange = (newOpen: boolean) => {\n      setInternalOpen(newOpen);\n    };\n\n    const handleClear = (e: React.MouseEvent) => {\n      e.stopPropagation();\n      if (!isControlled) {\n        setInternalValue(\"\");\n      }\n      onChange?.(\"\");\n    };\n\n    const hasValue = Boolean(value);\n\n    const renderOptionContent = (option: FilterOption) => {\n      if (renderOption) {\n        return renderOption(option);\n      }\n      if (option.description) {\n        return (\n          <div className=\"flex flex-col gap-0.5\">\n            <span>{option.label}</span>\n            <span className=\"text-xs text-muted-foreground\">\n              {option.description}\n            </span>\n          </div>\n        );\n      }\n      return option.label;\n    };\n\n    const filteredOptions = React.useMemo(\n      () =>\n        searchQuery.trim() === \"\"\n          ? allOptions\n          : allOptions.filter((opt) =>\n              opt.label\n                .toLowerCase()\n                .includes(searchQuery.trim().toLowerCase()),\n            ),\n      [allOptions, searchQuery],\n    );\n\n    if (searchable) {\n      return (\n        <div\n          className={cn(\"relative inline-flex w-fit flex-col\", className)}\n          {...props}\n        >\n          <div className=\"relative inline-flex w-fit\">\n            <Popover\n              open={open}\n              onOpenChange={(newOpen) => {\n                setInternalOpen(newOpen);\n                if (!newOpen) setSearchQuery(\"\");\n              }}\n            >\n              <PopoverTrigger asChild>\n                <Button\n                  ref={ref}\n                  type=\"button\"\n                  variant=\"ghost\"\n                  aria-label={ariaLabels?.popoverTrigger ?? placeholder}\n                  aria-describedby={describedBy}\n                  aria-expanded={open}\n                  disabled={disabled}\n                  className={cn(\n                    FILTER_SELECT_TRIGGER_CLASSNAME,\n                    hasValue && \"pr-8 overflow-hidden\",\n                  )}\n                >\n                  <span className=\"flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden text-left pointer-events-none\">\n                    <span\n                      className={cn(\n                        \"font-semibold truncate\",\n                        open && \"text-primary-fg\",\n                        !open && \"text-neutral-fg\",\n                      )}\n                    >\n                      {placeholder}\n                    </span>\n                    {selectedLabel && (\n                      <>\n                        <span\n                          className={cn(\n                            open && \"text-primary-fg\",\n                            !open && \"text-neutral-fg\",\n                          )}\n                        >\n                          :\n                        </span>\n                        <span\n                          className={cn(\n                            \"font-normal truncate min-w-0 ml-0.5\",\n                            open && \"text-primary-fg\",\n                            !open && \"text-neutral-fg\",\n                          )}\n                        >\n                          {selectedLabel}\n                        </span>\n                      </>\n                    )}\n                  </span>\n                  {!(hasValue && showClear) && (\n                    <Icon\n                      path={mdiChevronDown}\n                      size={1.3}\n                      className=\"size-4 shrink-0 pointer-events-none text-current\"\n                    />\n                  )}\n                </Button>\n              </PopoverTrigger>\n              <PopoverContent\n                align=\"start\"\n                aria-label={ariaLabels?.listbox ?? groupLabel ?? placeholder}\n                className={cn(\n                  FILTER_DROPDOWN_WIDTH,\n                  FILTER_DROPDOWN_CONTAINER,\n                  \"p-2 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 z-50 origin-(--radix-popover-content-transform-origin)\",\n                  dropdownClassName,\n                )}\n              >\n                {searchable && showSearch && (\n                  <div className=\"mb-2\">\n                    <SearchInput className=\"h-10 border-border has-[[data-slot=search-input-control]:focus-visible]:border-primary\">\n                      <SearchInputLeftElement>\n                        <Icon\n                          path={mdiMagnify}\n                          size={0.9}\n                          className=\"text-muted-foreground\"\n                        />\n                      </SearchInputLeftElement>\n                      <SearchInputField\n                        placeholder={searchPlaceholder}\n                        value={searchQuery}\n                        onChange={(e) => setSearchQuery(e.target.value)}\n                        aria-label={\n                          ariaLabels?.searchInput ?? searchPlaceholder\n                        }\n                        className=\"placeholder:text-muted-foreground\"\n                      />\n                      {searchQuery && (\n                        <SearchInputRightElement>\n                          <SearchInputClearButton\n                            onClear={() => setSearchQuery(\"\")}\n                            tooltipLabel=\"Clear search\"\n                          />\n                        </SearchInputRightElement>\n                      )}\n                    </SearchInput>\n                  </div>\n                )}\n                <div className=\"max-h-80 w-full -mx-1 px-1 overflow-y-auto scrollbar-hidden\">\n                  <div\n                    className=\"py-0.5\"\n                    role=\"listbox\"\n                    aria-label={\n                      ariaLabels?.listbox ?? groupLabel ?? placeholder\n                    }\n                  >\n                    {filteredOptions.length === 0 ? (\n                      <div className=\"py-6 text-center text-sm text-muted-foreground\">\n                        {noResultsText}\n                      </div>\n                    ) : (\n                      filteredOptions.map((option) => (\n                        <Button\n                          key={option.value}\n                          type=\"button\"\n                          variant=\"ghost\"\n                          role=\"option\"\n                          aria-selected={value === option.value}\n                          disabled={option.disabled}\n                          onClick={() => handleChange(option.value)}\n                          className={cn(\n                            \"h-auto min-h-0 w-full justify-between rounded-sm px-2 py-1 text-left text-sm font-normal hover:bg-accent/50 focus-visible:bg-accent/50\",\n                            option.disabled &&\n                              \"opacity-50 cursor-not-allowed pointer-events-none\",\n                          )}\n                        >\n                          <span className=\"min-w-0 truncate\">\n                            {renderOptionContent(option)}\n                          </span>\n                          {value === option.value && (\n                            <Icon\n                              path={mdiCheck}\n                              size={0.9}\n                              className=\"size-4 shrink-0 text-primary-fg\"\n                            />\n                          )}\n                        </Button>\n                      ))\n                    )}\n                  </div>\n                </div>\n              </PopoverContent>\n            </Popover>\n            {showClear && hasValue && (\n              <Button\n                onClick={handleClear}\n                variant=\"ghost\"\n                size=\"icon-xs\"\n                colorScheme=\"neutral\"\n                aria-label={ariaLabels?.clearSelection}\n                className=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-auto text-neutral-fg hover:bg-neutral-bg-active\"\n              >\n                <Icon path={mdiClose} size={0.75} />\n              </Button>\n            )}\n          </div>\n          {helperText && (\n            <p id={helperId} className=\"mt-1 text-xs text-muted-foreground\">\n              {helperText}\n            </p>\n          )}\n        </div>\n      );\n    }\n\n    return (\n      <div\n        className={cn(\"relative inline-flex w-fit flex-col\", className)}\n        {...props}\n      >\n        <div className=\"relative inline-flex w-fit\">\n          <Select\n            value={value}\n            onValueChange={handleChange}\n            disabled={disabled}\n            name={name}\n            open={open}\n            onOpenChange={handleOpenChange}\n          >\n            <SelectTrigger\n              ref={ref}\n              aria-label={ariaLabels?.popoverTrigger ?? placeholder}\n              aria-describedby={describedBy}\n              className={cn(\n                \"*:data-[slot=select-value]:hidden\",\n                FILTER_SELECT_TRIGGER_CLASSNAME,\n                hasValue && \"pr-8 overflow-hidden\",\n                hasValue && showClear && \"[&_svg]:hidden\",\n              )}\n            >\n              <SelectValue placeholder={placeholder} />\n              <span className=\"flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden text-left pointer-events-none\">\n                <span\n                  className={cn(\n                    \"font-semibold truncate\",\n                    open && \"text-primary-fg\",\n                    !open && \"text-neutral-fg\",\n                  )}\n                >\n                  {placeholder}\n                </span>\n                {selectedLabel && (\n                  <>\n                    <span\n                      className={cn(\n                        open && \"text-primary-fg\",\n                        !open && \"text-neutral-fg\",\n                      )}\n                    >\n                      :\n                    </span>\n                    <span\n                      className={cn(\n                        \"font-normal truncate min-w-0 ml-0.5\",\n                        open && \"text-primary-fg\",\n                        !open && \"text-neutral-fg\",\n                      )}\n                    >\n                      {selectedLabel}\n                    </span>\n                  </>\n                )}\n              </span>\n            </SelectTrigger>\n            <FilterSingleSelectContent\n              align=\"start\"\n              aria-label={ariaLabels?.listbox ?? groupLabel ?? placeholder}\n            >\n              {groups ? (\n                groups.map((group) => (\n                  <SelectGroup key={group.label}>\n                    <SelectLabel className=\"px-2\">{group.label}</SelectLabel>\n                    {group.options.map((option) => (\n                      <SelectItem\n                        key={option.value}\n                        value={option.value}\n                        disabled={option.disabled}\n                        className=\"pl-2\"\n                      >\n                        {renderOptionContent(option)}\n                      </SelectItem>\n                    ))}\n                  </SelectGroup>\n                ))\n              ) : (\n                <SelectGroup>\n                  {groupLabel && (\n                    <SelectLabel className=\"px-2\">{groupLabel}</SelectLabel>\n                  )}\n                  {options.map((option) => (\n                    <SelectItem\n                      key={option.value}\n                      value={option.value}\n                      disabled={option.disabled}\n                      className=\"pl-2\"\n                    >\n                      {renderOptionContent(option)}\n                    </SelectItem>\n                  ))}\n                </SelectGroup>\n              )}\n            </FilterSingleSelectContent>\n          </Select>\n          {showClear && hasValue && (\n            <Button\n              onClick={handleClear}\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              colorScheme=\"neutral\"\n              aria-label={ariaLabels?.clearSelection}\n              className=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-auto text-neutral-fg hover:bg-neutral-bg-active\"\n            >\n              <Icon path={mdiClose} size={0.75} />\n            </Button>\n          )}\n        </div>\n        {helperText && (\n          <p id={helperId} className=\"mt-1 text-xs text-muted-foreground\">\n            {helperText}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nFilterSingleSelect.displayName = \"FilterSingleSelect\";\n\n// FILTER MULTI SELECT COMPONENT\n\nconst FilterMultiSelect = React.forwardRef<\n  HTMLButtonElement,\n  FilterMultiSelectProps & Omit<ComponentProps<\"div\">, \"onChange\" | \"ref\">\n>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = [],\n      onChange,\n      options,\n      placeholder = \"Select options\",\n      groupLabel,\n      groups,\n      searchable = false,\n      showSearch = true,\n      searchPlaceholder = \"Search\",\n      noResultsText = \"No results found\",\n      displayMode = \"text\",\n      maxDisplayItems = 3,\n      showClear = true,\n      showSelectedCount = false,\n      maxButtonWidth,\n      className,\n      disabled = false,\n      name,\n      helperText,\n      ariaLabels = {\n        clearSelection: \"Clear selection\",\n      },\n      \"aria-describedby\": ariaDescribedBy,\n      renderOption,\n      open: controlledOpen,\n      onOpenChange,\n      dropdownClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    const [internalValue, setInternalValue] =\n      React.useState<string[]>(defaultValue);\n    const [internalOpen, setInternalOpen] = React.useState(false);\n    const [searchQuery, setSearchQuery] = React.useState(\"\");\n    const isControlled = controlledValue !== undefined;\n    const isOpenControlled = controlledOpen !== undefined;\n    const values = isControlled ? controlledValue : internalValue;\n    const open = isOpenControlled ? controlledOpen : internalOpen;\n    const helperId = React.useId();\n    const describedBy = helperText\n      ? ariaDescribedBy\n        ? `${helperId} ${ariaDescribedBy}`\n        : helperId\n      : ariaDescribedBy;\n\n    const allMultiOptions = React.useMemo(\n      () => (groups ? groups.flatMap((g) => g.options) : options),\n      [groups, options],\n    );\n    const selectedLabels = values\n      .map((val) => allMultiOptions.find((opt) => opt.value === val)?.label)\n      .filter(Boolean) as string[];\n\n    const setButtonRef = React.useCallback(\n      (node: HTMLButtonElement | null) => {\n        if (typeof ref === \"function\") {\n          ref(node);\n        } else if (ref) {\n          (ref as React.MutableRefObject<HTMLButtonElement | null>).current =\n            node;\n        }\n      },\n      [ref],\n    );\n\n    const displayParts = React.useMemo(\n      () => getMultiSelectDisplayParts(selectedLabels, maxDisplayItems),\n      [selectedLabels, maxDisplayItems],\n    );\n\n    const isButtonWidthConstrained = maxButtonWidth != null;\n\n    const handleOpenChange = (newOpen: boolean) => {\n      if (!isOpenControlled) {\n        setInternalOpen(newOpen);\n      }\n      onOpenChange?.(newOpen);\n    };\n\n    const handleToggle = (optionValue: string) => {\n      const newValues = values.includes(optionValue)\n        ? values.filter((v) => v !== optionValue)\n        : [...values, optionValue];\n      if (!isControlled) {\n        setInternalValue(newValues);\n      }\n      onChange?.(newValues);\n    };\n\n    const handleClear = (e: React.MouseEvent) => {\n      e.stopPropagation();\n      if (!isControlled) {\n        setInternalValue([]);\n      }\n      onChange?.([]);\n    };\n\n    const handleRemoveBadge = (optionValue: string, e: React.MouseEvent) => {\n      e.stopPropagation();\n      const newValues = values.filter((v) => v !== optionValue);\n      if (!isControlled) {\n        setInternalValue(newValues);\n      }\n      onChange?.(newValues);\n    };\n\n    const renderOptionContent = (option: FilterOption) => {\n      if (renderOption) {\n        return renderOption(option);\n      }\n      if (option.description) {\n        return (\n          <div className=\"flex flex-col gap-0.5\">\n            <span>{option.label}</span>\n            <span className=\"text-xs text-muted-foreground\">\n              {option.description}\n            </span>\n          </div>\n        );\n      }\n      return option.label;\n    };\n\n    const hasValues = values.length > 0;\n\n    const filteredOptions = React.useMemo(\n      () =>\n        searchQuery.trim() === \"\"\n          ? options\n          : options.filter((opt) =>\n              opt.label\n                .toLowerCase()\n                .includes(searchQuery.trim().toLowerCase()),\n            ),\n      [options, searchQuery],\n    );\n\n    const filteredMultiGroups = React.useMemo(() => {\n      if (!groups) return null;\n      const q = searchQuery.trim().toLowerCase();\n      return groups\n        .map((g) => ({\n          ...g,\n          options:\n            q === \"\"\n              ? g.options\n              : g.options.filter((opt) => opt.label.toLowerCase().includes(q)),\n        }))\n        .filter((g) => g.options.length > 0);\n    }, [groups, searchQuery]);\n\n    const handleOpenChangeWithSearch = (newOpen: boolean) => {\n      if (!newOpen) setSearchQuery(\"\");\n      handleOpenChange(newOpen);\n    };\n\n    const renderMultiOptionRow = (option: FilterOption) => (\n      <label\n        key={option.value}\n        className={cn(\n          \"flex items-center gap-2 px-2 py-1 text-sm rounded-sm cursor-pointer hover:bg-accent/50\",\n          option.disabled && \"opacity-50 cursor-not-allowed\",\n        )}\n      >\n        <Checkbox\n          checked={values.includes(option.value)}\n          onCheckedChange={() => handleToggle(option.value)}\n          disabled={option.disabled}\n          className=\"pointer-events-auto shrink-0\"\n        />\n        {renderOptionContent(option)}\n      </label>\n    );\n\n    return (\n      <div\n        className={cn(\"relative inline-flex w-fit flex-col\", className)}\n        {...props}\n      >\n        {name && <input type=\"hidden\" name={name} value={values.join(\",\")} />}\n        <div\n          className=\"relative inline-flex w-fit max-w-full\"\n          style={\n            isButtonWidthConstrained ? { maxWidth: maxButtonWidth } : undefined\n          }\n        >\n          <Popover open={open} onOpenChange={handleOpenChangeWithSearch}>\n            <PopoverTrigger asChild>\n              <Button\n                ref={setButtonRef}\n                type=\"button\"\n                variant=\"ghost\"\n                disabled={disabled}\n                aria-label={ariaLabels?.popoverTrigger ?? placeholder}\n                aria-describedby={describedBy}\n                aria-expanded={open}\n                style={\n                  isButtonWidthConstrained\n                    ? { maxWidth: maxButtonWidth }\n                    : undefined\n                }\n                className={cn(\n                  FILTER_SELECT_TRIGGER_CLASSNAME,\n                  hasValues && \"pr-8 overflow-hidden\",\n                )}\n              >\n                <span\n                  className={cn(\n                    \"flex items-center gap-0.5 text-left pointer-events-none\",\n                    isButtonWidthConstrained &&\n                      \"min-w-0 flex-1 overflow-hidden\",\n                  )}\n                >\n                  <span\n                    className={cn(\n                      \"font-semibold shrink-0\",\n                      open && \"text-primary-fg\",\n                      !open && \"text-neutral-fg\",\n                    )}\n                  >\n                    {placeholder}\n                  </span>\n                  {hasValues && showSelectedCount && displayMode === \"text\" && (\n                    <Badge colorScheme=\"neutral\" size=\"sm\" className=\"ml-1.5\">\n                      {values.length}\n                    </Badge>\n                  )}\n                  {hasValues &&\n                    !showSelectedCount &&\n                    displayMode === \"text\" && (\n                      <>\n                        <span\n                          className={cn(\n                            \"shrink-0\",\n                            open && \"text-primary-fg\",\n                            !open && \"text-neutral-fg\",\n                          )}\n                        >\n                          :\n                        </span>\n                        <span\n                          className={cn(\n                            \"font-normal\",\n                            isButtonWidthConstrained\n                              ? \"min-w-0 flex-1 truncate\"\n                              : \"shrink-0\",\n                            open && \"text-primary-fg\",\n                            !open && \"text-neutral-fg\",\n                          )}\n                          title={selectedLabels.join(\", \")}\n                        >\n                          {displayParts.shownLabels.join(\", \")}\n                        </span>\n                        {displayParts.overflowCount > 0 && (\n                          <span\n                            className={cn(\n                              \"shrink-0 font-normal\",\n                              open && \"text-primary-fg\",\n                              !open && \"text-neutral-fg\",\n                            )}\n                          >\n                            +{displayParts.overflowCount}\n                          </span>\n                        )}\n                        <span className=\"sr-only\">\n                          {selectedLabels.join(\", \")}\n                        </span>\n                      </>\n                    )}\n                  {hasValues && displayMode === \"badge\" && (\n                    <>\n                      <span className=\"text-neutral-fg shrink-0\">:</span>\n                      <span\n                        className={cn(\n                          \"flex items-center gap-1.5 flex-nowrap ml-0.5\",\n                          isButtonWidthConstrained &&\n                            \"min-w-0 flex-1 overflow-hidden\",\n                        )}\n                      >\n                        {values.slice(0, maxDisplayItems).map((val) => {\n                          const label = allMultiOptions.find(\n                            (opt) => opt.value === val,\n                          )?.label;\n                          if (!label) return null;\n                          return (\n                            <Badge\n                              key={val}\n                              colorScheme=\"neutral\"\n                              size=\"sm\"\n                              style={{ minHeight: \"1.5rem\" }}\n                              className=\"overflow-visible! shrink-0\"\n                            >\n                              <span>{label}</span>\n                              <span\n                                role=\"button\"\n                                tabIndex={0}\n                                className=\"cursor-pointer rounded-full p-0 hover:bg-neutral-bg-active flex items-center justify-center -mr-1 pointer-events-auto focus:outline-none focus:ring-1 focus:ring-ring\"\n                                onClick={(e) => handleRemoveBadge(val, e)}\n                                onKeyDown={(e) => {\n                                  if (e.key === \"Enter\" || e.key === \" \") {\n                                    e.preventDefault();\n                                    handleRemoveBadge(\n                                      val,\n                                      e as unknown as React.MouseEvent,\n                                    );\n                                  }\n                                }}\n                                aria-label={`Remove ${label}`}\n                              >\n                                <Icon\n                                  path={mdiClose}\n                                  size={0.7}\n                                  className=\"pointer-events-none\"\n                                />\n                              </span>\n                            </Badge>\n                          );\n                        })}\n                      </span>\n                      {values.length > maxDisplayItems && (\n                        <span className=\"text-neutral-fg font-normal shrink-0 ml-0.5\">\n                          +{values.length - maxDisplayItems}\n                          <span className=\"sr-only\">\n                            {values.length - maxDisplayItems} more selected\n                          </span>\n                        </span>\n                      )}\n                    </>\n                  )}\n                </span>\n                {!(hasValues && showClear) && (\n                  <Icon\n                    path={mdiChevronDown}\n                    size={1.3}\n                    className=\"size-4 shrink-0 pointer-events-none text-current\"\n                  />\n                )}\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent\n              aria-label={ariaLabels?.listbox ?? groupLabel ?? placeholder}\n              className={cn(\n                FILTER_DROPDOWN_WIDTH,\n                FILTER_DROPDOWN_CONTAINER,\n                \"p-2 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 z-50 origin-(--radix-popover-content-transform-origin)\",\n                dropdownClassName,\n              )}\n              align=\"start\"\n            >\n              {searchable && showSearch && (\n                <div className=\"mb-2\">\n                  <SearchInput className=\"h-10 border-border has-[[data-slot=search-input-control]:focus-visible]:border-primary\">\n                    <SearchInputLeftElement>\n                      <Icon\n                        path={mdiMagnify}\n                        size={0.9}\n                        className=\"text-muted-foreground\"\n                      />\n                    </SearchInputLeftElement>\n                    <SearchInputField\n                      placeholder={searchPlaceholder}\n                      value={searchQuery}\n                      onChange={(e) => setSearchQuery(e.target.value)}\n                      aria-label={ariaLabels?.searchInput ?? searchPlaceholder}\n                      className=\"placeholder:text-muted-foreground\"\n                    />\n                    {searchQuery && (\n                      <SearchInputRightElement>\n                        <SearchInputClearButton\n                          onClear={() => setSearchQuery(\"\")}\n                          tooltipLabel=\"Clear search\"\n                        />\n                      </SearchInputRightElement>\n                    )}\n                  </SearchInput>\n                </div>\n              )}\n              <div className=\"max-h-80 w-full -mx-1 px-1 overflow-y-auto scrollbar-hidden\">\n                <div\n                  className=\"py-0.5\"\n                  role=\"group\"\n                  aria-label={ariaLabels?.listbox ?? groupLabel ?? placeholder}\n                >\n                  {groups ? (\n                    !filteredMultiGroups || filteredMultiGroups.length === 0 ? (\n                      <div className=\"py-6 text-center text-sm text-muted-foreground\">\n                        {noResultsText}\n                      </div>\n                    ) : (\n                      filteredMultiGroups.map((group) => (\n                        <React.Fragment key={group.label}>\n                          <div className=\"px-2 py-1 text-xs font-semibold uppercase text-muted-foreground\">\n                            {group.label}\n                          </div>\n                          {group.options.map((option) =>\n                            renderMultiOptionRow(option),\n                          )}\n                        </React.Fragment>\n                      ))\n                    )\n                  ) : groupLabel ? (\n                    <div className=\"px-2 py-1 text-xs font-semibold uppercase text-muted-foreground\">\n                      {groupLabel}\n                    </div>\n                  ) : null}\n                  {!groups &&\n                    (filteredOptions.length === 0 ? (\n                      <div className=\"py-6 text-center text-sm text-muted-foreground\">\n                        {noResultsText}\n                      </div>\n                    ) : (\n                      filteredOptions.map((option) =>\n                        renderMultiOptionRow(option),\n                      )\n                    ))}\n                </div>\n              </div>\n            </PopoverContent>\n          </Popover>\n          {showClear && hasValues && (\n            <Button\n              onClick={handleClear}\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              colorScheme=\"neutral\"\n              aria-label={ariaLabels?.clearSelection}\n              className=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-auto hover:bg-neutral-bg-active\"\n            >\n              <Icon path={mdiClose} size={0.75} />\n            </Button>\n          )}\n        </div>\n        {helperText && (\n          <p id={helperId} className=\"mt-1 text-xs text-muted-foreground\">\n            {helperText}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nFilterMultiSelect.displayName = \"FilterMultiSelect\";\n\n// FILTER BAR COMPONENT (Composable Layout)\n\nfunction FilterBar({\n  filters,\n  values = {},\n  onChange,\n  onClearAll,\n  showClearAll = true,\n  clearAllText = \"Clear all\",\n  direction = \"horizontal\",\n  gap = \"gap-3\",\n  className,\n  ariaLabel = \"Filters\",\n  \"aria-label\": ariaLabelNative,\n  \"aria-labelledby\": ariaLabelledBy,\n  ...props\n}: FilterBarProps & Omit<ComponentProps<\"div\">, \"onChange\">) {\n  const regionAccessibleName =\n    ariaLabelledBy != null && ariaLabelledBy !== \"\"\n      ? undefined\n      : (ariaLabelNative ?? ariaLabel);\n\n  const renderFilter = (filter: FilterDefinition) => {\n    const filterKey = `${filter.type}-${filter.key}`;\n    const filterValue = values[filter.key];\n\n    switch (filter.type) {\n      case \"input\":\n        return (\n          <FilterInput\n            key={filterKey}\n            {...filter.props}\n            value={filterValue as string | undefined}\n            onChange={(value) => onChange?.(filter.key, value)}\n          />\n        );\n      case \"single-select\":\n        return (\n          <FilterSingleSelect\n            key={filterKey}\n            {...filter.props}\n            value={filterValue as string | undefined}\n            onChange={(value) => onChange?.(filter.key, value)}\n          />\n        );\n      case \"multi-select\":\n        return (\n          <FilterMultiSelect\n            key={filterKey}\n            {...filter.props}\n            value={filterValue as string[] | undefined}\n            onChange={(value) => onChange?.(filter.key, value)}\n          />\n        );\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <div\n      role=\"region\"\n      aria-label={regionAccessibleName}\n      aria-labelledby={ariaLabelledBy}\n      className={cn(\n        \"flex\",\n        direction === \"horizontal\"\n          ? \"flex-row flex-wrap items-center\"\n          : \"flex-col items-start\",\n        gap,\n        className,\n      )}\n      {...props}\n    >\n      {filters.map(renderFilter)}\n      {showClearAll && onClearAll && (\n        <Button\n          onClick={onClearAll}\n          variant=\"link\"\n          size=\"sm\"\n          colorScheme=\"primary\"\n          className=\"w-fit\"\n        >\n          {clearAllText}\n        </Button>\n      )}\n    </div>\n  );\n}\n\nexport { FilterInput, FilterSingleSelect, FilterMultiSelect, FilterBar };\n",
      "type": "registry:ui"
    },
    {
      "path": "src/lib/icon.tsx",
      "content": "import type { SVGProps } from \"react\";\n\ntype IconProps = SVGProps<SVGSVGElement> & {\n  path: string;\n  title?: string;\n  size?: number | string;\n  className?: string;\n  fill?: string;\n};\n\nexport function Icon({\n  path,\n  title,\n  size = 1,\n  className,\n  fill = \"currentColor\",\n  ...rest\n}: IconProps) {\n  return (\n    <svg\n      transform={`scale(${size})`}\n      viewBox=\"0 0 24 24\"\n      aria-label={title}\n      className={className}\n      {...rest}\n    >\n      {title ? <title>{title}</title> : null}\n      <path d={path} fill={fill} />\n    </svg>\n  );\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}