{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multiselect",
  "title": "Multiselect",
  "description": "Multiselect from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0",
    "react-dnd@^16.0.1",
    "react-dnd-html5-backend@^16.0.1"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/card-default.json",
    "https://zero-design.itdone.de/r/checkbox.json",
    "https://zero-design.itdone.de/r/chip.json",
    "https://zero-design.itdone.de/r/command.json",
    "https://zero-design.itdone.de/r/dialog.json",
    "https://zero-design.itdone.de/r/input.json",
    "https://zero-design.itdone.de/r/is-mobile.json",
    "https://zero-design.itdone.de/r/label.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/nested-layer.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/sheet.json",
    "https://zero-design.itdone.de/r/spacing.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/multiselect.tsx",
      "content": "'use client';\n\nimport { ChevronsUpDown, GripVertical, Loader2, PlusIcon, X } from 'lucide-react';\nimport { Dispatch, FC, ReactNode, SetStateAction, createContext, useContext, useEffect, useRef, useState } from 'react';\nimport { DndProvider, useDrag, useDrop } from 'react-dnd';\nimport { HTML5Backend } from 'react-dnd-html5-backend';\nimport { compositeFieldDefault, compositeFieldZero } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { isMobileDevice } from '@/lib/is-mobile';\nimport { Button } from '@/components/ui/button';\nimport { Card } from '@/components/ui/card/Card';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Chip } from '@/components/ui/chip';\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';\nimport { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from '@/components/ui/dialog';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';\nimport { PERSISTENT_LIST } from '@/lib/nested-layer';\nimport { Spacing } from '@/components/ui/spacing';\nimport { LabelScope, useLabels, usePreset } from '@/components/ui/labels-context';\n\nexport type MultiSelectOptionId = string;\n\nexport type MultiSelectOption = {\n  label: string;\n  id: MultiSelectOptionId;\n  // Marks this value as invalid. Its chip uses the destructive look and is announced as invalid.\n  invalid?: boolean;\n};\n\ntype InvalidValueProps = {\n  invalidValues?: MultiSelectOptionId[];\n  isValueInvalid?: (id: MultiSelectOptionId) => boolean;\n  loading?: boolean;\n};\n\n// A selected id that is not in the options is shown with its id as label (never an empty chip).\nconst resolveSelectedOption = (options: MultiSelectOption[] | undefined, id: MultiSelectOptionId): MultiSelectOption =>\n  options?.find((option) => option.id === id) ?? { id, label: id };\n\n// A selected value is invalid when the option says so, the consumer marks it, or it is not in the options (unless options are still loading).\nconst isSelectedValueInvalid = (options: MultiSelectOption[] | undefined, id: MultiSelectOptionId, { invalidValues, isValueInvalid, loading }: InvalidValueProps) => {\n  const option = options?.find((o) => o.id === id);\n  if (!option) return !loading;\n  return Boolean(option.invalid) || Boolean(invalidValues?.includes(id)) || isValueInvalid?.(id) === true;\n};\n\n// Create a context for managing the currently open MultiSelect\nexport const MultiSelectContext = createContext<{\n  openId: string | null;\n  setOpenId: Dispatch<SetStateAction<string | null>>;\n}>({\n  openId: null,\n  setOpenId: () => {},\n});\n\ninterface MultiSelectProviderProps {\n  children: React.ReactNode;\n}\n\nconst MultiSelectProvider: FC<MultiSelectProviderProps> = ({ children }) => {\n  const [openId, setOpenId] = useState<string | null>(null);\n\n  return <MultiSelectContext.Provider value={{ openId, setOpenId }}>{children}</MultiSelectContext.Provider>;\n};\n\nexport interface MultiSelectProps {\n  // `sm` puts the field on the Button sm height (control-sm token: 32, zero 28): one row, compact search input and toggle. Chips inside wrap with a tighter gap and padding. `default` keeps the control-md field.\n  size?: 'default' | 'sm';\n  // Built-in texts. Each wins over the provider text.\n  emptyText?: string;\n  addHintText?: string;\n  loadingText?: string;\n  addNewText?: string;\n  options: MultiSelectOption[];\n  selected: MultiSelectOptionId[];\n  onChange: Dispatch<SetStateAction<string[]>>;\n  onAddItemClick?: (item: string) => void;\n  placeholder?: string;\n  loading?: boolean;\n  defaultValue?: string;\n  label?: string;\n  id: string;\n  disabled?: boolean;\n  disableSearch?: boolean;\n  chipsInside?: boolean;\n  disableAddNew?: boolean;\n  decentToggle?: boolean;\n  useSheet?: boolean;\n  useDragAndDrop?: boolean;\n  dropdownClassName?: string;\n  chipsAsCol?: boolean;\n  disableCheckbox?: boolean;\n  hideCheckedItems?: boolean;\n  selectedOptionsAsText?: boolean;\n  renderItem?: (option: MultiSelectOption) => React.ReactNode;\n  onUpdateOrder?: (options: MultiSelectOption[]) => void;\n  onlyCheckOnCheckbox?: boolean;\n  orderCheckedOnTop?: boolean;\n  selectedOptionsAsListItems?: boolean;\n  chipVariant?: 'default' | 'secondary' | 'info' | 'success' | 'destructive';\n  customAction?: React.ReactNode;\n  customAddAction?: React.ReactNode;\n  description?: string;\n  useSheetText?: string;\n  useSheetTrigger?: React.ReactNode;\n  disableCheckAll?: boolean;\n  highlightChipById?: MultiSelectOptionId[];\n  isMobile?: boolean;\n  icon?: ReactNode;\n  mode?: 'default' | 'persistent';\n  applyButtonText?: string;\n  cancelButtonText?: string;\n  onCancel?: () => void;\n  disableBackdropClose?: boolean;\n  truncateChips?: boolean;\n  disableMaxChips?: boolean;\n  maxChipsToShow?: number;\n  /**\n   * Forces the error-token treatment (`border-error`) on the trigger and sets `aria-invalid`,\n   * mirroring `<SelectTrigger>`'s `aria-[invalid=true]:border-error` (select.tsx) so a required\n   * MultiSelect can show the same validation-error state as the base Select (SELECT-MAT-07).\n   */\n  invalid?: boolean;\n  /** Points to the id of an external error message, announced via `aria-describedby`. */\n  ariaDescribedBy?: string;\n  // Ids of selected values to show as invalid (destructive chip, announced as invalid). Same as `invalid: true` on the option.\n  invalidValues?: MultiSelectOptionId[];\n  // Decides per selected id whether the value is invalid. A selected id that is missing from `options` is always invalid (unless `loading`).\n  isValueInvalid?: (id: MultiSelectOptionId) => boolean;\n  // Accessible name of the field when there is no visible `label`. Falls back to the placeholder.\n  ariaLabel?: string;\n  // Shows the chosen values as plain chips on the muted read-only surface. Stays focusable (`aria-readonly`), cannot be opened or changed.\n  readOnly?: boolean;\n}\n\nconst multiSelectLabelId = (id?: string) => (id ? `${id}-label` : undefined);\n\nconst MultiSelectLabel: FC<{ label?: string; disabled?: boolean; htmlFor?: string }> = ({\n  label,\n  disabled = false,\n  htmlFor = 'multiselect',\n}) =>\n  label ? (\n    <Label id={multiSelectLabelId(htmlFor)} htmlFor={htmlFor} aria-disabled={disabled || undefined} className={cn('leading-4', disabled && 'opacity-disabled')}>\n      {label}\n    </Label>\n  ) : (\n    <></>\n  );\n\nconst MultiSelectInput: FC<\n  MultiSelectProps & {\n    comboboxRef: React.RefObject<HTMLDivElement>;\n    chipsInside?: boolean;\n    height: number[];\n    setOpenId: Dispatch<SetStateAction<string | null>>;\n    open: boolean;\n    id: string;\n    disabled?: boolean;\n    placeholder?: string;\n    options?: MultiSelectOption[];\n    selected?: MultiSelectOptionId[];\n    handleUnselect: (item: MultiSelectOptionId) => void;\n    disableSearch?: boolean;\n    inputRef: React.RefObject<HTMLInputElement>;\n    search: string;\n    setSearch: Dispatch<SetStateAction<string>>;\n    handleSearchChange: (value: string) => void;\n    handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;\n    decentToggle?: boolean;\n    noButton?: boolean;\n    onUpdateOrder: (options: MultiSelectOption[]) => void;\n    useSheetText?: string;\n    useSheetTrigger?: React.ReactNode;\n    disableMaxChips?: boolean;\n    maxChipsToShow?: number;\n  }\n> = ({\n  comboboxRef,\n  chipsInside,\n  height,\n  setOpenId,\n  open,\n  id,\n  disabled,\n  placeholder,\n  options,\n  selected,\n  handleUnselect,\n  disableSearch,\n  inputRef,\n  search,\n  handleSearchChange,\n  handleKeyDown,\n  decentToggle,\n  useSheet,\n  useDragAndDrop,\n  noButton = false,\n  onChange,\n  onUpdateOrder,\n  chipVariant,\n  useSheetText,\n  useSheetTrigger,\n  icon,\n  disableMaxChips,\n  maxChipsToShow = 3,\n  invalid,\n  ariaDescribedBy,\n  label,\n  ariaLabel,\n  invalidValues,\n  isValueInvalid,\n  loading,\n  size = 'default',\n}) => {\n  const l = useLabels();\n  const zero = usePreset() === 'zero';\n  const sm = size === 'sm';\n  const invalidProps = { invalidValues, isValueInvalid, loading };\n  const anyValueInvalid = (selected ?? []).some((id) => isSelectedValueInvalid(options, id, invalidProps));\n  return (\n  <div\n    ref={comboboxRef}\n    // eslint-disable-next-line jsx-a11y/role-has-required-aria-props -- ARIA 1.2: aria-controls is required only while the lazily mounted list is expanded\n    role=\"combobox\"\n    aria-expanded={open}\n    aria-haspopup=\"listbox\"\n    aria-labelledby={label ? multiSelectLabelId(id) : undefined}\n    aria-disabled={disabled || undefined}\n    aria-label={label ? undefined : (ariaLabel ?? placeholder ?? l.selectOptions)}\n    aria-invalid={invalid || anyValueInvalid || undefined}\n    aria-describedby={ariaDescribedBy}\n    data-id={id}\n    className={cn(\n      'z-20',\n      'flex',\n      'flex-row',\n      'items-center',\n      'bg-background',\n      'w-full',\n      'justify-between',\n      'border',\n      'border-solid',\n      ...(zero ? compositeFieldZero : compositeFieldDefault),\n      // Size sm: min height is the control-sm token (border included), after the composite recipe so it wins.\n      { 'min-h-[var(--size-control-sm)]': sm },\n      { 'cursor-text': !useSheet },\n      { 'cursor-pointer': useSheet },\n      'rounded-default',\n      'relative',\n      'overflow-visible',\n      { 'opacity-disabled cursor-not-allowed': disabled },\n      { 'py-2': chipsInside && !sm && height[0] > 44 },\n      { 'py-1': chipsInside && sm && height[0] > 32 },\n    )}\n  >\n    <div\n      className={cn(\n        'flex flex-wrap items-center flex-grow-default [&_[cmdk-input-wrapper]]:flex-grow-default',\n        sm ? 'gap-1' : 'gap-2',\n        zero || sm ? 'min-h-0' : 'min-h-[40px]',\n      )}\n      onClick={() => setOpenId(id)}\n    >\n      {chipsInside &&\n        (() => {\n          const visibleItems = disableMaxChips ? selected : selected?.slice(0, maxChipsToShow) || [];\n          const remainingCount = disableMaxChips ? 0 : (selected?.length || 0) - maxChipsToShow;\n\n          return (\n            <>\n              {visibleItems.map((item, index) => (\n                <DraggableChip\n                  key={item}\n                  option={resolveSelectedOption(options, item)}\n                  index={index}\n                  selected={selected}\n                  onChange={onChange}\n                  handleUnselect={handleUnselect}\n                  disabled={disabled}\n                  useDragAndDrop={useDragAndDrop}\n                  options={options}\n                  onUpdateOrder={onUpdateOrder}\n                  chipVariant={chipVariant}\n                  invalid={isSelectedValueInvalid(options, item, invalidProps)}\n                />\n              ))}\n              {remainingCount > 0 && (\n                <div\n                  className={cn(\n                    'flex items-center px-2 py-1 text-xs rounded-default bg-background-neutral-default text-text-subtle-on-muted',\n                    'select-none',\n                  )}\n                >\n                  +{remainingCount} {l.itemCount(remainingCount)}\n                </div>\n              )}\n            </>\n          );\n        })()}\n      {useSheet && (\n        <span className=\"flex-grow-default text-sm leading-button-sm text-text-subtle\">\n          {useSheetText || l.addItems}\n        </span>\n      )}\n      {!useSheet && !disableSearch && (\n        <CommandInput\n          icon={icon}\n          id={'multiselect-input' + id}\n          placeholder={placeholder}\n          value={search}\n          onValueChange={handleSearchChange}\n          onKeyDown={handleKeyDown}\n          ref={inputRef}\n          disabled={disabled}\n          className={cn('flex-grow-default', sm ? 'h-6 py-0' : 'h-7')}\n          autoFocus={false}\n        />\n      )}\n    </div>\n    {useSheet ? (\n      <Button\n        asChild\n        disabled={disabled}\n        variant=\"ghost\"\n        size=\"sm\"\n        className={cn(\n          { 'hover:bg-transparent': decentToggle },\n          'text-sm',\n          sm ? 'h-control-xs' : 'h-control-sm',\n          'p-2',\n          'flex',\n          'items-center',\n          'justify-center',\n          '[&_svg]:text-text-subtle',\n          '[&_svg]:active:text-foreground-accent',\n          'text-text-subtle',\n          'active:text-foreground-accent',\n          'flex flex-row gap-1',\n        )}\n        onClick={() => {\n          open ? setOpenId(null) : setOpenId(id);\n        }}\n      >\n        <span>\n          {useSheetTrigger || (\n            <>\n              <PlusIcon className=\"h-4 w-4 min-w-[16px]\" />\n              Add\n            </>\n          )}\n        </span>\n      </Button>\n    ) : (\n      <>\n        {!noButton && (\n          <Button\n            asChild\n            disabled={disabled}\n            variant=\"ghost\"\n            size=\"sm\"\n            className={cn(\n              { 'hover:bg-transparent': decentToggle },\n              sm ? 'h-control-xs' : 'h-control-sm',\n              sm ? 'w-control-xs' : 'w-10',\n              'flex',\n              'items-center',\n              'justify-center',\n              '[&_svg]:text-text-subtle',\n              '[&_svg]:active:text-foreground-accent',\n            )}\n            onClick={() => {\n              open ? setOpenId(null) : setOpenId(id);\n            }}\n          >\n            <span>\n              <ChevronsUpDown className=\"h-4 w-4 min-w-[16px]\" />\n            </span>\n          </Button>\n        )}\n      </>\n    )}\n  </div>\n  );\n};\n\nconst DraggableChip: FC<{\n  option: MultiSelectOption;\n  index: number;\n  selected: MultiSelectOptionId[];\n  onChange: Dispatch<SetStateAction<string[]>>;\n  handleUnselect: (item: MultiSelectOptionId) => void;\n  disabled?: boolean;\n  useDragAndDrop?: boolean;\n  selectedOptionsAsText?: boolean;\n  chipsAsCol?: boolean;\n  onUpdateOrder: (updatedOptions: MultiSelectOption[]) => void;\n  options: MultiSelectOption[];\n  selectedOptionsAsListItems?: boolean;\n  chipVariant?: 'default' | 'secondary' | 'info' | 'success' | 'destructive';\n  truncateChips?: boolean;\n  invalid?: boolean;\n}> = ({\n  option,\n  index,\n  handleUnselect,\n  disabled,\n  useDragAndDrop,\n  selectedOptionsAsText,\n  chipsAsCol,\n  onUpdateOrder,\n  options,\n  selectedOptionsAsListItems,\n  chipVariant,\n  truncateChips,\n  invalid,\n}) => {\n  const l = useLabels();\n  const zero = usePreset() === 'zero';\n  const ref = useRef<HTMLDivElement>(null);\n\n  const [, drop] = useDrop({\n    accept: 'chip',\n    hover(item: { index: number }) {\n      if (item.index === index) return;\n\n      const updatedOptions = [...options];\n      const draggedOption = updatedOptions.splice(item.index, 1)[0];\n      updatedOptions.splice(index, 0, draggedOption);\n\n      onUpdateOrder(updatedOptions);\n      item.index = index;\n    },\n  });\n\n  const [{ isDragging }, drag] = useDrag({\n    type: 'chip',\n    item: { type: 'chip', index },\n    collect: (monitor) => ({\n      isDragging: monitor.isDragging(),\n    }),\n  });\n\n  if (useDragAndDrop) {\n    drag(drop(ref));\n  }\n\n  return (\n    <>\n      {useDragAndDrop && (\n        <div\n          className={cn(\n            { 'border-2 border-solid border-primary rounded-checkbox': isDragging },\n            { 'h-auto': isDragging && !chipsAsCol },\n            { 'w-full': isDragging && chipsAsCol },\n          )}\n        />\n      )}\n      <div\n        ref={ref}\n        className={cn(\n          'flex items-center',\n          { 'list-item list-disc ml-4': selectedOptionsAsListItems },\n          { 'cursor-move': useDragAndDrop },\n          { ' w-fit': !chipsAsCol },\n          { ' w-full': chipsAsCol },\n          { 'p-0': selectedOptionsAsText },\n          { ' px-2 py-1': useDragAndDrop && !selectedOptionsAsText },\n          { 'bg-background-neutral-default rounded-default': isDragging },\n        )}\n      >\n        {useDragAndDrop && <GripVertical className=\"min-w-[15px] mr-2 text-text-subtle cursor-move\" />}\n        {selectedOptionsAsText ? (\n          <div\n            key={option?.id}\n            aria-invalid={invalid || undefined}\n            data-invalid={invalid || undefined}\n            className={cn(\n              'leading-0 text-sm flex flex-row items-center',\n              { 'text-text-destructive': invalid },\n              { 'cursor-move': useDragAndDrop },\n              { 'cursor-text': !useDragAndDrop },\n              { disabled: 'text-text-subtle' },\n            )}\n          >\n            {option?.label}\n            {invalid && <span className=\"sr-only\">{l.invalidSuffix}</span>}\n            <button\n              type=\"button\"\n              aria-label={l.remove(option?.label)}\n              className={cn(\n                'ml-1',\n                'h-6',\n                'w-6',\n                'rounded-item',\n                'outline-none',\n                'focus-visible:ring-focus',\n                'focus-visible:ring-ring-default',\n                'focus-visible:ring-offset-focus',\n                'focus-visible:ring-offset-background',\n                'flex justify-center items-center',\n                'text-icon-subtle hover:text-icon-default',\n                'disabled:pointer-events-none',\n              )}\n              onMouseDown={(e) => {\n                e.preventDefault();\n                e.stopPropagation();\n              }}\n              onClick={() => handleUnselect(option?.id)}\n              disabled={disabled}\n            >\n              <X aria-hidden=\"true\" className={cn('h-icon-xs', 'w-icon-xs', { hidden: disabled })} />\n            </button>\n          </div>\n        ) : (\n          <Chip\n            key={option?.id}\n            variant={chipVariant}\n            invalid={invalid}\n            tinted={invalid ? true : undefined}\n            size={zero ? 'compact' : undefined}\n            className={cn(\n              'items-center',\n              { 'cursor-move': useDragAndDrop },\n              { 'cursor-text': !useDragAndDrop },\n              { 'max-w-[200px]': truncateChips },\n              { 'border-error': invalid },\n            )}\n            disabled={disabled}\n            label={option?.label}\n            onRemove={disabled ? undefined : () => handleUnselect(option?.id)}\n            removeLabel={l.remove(option?.label)}\n          />\n        )}\n      </div>\n    </>\n  );\n};\n\nconst MultiSelectDropdownActions: FC<{\n  disableAddNew?: boolean;\n  disabled?: boolean;\n  loading?: boolean;\n  handleDialogSubmit: () => void;\n  search: string;\n  isNewItem: (item: string) => boolean;\n  setOpenId: Dispatch<SetStateAction<string | null>>;\n  useSheet?: boolean;\n  disableCheckAll?: boolean;\n  options: MultiSelectOption[];\n  selected: MultiSelectOptionId[];\n  onChange: Dispatch<SetStateAction<string[]>>;\n  mode?: 'default' | 'persistent';\n  hasChanges?: boolean;\n  onApply?: () => void;\n  onCancel?: () => void;\n  applyButtonText?: string;\n  cancelButtonText?: string;\n  disableBackdropClose?: boolean;\n  customAction?: React.ReactNode;\n  customAddAction?: React.ReactNode;\n}> = ({\n  disableAddNew,\n  disabled,\n  loading,\n  handleDialogSubmit,\n  search,\n  isNewItem,\n  setOpenId,\n  useSheet,\n  disableCheckAll,\n  options,\n  selected,\n  onChange,\n  mode,\n  hasChanges,\n  onApply,\n  onCancel,\n  applyButtonText,\n  cancelButtonText,\n  disableBackdropClose,\n  customAction,\n  customAddAction,\n}) => {\n  const l = useLabels();\n  const handleCheckAll = () => {\n    const allSelected = options.map((option) => option.id);\n    onChange(allSelected);\n  };\n\n  const handleUncheckAll = () => {\n    onChange([]);\n  };\n\n  const getCheckboxState = () => {\n    if (selected.length === options.length) {\n      return 'checked';\n    } else if (selected.length > 0) {\n      return 'indeterminate';\n    } else {\n      return 'unchecked';\n    }\n  };\n\n  return (\n    <>\n      <Spacing alignItems=\"center\" direction=\"row\" justifyContent=\"between\">\n        <Spacing direction=\"row\" alignItems=\"center\" justifyContent=\"start\">\n          {!disableCheckAll && (\n            <div className=\"flex justify-end\">\n              {getCheckboxState() === 'checked' ? (\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={handleUncheckAll}\n                  disabled={disabled}\n                  className=\"text-sm px-2\"\n                >\n                  {l.uncheckAll}\n                </Button>\n              ) : (\n                <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={handleCheckAll} disabled={disabled} className=\"text-sm px-2\">\n                  {getCheckboxState() === 'indeterminate'\n                    ? l.checkAll\n                    : getCheckboxState() === 'unchecked'\n                    ? l.checkAll\n                    : l.uncheckAll}\n                </Button>\n              )}\n            </div>\n          )}\n          {!disableAddNew && !customAction && !customAddAction && (\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              onClick={handleDialogSubmit}\n              disabled={loading || disabled || !isNewItem(search)}\n              className=\"text-sm px-2\"\n            >\n              {loading ? (\n                <Spacing direction=\"row\" gap=\"1\" alignItems=\"center\" className=\"text-sm\">\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  {l.pleaseWait}\n                </Spacing>\n              ) : (\n                l.add\n              )}\n            </Button>\n          )}\n          {customAddAction}\n        </Spacing>\n        <Spacing direction=\"row\" className=\"self-end\" alignItems=\"center\">\n          {mode === 'persistent' ? (\n            <>\n              <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onCancel} className=\"text-sm px-2 mr-2\">\n                {cancelButtonText || l.cancel}\n              </Button>\n              <Button type=\"button\" variant=\"default\" size=\"sm\" onClick={onApply} className=\"text-sm px-2\" disabled={!hasChanges}>\n                {applyButtonText || l.apply}\n              </Button>\n            </>\n          ) : (\n            <>\n              {useSheet ? (\n                <SheetClose asChild>\n                  <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setOpenId(null)} className=\"text-sm px-2\">\n                    {l.close} <span className=\"ml-1 font-regular\">{l.autosaveHint}</span>\n                  </Button>\n                </SheetClose>\n              ) : (\n                <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setOpenId(null)} className=\"text-sm px-2\">\n                  {l.close} <span className=\"ml-1 font-regular\">{l.autosaveHint}</span>\n                </Button>\n              )}\n            </>\n          )}\n        </Spacing>\n      </Spacing>\n      {/*\n        `text-general-warning-hover` — the same warning-role token Alert's `warning` variant\n        uses for its text (alert.tsx), not a raw `amber-600` swatch (SELECT-MAT-04).\n      */}\n      {mode === 'persistent' && hasChanges && (\n        <div className=\"mt-2 text-sm text-general-warning-hover text-center\">{l.unsavedChanges}</div>\n      )}\n      <hr className=\"mt-2 mb-4\" />\n    </>\n  );\n};\n\nconst MultiSelectDropdown: FC<\n  MultiSelectProps & {\n    id: string;\n    options: MultiSelectOption[];\n    selected: MultiSelectOptionId[];\n    onChange: Dispatch<SetStateAction<string[]>>;\n    handleDialogSubmit: () => void;\n    dropdownRef: React.RefObject<HTMLDivElement>;\n    search: string;\n    setSearch: Dispatch<SetStateAction<string>>;\n    dropdownDirection: 'down' | 'up';\n    isNewItem: (item: string) => boolean;\n    disableAddNew?: boolean;\n    disabled?: boolean;\n    loading?: boolean;\n    open: boolean;\n    topDistances: () => number | undefined;\n    height: number[];\n    inputRef: React.RefObject<HTMLInputElement>;\n    setOpenId: Dispatch<SetStateAction<string | null>>;\n    useSheet?: boolean;\n    useDragAndDrop?: boolean;\n    mode?: 'default' | 'persistent';\n    hasChanges?: boolean;\n    onApply?: () => void;\n    onCancel?: () => void;\n    applyButtonText?: string;\n    cancelButtonText?: string;\n    disableBackdropClose?: boolean;\n    customAddAction?: React.ReactNode;\n  }\n> = ({\n  id,\n  options,\n  selected,\n  onChange,\n  handleDialogSubmit,\n  dropdownRef,\n  search,\n  setSearch,\n  dropdownDirection,\n  isNewItem,\n  disableAddNew,\n  disabled,\n  loading,\n  open,\n  topDistances,\n  height,\n  inputRef,\n  setOpenId,\n  useSheet = false,\n  disableCheckbox,\n  hideCheckedItems,\n  renderItem,\n  onlyCheckOnCheckbox,\n  orderCheckedOnTop, // New prop\n  isMobile,\n  disableCheckAll,\n  mode,\n  hasChanges,\n  onApply,\n  onCancel,\n  applyButtonText,\n  cancelButtonText,\n  disableBackdropClose,\n  customAction,\n  customAddAction,\n}) => {\n  const l = useLabels();\n  const isVisible = useSheet ? true : open;\n\n  const multiSelectDropdownActionsProps = {\n    disableAddNew,\n    disableCheckAll,\n    disabled,\n    loading,\n    handleDialogSubmit,\n    search,\n    isNewItem,\n    setOpenId,\n    useSheet,\n    onlyCheckOnCheckbox,\n    options,\n    selected,\n    onChange,\n    mode,\n    hasChanges,\n    onApply,\n    onCancel,\n    applyButtonText,\n    cancelButtonText,\n    disableBackdropClose,\n    customAction,\n    customAddAction,\n  };\n\n  const multiSelectCommandItemProps = {\n    selected,\n    onChange,\n    setSearch,\n    inputRef,\n    setOpenId,\n    id,\n    disabled,\n    disableCheckbox,\n    renderItem,\n    onlyCheckOnCheckbox,\n    isMobile,\n  };\n\n  const sortedOptions = orderCheckedOnTop\n    ? options.slice().sort((a, b) => {\n        const aSelected = selected.includes(a.id);\n        const bSelected = selected.includes(b.id);\n        if (aSelected && !bSelected) return -1;\n        if (!aSelected && bSelected) return 1;\n        return 0;\n      })\n    : options;\n\n  return (\n    isVisible && (\n      <Card\n        className={cn(\n          'order-2',\n          '!gap-0',\n          '!h-auto',\n          { '!p-2': !useSheet },\n          { '!px-0': useSheet },\n          { '!py-2': useSheet },\n          'z-30',\n          { absolute: !useSheet },\n          { 'shadow-lg': !useSheet },\n          { 'mt-[-4px] z-10': useSheet },\n          `transition-all duration-300  ${\n            isVisible ? 'animate-[fadeInGrow_0.3s_ease-out_forwards]' : 'animate-[fadeOutShrink_0.3s_ease-in_forwards]'\n          }`,\n        )}\n        style={{\n          top: topDistances() || 0,\n          bottom: dropdownDirection === 'up' ? height[0] : undefined,\n        }}\n        noBorder={useSheet}\n        ref={dropdownRef}\n        shadow={!useSheet}\n      >\n        <div>\n          <MultiSelectDropdownActions {...multiSelectDropdownActionsProps} />\n\n          <CommandList>\n            <CommandEmpty className=\"px-2 pt-2 pb-4 text-sm text-text-subtle text-center\">\n              <Spacing direction=\"column\">\n                <span>{l.notFound}</span>\n                <span className=\"opacity-disabled\">{l.pressEnterToAdd}</span>\n              </Spacing>\n            </CommandEmpty>\n            {sortedOptions.length > 0 &&\n              sortedOptions\n                .filter((option) => !hideCheckedItems || !selected.includes(option.id))\n                .map((option) => (\n                  <MultiSelectCommandItem {...multiSelectCommandItemProps} option={option} key={option.id} />\n                ))}\n          </CommandList>\n        </div>\n      </Card>\n    )\n  );\n};\n\nconst MultiSelectCommandItem: FC<{\n  option: MultiSelectOption;\n  selected: MultiSelectOptionId[];\n  onChange: Dispatch<SetStateAction<string[]>>;\n  setSearch: Dispatch<SetStateAction<string>>;\n  inputRef: React.RefObject<HTMLInputElement>;\n  setOpenId: Dispatch<SetStateAction<string | null>>;\n  id: string;\n  disabled?: boolean;\n  disableCheckbox?: boolean;\n  renderItem?: (option: MultiSelectOption) => React.ReactNode;\n  onlyCheckOnCheckbox?: boolean;\n  isMobile?: boolean;\n}> = ({\n  option,\n  selected,\n  onChange,\n  setSearch,\n  inputRef,\n  setOpenId,\n  id,\n  disabled,\n  disableCheckbox,\n  renderItem,\n  onlyCheckOnCheckbox,\n  isMobile,\n}) => {\n  return (\n    <CommandItem\n      className={cn(\n        // Selected/checked item — same token pairing as Select's own\n        // `data-[state=checked]:bg-background-accent` (SELECT-MAT-04). The previous\n        // `aria-selected:!bg-slate-200`/`!border-slate-300` `!important` overrides are\n        // dropped entirely: CommandItem (command.tsx) already applies\n        // `aria-selected:bg-menu-background-hover aria-selected:text-foreground-accent` for\n        // the keyboard/pointer-highlighted row, and Checkbox already carries its own\n        // `border-checkbox-border-default` token for the unchecked state — no override needed.\n        { 'bg-background-accent text-foreground-accent': !onlyCheckOnCheckbox && selected?.includes(option.id) },\n        { '!bg-transparent !cursor-text': onlyCheckOnCheckbox },\n        'w-full',\n      )}\n      key={option.id}\n      onSelect={\n        onlyCheckOnCheckbox\n          ? undefined\n          : () => {\n              if (!disabled) {\n                onChange((prevSelected) =>\n                  prevSelected.includes(option.id)\n                    ? prevSelected.filter((item) => item !== option.id)\n                    : [...prevSelected, option.id],\n                );\n                // Keep search and selection state\n                setOpenId(id);\n              }\n            }\n      }\n      onKeyDown={(e) => {\n        if (e.key === ' ' && !disabled) {\n          // Space key - toggle selection\n          e.preventDefault();\n          e.stopPropagation();\n          onChange((prevSelected) =>\n            prevSelected.includes(option.id)\n              ? prevSelected.filter((item) => item !== option.id)\n              : [...prevSelected, option.id],\n          );\n          setOpenId(id);\n        } else if (e.key === 'Tab' && !e.shiftKey) {\n          // Tab - return to search\n          e.preventDefault();\n          e.stopPropagation();\n          inputRef.current?.focus();\n        }\n        // Let cmdk handle Enter and arrow keys\n      }}\n    >\n      <Spacing direction=\"row\" alignItems=\"center\" gap=\"2\" wrap=\"nowrap\" className=\"w-full\">\n        {!disableCheckbox && (\n          <Checkbox\n            className=\"ml-2\"\n            checked={selected?.includes(option.id)}\n            disabled={disabled}\n            onClick={\n              onlyCheckOnCheckbox\n                ? () => {\n                    if (!disabled) {\n                      onChange((prevSelected) =>\n                        prevSelected.includes(option.id)\n                          ? prevSelected.filter((item) => item !== option.id)\n                          : [...prevSelected, option.id],\n                      );\n                      // Keep search and selection state when clicking checkbox\n                      setOpenId(id);\n                    }\n                  }\n                : undefined\n            }\n          />\n        )}\n        {renderItem ? (\n          renderItem(option)\n        ) : (\n          <Label\n            className={cn(\n              { 'font-regular ': !selected?.includes(option.id) },\n              'text-left leading-5 p-1 !cursor-pointer truncate',\n            )}\n            title={option.label}\n          >\n            {option.label}\n          </Label>\n        )}\n      </Spacing>\n    </CommandItem>\n  );\n};\n\nconst MultiSelectField: FC<MultiSelectProps> = ({\n  options = [],\n  selected = [],\n  onChange,\n  onAddItemClick,\n  placeholder: placeholderProp,\n  loading = false,\n  defaultValue = '',\n  label,\n  id,\n  disabled = false,\n  disableSearch = false,\n  chipsInside = false,\n  disableAddNew = false,\n  decentToggle = false,\n  useSheet = false,\n  useDragAndDrop = false,\n  dropdownClassName = '[&_[cmdk-list-sizer]]:max-h-[250px]',\n  chipsAsCol = false,\n  disableCheckbox = false,\n  hideCheckedItems = false,\n  selectedOptionsAsText = false,\n  selectedOptionsAsListItems = false,\n  renderItem,\n  onUpdateOrder = () => {},\n  onlyCheckOnCheckbox = false,\n  orderCheckedOnTop = false,\n  chipVariant = 'info',\n  customAction = null,\n  customAddAction = null,\n  description,\n  useSheetText,\n  useSheetTrigger,\n  disableCheckAll = false,\n  highlightChipById = [],\n  isMobile = false,\n  icon,\n  mode = 'default',\n  applyButtonText: applyButtonTextProp,\n  cancelButtonText: cancelButtonTextProp,\n  onCancel,\n  disableBackdropClose = false,\n  truncateChips = false,\n  disableMaxChips = false,\n  maxChipsToShow = 3,\n  invalid = false,\n  ariaDescribedBy,\n  ariaLabel,\n  invalidValues,\n  isValueInvalid,\n  size = 'default',\n}) => {\n  const l = useLabels();\n  const placeholder = placeholderProp ?? l.searchEllipsis;\n  const applyButtonText = applyButtonTextProp ?? l.apply;\n  const cancelButtonText = cancelButtonTextProp ?? l.cancel;\n  const { openId, setOpenId } = useContext(MultiSelectContext);\n  const [search, setSearch] = useState(defaultValue);\n  const [height, setHeight] = useState<number[]>([]);\n  const [dropdownDirection, setDropdownDirection] = useState<'down' | 'up'>('down');\n  const labelHeight = label ? 36 : 0;\n  const comboboxRef = useRef<HTMLDivElement | null>(null);\n  const chipWrapperRef = useRef<HTMLDivElement | null>(null);\n  const dropdownRef = useRef<HTMLDivElement | null>(null);\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const [dialogOpen, setDialogOpen] = useState(false);\n  const [newItemLabel, setNewItemLabel] = useState('');\n  const initialHeight = size === 'sm' ? 32 : 44;\n\n  // State for persistent mode\n  const [tempSelected, setTempSelected] = useState<string[]>(selected);\n  const [hasChanges, setHasChanges] = useState(false);\n\n  const open = openId === id;\n\n  // Update temp selections when selected prop changes (for persistent mode)\n  useEffect(() => {\n    if (mode === 'persistent') {\n      setTempSelected(selected);\n      setHasChanges(false);\n    }\n  }, [selected, mode]);\n\n  useEffect(() => {\n    if (search !== '') {\n      setOpenId(id);\n    }\n  }, [search, setOpenId, id]);\n\n  useEffect(() => {\n    const handleClick = (e: MouseEvent) => {\n      if (e.target instanceof Element && !e.target.closest('.command')) {\n        setOpenId(null);\n      }\n    };\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        setOpenId(null);\n      }\n    };\n\n    document.addEventListener('click', handleClick);\n    document.addEventListener('keydown', handleKeyDown);\n\n    return () => {\n      document.removeEventListener('click', handleClick);\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [setOpenId]);\n\n  useEffect(() => {\n    if (useSheet) return;\n    const handleResize = () => {\n      if (!comboboxRef.current) return;\n      const rectCombobox = comboboxRef.current.getBoundingClientRect();\n      const rectDropdown = dropdownRef.current?.getBoundingClientRect();\n      const viewportHeight = window.innerHeight;\n\n      if (rectCombobox.top > viewportHeight - rectCombobox.bottom) {\n        setDropdownDirection('up');\n      } else {\n        setDropdownDirection('down');\n      }\n\n      if (rectCombobox.top < 208 && viewportHeight - rectCombobox.bottom < 208) {\n        if (!dropdownRef.current) return;\n\n        dropdownRef.current?.setAttribute(\n          'style',\n          `position: fixed; top: 0; height: 100% !important; width: ${\n            rectCombobox.width ? rectCombobox.width : 0\n          }px !important;`,\n        );\n        dropdownRef.current\n          .querySelector('[role=\"listbox\"]')\n          ?.setAttribute(\n            'style',\n            `max-height: ${disableAddNew ? viewportHeight - 16 : viewportHeight - 16 - 50}px !important;`,\n          );\n      }\n\n      if (!rectDropdown?.height) return;\n    };\n\n    const handleHeight = (entries: ResizeObserverEntry[]) => {\n      const heightArr = [initialHeight, initialHeight];\n      for (const [index, entry] of entries.entries()) {\n        const { height } = entry.contentRect;\n        heightArr[index] = height === 0 ? initialHeight : height;\n      }\n      setHeight(heightArr);\n    };\n\n    const observer = new ResizeObserver((entries) => {\n      handleHeight(entries);\n      handleResize();\n    });\n\n    // Capture the nodes so cleanup unobserves exactly what this effect observed\n    const combobox = comboboxRef.current;\n    const chipWrapper = chipWrapperRef.current;\n    const dropdown = dropdownRef.current;\n    if (combobox) observer.observe(combobox);\n    if (chipWrapper) observer.observe(chipWrapper);\n\n    return () => {\n      if (combobox) observer.unobserve(combobox);\n      if (chipWrapper) observer.unobserve(chipWrapper);\n      if (dropdown) observer.unobserve(dropdown);\n      window.removeEventListener('resize', handleResize);\n    };\n  }, [open, selected, disableAddNew, useSheet, initialHeight]);\n\n  useEffect(() => {\n    const isMobileDev = isMobileDevice();\n    if (open && inputRef.current && !isMobileDev && !useSheet) {\n      inputRef.current.focus();\n    }\n  }, [open, useSheet]);\n\n  useEffect(() => {\n    setTimeout(() => {\n      inputRef.current?.blur();\n    }, 0);\n  }, []);\n\n  // Check for changes in persistent mode\n  useEffect(() => {\n    if (mode === 'persistent') {\n      const hasChanges = JSON.stringify(tempSelected.sort()) !== JSON.stringify(selected.sort());\n      setHasChanges(hasChanges);\n    }\n  }, [tempSelected, selected, mode]);\n\n  const handleUnselect = (item: MultiSelectOptionId) => {\n    if (mode === 'persistent') {\n      setTempSelected((prevSelected) => prevSelected.filter((i) => i !== item));\n    } else {\n      onChange((prevSelected) => prevSelected.filter((i) => i !== item));\n    }\n  };\n\n  const handleApply = () => {\n    onChange(tempSelected);\n    if (!disableBackdropClose) {\n      setOpenId(null);\n    }\n    setHasChanges(false);\n  };\n\n  const handleCancel = () => {\n    setTempSelected(selected);\n    setHasChanges(false);\n    if (onCancel) {\n      onCancel();\n    } else if (!disableBackdropClose) {\n      setOpenId(null);\n    }\n  };\n\n  const isNewItem = (item: string) => options?.every((option) => option.label !== item.trim());\n\n  const handleSearchChange = (value: string) => {\n    setSearch(value);\n  };\n\n  const handleDialogSubmit = () => {\n    if (isNewItem(search)) {\n      setDialogOpen(true);\n      setNewItemLabel(search.trim());\n    }\n  };\n\n  const handleAddNewItem = () => {\n    if (onAddItemClick && isNewItem(newItemLabel)) {\n      onAddItemClick(newItemLabel);\n      setNewItemLabel('');\n      setDialogOpen(false);\n      setTimeout(() => {\n        inputRef.current?.focus();\n      }, 0);\n    }\n  };\n\n  const hasFilteredMatches = (searchTerm: string) => {\n    if (!searchTerm.trim()) return true; // Empty search shows all items\n    const normalizedSearch = searchTerm.toLowerCase().trim();\n    return options.some((option) => option.label.toLowerCase().includes(normalizedSearch));\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Backspace' && search === '') {\n      e.preventDefault();\n      if (mode === 'persistent') {\n        setTempSelected((prevSelected) => prevSelected.slice(0, -1));\n      } else {\n        onChange((prevSelected) => prevSelected.slice(0, -1));\n      }\n    }\n\n    // Handle Enter key - only trigger add new when there are NO matches\n    if (e.key === 'Enter' && search.trim() && !disableAddNew) {\n      // Only prevent default and show add dialog if there are no filtered matches\n      if (!hasFilteredMatches(search) && isNewItem(search)) {\n        e.preventDefault();\n        setNewItemLabel(search.trim());\n        setDialogOpen(true);\n      }\n      // Otherwise, let cmdk handle the Enter key to select/toggle the first match\n    }\n\n    // Note: Arrow keys are handled by cmdk itself for navigation\n    // We don't need to handle them manually\n  };\n\n  const topDistances = () => {\n    if (dropdownDirection === 'up') return undefined;\n\n    const inputHeight = height[0];\n    const chipsHeight = height[1];\n    const chipsOutside = !chipsInside;\n\n    if (chipsOutside) {\n      const currentSelected = mode === 'persistent' ? tempSelected : selected;\n      if (currentSelected.length === 0) {\n        return inputHeight + labelHeight;\n      } else {\n        return inputHeight + chipsHeight + labelHeight + 4;\n      }\n    }\n\n    if (chipsInside) {\n      return inputHeight < initialHeight ? inputHeight + labelHeight : inputHeight + labelHeight + (size === 'sm' ? 4 : 12);\n    }\n  };\n\n  const multiSelectInputProps = {\n    setSearch,\n    onChange: mode === 'persistent' ? setTempSelected : onChange,\n    comboboxRef,\n    chipsInside,\n    height,\n    setOpenId,\n    open,\n    id,\n    disabled,\n    placeholder,\n    options,\n    selected: mode === 'persistent' ? tempSelected : selected,\n    handleUnselect,\n    disableSearch,\n    inputRef,\n    search,\n    handleSearchChange,\n    handleKeyDown,\n    decentToggle,\n    useSheet,\n    onUpdateOrder,\n    chipVariant,\n    useSheetText,\n    useSheetTrigger,\n    icon,\n    disableMaxChips,\n    maxChipsToShow,\n    invalid,\n    ariaDescribedBy,\n    label,\n    ariaLabel,\n    invalidValues,\n    isValueInvalid,\n    loading,\n    size,\n  };\n\n  const multiSelectChipsProps = {\n    selected: mode === 'persistent' ? tempSelected : selected,\n    options,\n    handleUnselect,\n    chipWrapperRef,\n    disabled,\n    chipsInside,\n    onChange: mode === 'persistent' ? setTempSelected : onChange,\n    useDragAndDrop,\n    id,\n    chipsAsCol,\n    selectedOptionsAsText,\n    onUpdateOrder,\n    selectedOptionsAsListItems,\n    chipVariant,\n    highlightChipById,\n    truncateChips,\n    disableMaxChips,\n    maxChipsToShow,\n    invalidValues,\n    isValueInvalid,\n    loading,\n  };\n\n  const multiSelectDropdownProps = {\n    id,\n    options,\n    selected: mode === 'persistent' ? tempSelected : selected,\n    onChange: mode === 'persistent' ? setTempSelected : onChange,\n    handleDialogSubmit,\n    dropdownRef,\n    search,\n    setSearch,\n    dropdownDirection,\n    isNewItem,\n    disableAddNew,\n    disabled,\n    loading,\n    open,\n    topDistances,\n    height,\n    inputRef,\n    setOpenId,\n    useSheet,\n    useDragAndDrop,\n    dropdownClassName,\n    disableCheckbox,\n    hideCheckedItems,\n    renderItem,\n    onUpdateOrder,\n    onlyCheckOnCheckbox,\n    orderCheckedOnTop,\n    disableCheckAll,\n    isMobile,\n    mode,\n    hasChanges,\n    onApply: handleApply,\n    onCancel: handleCancel,\n    applyButtonText,\n    cancelButtonText,\n    customAction,\n    customAddAction,\n  };\n\n  return (\n    <DndProvider backend={HTML5Backend}>\n      {useSheet ? (\n        <Sheet>\n          <Spacing direction=\"column\" gap=\"2\" wrap=\"nowrap\">\n            {customAction ? (\n              <Spacing direction=\"row\" justifyContent=\"between\" alignItems=\"center\">\n                <MultiSelectLabel label={label} disabled={disabled} htmlFor={id} />\n                {customAction}\n              </Spacing>\n            ) : (\n              <MultiSelectLabel label={label} disabled={disabled} htmlFor={id} />\n            )}\n            {description && <span className=\"text-sm text-text-subtle leading-lg\">{description}</span>}\n            {!chipsInside && <MultiSelectChips {...multiSelectChipsProps} />}\n            <SheetTrigger className=\"w-full text-left\">\n              <MultiSelectInput {...multiSelectInputProps} />\n            </SheetTrigger>\n          </Spacing>\n          <SheetContent\n            onOpenAutoFocus={(event) => {\n              // A select sheet opens in its search field, not on a chip or the first control.\n              const input = (event.currentTarget as HTMLElement).querySelector<HTMLInputElement>('input:not([disabled])');\n              if (input) {\n                event.preventDefault();\n                input.focus();\n              }\n            }}\n            onAfterClose={() => setSearch('')}\n            side={isMobile || isMobileDevice() ? 'bottom' : 'right'}\n            className={cn('overflow-y-auto', (isMobile || isMobileDevice()) && 'h-[90vh]')}\n            onInteractOutside={disableBackdropClose ? (e) => e.preventDefault() : undefined}\n          >\n            <SheetHeader className=\"mb-6\">\n              {customAction ? (\n                <Spacing direction=\"row\" justifyContent=\"between\" alignItems=\"center\">\n                  <SheetTitle>{label}</SheetTitle>\n                  <div className=\"mr-4\">{customAction}</div>\n                </Spacing>\n              ) : (\n                <SheetTitle>{label}</SheetTitle>\n              )}\n              {description && <span className=\"text-sm text-text-subtle leading-lg\">{description}</span>}\n            </SheetHeader>\n            <Spacing direction=\"column\" gap=\"2\" className=\"relative\" wrap=\"nowrap\">\n              {!chipsInside && <MultiSelectChips {...multiSelectChipsProps} />}\n              <Command className=\"w-full command\" vimBindings={false} {...{ [PERSISTENT_LIST]: '' }}>\n                <MultiSelectInput {...multiSelectInputProps} useSheet={false} noButton />\n                <CommandGroup className=\"[&_[cmdk-group-items]]:flex [&_[cmdk-group-items]]:flex-col [&_[cmdk-group-items]]:nowrap\">\n                  <MultiSelectDropdown {...multiSelectDropdownProps} useSheet={true} />\n                </CommandGroup>\n              </Command>\n            </Spacing>\n            {mode === 'persistent' && disableBackdropClose && (\n              <SheetFooter className=\"mt-4\">\n                <SheetClose asChild>\n                  <Button type=\"button\" variant=\"ghost\" size=\"sm\">\n                    {l.close}\n                  </Button>\n                </SheetClose>\n              </SheetFooter>\n            )}\n          </SheetContent>\n        </Sheet>\n      ) : (\n        <Spacing direction=\"column\" gap=\"2\" className=\"relative\" wrap=\"nowrap\">\n          {customAction ? (\n            <Spacing direction=\"row\" justifyContent=\"between\" alignItems=\"center\">\n              <MultiSelectLabel label={label} disabled={disabled} htmlFor={id} />\n              {customAction}\n            </Spacing>\n          ) : (\n            <MultiSelectLabel label={label} disabled={disabled} htmlFor={id} />\n          )}\n          {description && <span className=\"text-sm text-text-subtle leading-lg\">{description}</span>}\n          {!chipsInside && <MultiSelectChips {...multiSelectChipsProps} />}\n          <Command className=\"w-full command\" vimBindings={false}>\n            <MultiSelectInput {...multiSelectInputProps} />\n            <CommandList className={dropdownClassName}>\n              <CommandGroup>\n                <MultiSelectDropdown {...multiSelectDropdownProps} />\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </Spacing>\n      )}\n      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n        <DialogTrigger asChild></DialogTrigger>\n        {/* Escape closes the dialog like Cancel; focus goes back into the field, since the dialog trigger is empty. */}\n        <DialogContent\n          onCloseAutoFocus={(event) => {\n            event.preventDefault();\n            (inputRef.current ?? comboboxRef.current?.querySelector<HTMLElement>('input:not([disabled]), button:not([disabled])'))?.focus();\n          }}\n        >\n          <DialogTitle>{l.add}</DialogTitle>\n          <DialogDescription>\n            <Input\n              placeholder={l.enterNew(label)}\n              value={newItemLabel}\n              onChange={(e) => setNewItemLabel(e.target.value)}\n              className=\"w-full border border-solid rounded-default px-2 py-1\"\n              disabled={disabled}\n            />\n          </DialogDescription>\n          <div className=\"mt-4 flex justify-end\">\n            <DialogClose asChild>\n              <Button type=\"button\" size=\"sm\" variant=\"ghost\" className=\"ml-2\" disabled={disabled}>\n                {l.cancel}\n              </Button>\n            </DialogClose>\n            <Button\n              type=\"button\"\n              size=\"sm\"\n              onClick={handleAddNewItem}\n              variant=\"default\"\n              disabled={disabled || newItemLabel.trim() === ''}\n            >\n              {l.send}\n            </Button>\n          </div>\n        </DialogContent>\n      </Dialog>\n    </DndProvider>\n  );\n};\n\nconst MultiSelectChips: FC<\n  MultiSelectProps & {\n    chipWrapperRef: React.RefObject<HTMLDivElement>;\n    handleUnselect: (item: MultiSelectOptionId) => void;\n    disabled?: boolean;\n    chipsInside?: boolean;\n    useDragAndDrop?: boolean;\n    onUpdateOrder: (options: MultiSelectOption[]) => void;\n    highlightChipById?: MultiSelectOptionId[];\n    truncateChips?: boolean;\n    disableMaxChips?: boolean;\n    maxChipsToShow?: number;\n  } & InvalidValueProps\n> = ({\n  selected,\n  options,\n  handleUnselect,\n  chipWrapperRef,\n  disabled,\n  onChange,\n  useDragAndDrop,\n  chipsAsCol,\n  selectedOptionsAsText,\n  onUpdateOrder,\n  selectedOptionsAsListItems,\n  chipVariant,\n  highlightChipById = [],\n  truncateChips,\n  disableMaxChips = false,\n  maxChipsToShow = 3,\n  invalidValues,\n  isValueInvalid,\n  loading,\n}) => {\n  const l = useLabels();\n  const invalidProps = { invalidValues, isValueInvalid, loading };\n  const visibleItems = disableMaxChips ? selected : selected.slice(0, maxChipsToShow);\n  const remainingCount = disableMaxChips ? 0 : selected.length - maxChipsToShow;\n\n  return (\n    selected.length > 0 && (\n      <div ref={chipWrapperRef} className={cn('flex flex-wrap gap-1 items-stretch', { 'flex-col': chipsAsCol })}>\n        {visibleItems.map((item, index) => (\n          <DraggableChip\n            key={item}\n            option={resolveSelectedOption(options, item)}\n            index={index}\n            selected={selected}\n            onChange={onChange}\n            handleUnselect={handleUnselect}\n            disabled={disabled}\n            useDragAndDrop={useDragAndDrop}\n            selectedOptionsAsText={selectedOptionsAsText}\n            chipsAsCol={chipsAsCol}\n            onUpdateOrder={onUpdateOrder}\n            options={selected.map((id) => resolveSelectedOption(options, id))}\n            selectedOptionsAsListItems={selectedOptionsAsListItems}\n            chipVariant={highlightChipById.includes(item) ? 'info' : chipVariant}\n            truncateChips={truncateChips}\n            invalid={isSelectedValueInvalid(options, item, invalidProps)}\n          />\n        ))}\n        {remainingCount > 0 && (\n          <div\n            className={cn(\n              'flex items-center px-2 py-1 text-xs rounded-default bg-background-neutral-default text-text-subtle-on-muted',\n              'select-none',\n            )}\n          >\n            +{remainingCount} {l.itemCount(remainingCount)}\n          </div>\n        )}\n      </div>\n    )\n  );\n};\n\nconst labelScope = (props: { emptyText?: string; addHintText?: string; loadingText?: string; addNewText?: string }) => ({\n  ...(props.emptyText !== undefined && { notFound: props.emptyText }),\n  ...(props.addHintText !== undefined && { pressEnterToAdd: props.addHintText }),\n  ...(props.loadingText !== undefined && { pleaseWait: props.loadingText }),\n  ...(props.addNewText !== undefined && { addNew: props.addNewText }),\n});\n\nconst MultiSelect: FC<MultiSelectProps> = ({ readOnly = false, ...props }) => {\n  const l = useLabels();\n  const zero = usePreset() === 'zero';\n  if (!readOnly) {\n    return (\n      <LabelScope labels={labelScope(props)}>\n        <MultiSelectField {...props} />\n      </LabelScope>\n    );\n  }\n  const { id, label, options = [], selected = [], placeholder = l.searchEllipsis, className, invalid = false, ariaDescribedBy } = props as MultiSelectProps & { className?: string };\n  const chosen = selected.map((id) => resolveSelectedOption(options, id));\n  const invalidProps = { invalidValues: props.invalidValues, isValueInvalid: props.isValueInvalid, loading: props.loading };\n  const anyValueInvalid = chosen.some((option) => isSelectedValueInvalid(options, option.id, invalidProps));\n  return (\n    <Spacing direction=\"column\" gap=\"2\" wrap=\"nowrap\">\n      {label && <MultiSelectLabel label={label} htmlFor={id} />}\n      <div\n        id={id}\n        // eslint-disable-next-line jsx-a11y/role-has-required-aria-props -- ARIA 1.2: aria-controls is required only while the lazily mounted list is expanded\n        role=\"combobox\"\n        tabIndex={0}\n        aria-readonly=\"true\"\n        aria-expanded=\"false\"\n        aria-haspopup=\"listbox\"\n        aria-labelledby={label ? multiSelectLabelId(id) : undefined}\n        aria-label={label ? undefined : (props.ariaLabel ?? placeholder)}\n        aria-invalid={invalid || anyValueInvalid || undefined}\n        aria-describedby={ariaDescribedBy}\n        data-readonly=\"\"\n        className={cn(\n          'flex w-full flex-wrap items-center gap-2 border border-solid bg-muted text-sm',\n          zero ? 'min-h-[var(--size-control-md)] border-input px-control-px-sm py-1' : 'border-form-border-default px-2 py-1',\n          props.size === 'sm' && 'min-h-[var(--size-control-sm)] gap-1 py-0',\n          'rounded-default focus-visible:outline-none focus-visible:ring-focus focus-visible:ring-offset-focus focus-visible:ring-offset-background focus-visible:ring-ring-default focus-visible:border-ring',\n          'aria-[invalid=true]:border-error',\n          className,\n        )}\n      >\n        {chosen.length > 0 ? (\n          chosen.map((option) => {\n            const optionInvalid = isSelectedValueInvalid(options, option.id, invalidProps);\n            return <Chip key={option.id} label={option.label} variant=\"outline\" size=\"control\" invalid={optionInvalid} tinted={optionInvalid ? true : undefined} className={cn(optionInvalid && 'border-error')} />;\n          })\n        ) : (\n          <span className=\"text-text-subtle\">{placeholder}</span>\n        )}\n      </div>\n    </Spacing>\n  );\n};\n\nexport {\n  MultiSelect,\n  MultiSelectChips,\n  MultiSelectCommandItem,\n  MultiSelectDropdown,\n  MultiSelectDropdownActions,\n  MultiSelectInput,\n  MultiSelectLabel,\n  MultiSelectProvider,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}