{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "singleselect",
  "title": "Singleselect",
  "description": "Singleselect from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/card-default.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/singleselect.tsx",
      "content": "'use client';\n\nimport { Check, ChevronsUpDown, Loader2, X } from 'lucide-react';\nimport { Dispatch, FC, RefObject, SetStateAction, createContext, useContext, useEffect, useId, useRef, useState } from 'react';\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 { 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, SheetContent, 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. The shown value uses the destructive look and is announced as invalid.\n  invalid?: boolean;\n};\n\n// The current 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 isCurrentValueInvalid = ({\n  options,\n  selected,\n  invalidValues,\n  isValueInvalid,\n  loading,\n}: {\n  options: MultiSelectOption[];\n  selected: MultiSelectOptionId | null;\n  invalidValues?: MultiSelectOptionId[];\n  isValueInvalid?: (id: MultiSelectOptionId) => boolean;\n  loading?: boolean;\n}) => {\n  if (!selected) return false;\n  const option = options.find((o) => o.id === selected);\n  if (!option) return !loading;\n  return Boolean(option.invalid) || Boolean(invalidValues?.includes(selected)) || isValueInvalid?.(selected) === true;\n};\n\n// Create a context for managing the currently open SingleSelect\nexport const SingleSelectContext = createContext<{\n  openId: string | null;\n  setOpenId: Dispatch<SetStateAction<string | null>>;\n}>({\n  openId: null,\n  setOpenId: () => {},\n});\n\ninterface SingleSelectProviderProps {\n  children: React.ReactNode;\n}\n\nconst SingleSelectProvider: FC<SingleSelectProviderProps> = ({ children }) => {\n  const [openId, setOpenId] = useState<string | null>(null);\n\n  return <SingleSelectContext.Provider value={{ openId, setOpenId }}>{children}</SingleSelectContext.Provider>;\n};\n\nconst singleSelectLabelId = (id: string) => `${id}-label`;\n\n// The chevron names the field it opens: \"Open options\" plus the visible label (aria-labelledby, so the text follows the label), or plus the accessible name of the field when there is no visible label.\nconst toggleName = (openLabel: string, id: string, label: string | undefined, fallbackName: string) =>\n  label\n    ? ({ id: `${id}-toggle`, 'aria-label': openLabel, 'aria-labelledby': `${id}-toggle ${singleSelectLabelId(id)}` } as const)\n    : ({ 'aria-label': `${openLabel} ${fallbackName}` } as const);\n\n// cmdk takes Enter on its root (it selects the highlighted item and prevents the default). A button inside must keep Enter as its own activation while nothing is highlighted.\nconst letEnterThrough = (e: React.KeyboardEvent) => {\n  if (e.key === 'Enter') e.stopPropagation();\n};\n\n// Clear control of the chosen value: named, 24 x 24 target, keeps the focus in the field on pointer down.\nconst RemoveSelected: FC<{ label?: string; disabled?: boolean; onRemove: () => void }> = ({ label, disabled, onRemove }) => {\n  const l = useLabels();\n  return (\n    <button\n      type=\"button\"\n      aria-label={l.remove(label)}\n      className={cn(\n        'ml-1 flex h-6 w-6 items-center justify-center rounded-item text-icon-subtle outline-none hover:text-icon-default',\n        'focus-visible:ring-focus focus-visible:ring-ring-default focus-visible:ring-offset-focus focus-visible:ring-offset-background',\n        'disabled:pointer-events-none',\n      )}\n      onMouseDown={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n      }}\n      onKeyDown={letEnterThrough}\n      onClick={() => onRemove()}\n      disabled={disabled}\n    >\n      <X aria-hidden=\"true\" className={cn('h-icon-xs w-icon-xs', { hidden: disabled })} />\n    </button>\n  );\n};\n\nexport interface SingleSelectProps {\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 | null;\n  onChange: (id: string | null) => void;\n  onAddItemClick?: (item: string) => void;\n  placeholder?: string;\n  loading?: boolean;\n  defaultValue?: string;\n  label?: string;\n  // DOM id of the field (the combobox element). Label `htmlFor`, `aria-labelledby` and focus restoration point at it. Generated when omitted.\n  id?: string;\n  disabled?: boolean;\n  enableSearch?: boolean;\n  disableAddNew?: boolean;\n  decentToggle?: boolean;\n  disableRemove?: boolean;\n  useSheet?: boolean;\n  description?: string;\n  className?: string;\n  customAction?: React.ReactNode;\n  isMobile?: boolean;\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   * SingleSelect 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 to show as invalid when selected. Same as `invalid: true` on the option.\n  invalidValues?: MultiSelectOptionId[];\n  // Decides whether the current 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 value on the muted read-only surface. Stays focusable (`aria-readonly`), cannot be opened or changed.\n  readOnly?: boolean;\n}\n\nconst SingleSelectField: FC<SingleSelectProps> = ({\n  options = [],\n  selected = null,\n  onChange,\n  onAddItemClick,\n  placeholder: placeholderProp,\n  loading = false,\n  defaultValue = '',\n  label,\n  id: idProp,\n  disabled = false,\n  enableSearch = false,\n  disableAddNew = false,\n  decentToggle = false,\n  disableRemove = false,\n  useSheet = false,\n  description,\n  className,\n  customAction,\n  isMobile = false,\n  invalid = false,\n  ariaDescribedBy,\n  ariaLabel,\n  invalidValues,\n  isValueInvalid,\n}: SingleSelectProps) => {\n  const l = useLabels();\n  const generatedId = useId();\n  const id = idProp ?? generatedId;\n  const valueInvalid = isCurrentValueInvalid({ options, selected, invalidValues, isValueInvalid, loading });\n  const selectedLabel = options.find((option) => option.id === selected)?.label ?? selected;\n  const zero = usePreset() === 'zero';\n  const placeholder = placeholderProp ?? l.searchEllipsis;\n  const { openId, setOpenId } = useContext(SingleSelectContext);\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 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 [sheetOpen, setSheetOpen] = useState(false); // Sheet open state\n  const initialHeight = 44;\n\n  const open = openId === id;\n\n  // The id sits on the combobox element, which is not a tab stop. Focus (from focus restoration or a label click) moves on to the real control inside.\n  const focusField = () => {\n    if (disabled) return;\n    comboboxRef.current?.querySelector<HTMLElement>('input:not([disabled]), [data-select-toggle]:not([disabled])')?.focus();\n  };\n\n  // The id sits on a div, which a native label cannot activate. A click on any label for this field (the own one or one outside the component) moves focus into the field.\n  useEffect(() => {\n    const handleLabelClick = (e: MouseEvent) => {\n      if (!(e.target instanceof Element)) return;\n      const label = e.target.closest('label');\n      if (label && label.getAttribute('for') === id) focusField();\n    };\n    document.addEventListener('click', handleLabelClick);\n    return () => document.removeEventListener('click', handleLabelClick);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [id, disabled]);\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        dropdownRef.current?.setAttribute(\n          'style',\n          `position: fixed; top: 0; height: 100% !important; width: ${rectCombobox.width}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 dropdown = dropdownRef.current;\n    if (combobox) observer.observe(combobox);\n\n    return () => {\n      if (combobox) observer.unobserve(combobox);\n      if (dropdown) observer.unobserve(dropdown);\n      window.removeEventListener('resize', handleResize);\n    };\n  }, [open, selected, disableAddNew, useSheet]);\n\n  useEffect(() => {\n    if (open && inputRef.current && !isMobile && !useSheet) {\n      inputRef.current.focus();\n    }\n  }, [open, isMobile, useSheet]);\n\n  useEffect(() => {\n    setTimeout(() => {\n      inputRef.current?.blur();\n    }, 0);\n  }, []);\n\n  const isNewItem = (item: string) => options?.every((option) => option.label !== item.trim());\n\n  const handleUnselect = () => {\n    onChange(null);\n    // The remove button leaves the page with the value. Focus goes to the chevron, not to the body.\n    setTimeout(focusField, 0);\n  };\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      onChange(null);\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 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    return inputHeight + labelHeight;\n  };\n\n  const multiSelectInputProps = {\n    valueInvalid,\n    setSearch,\n    onChange,\n    comboboxRef,\n    height,\n    setOpenId,\n    open,\n    id,\n    disabled,\n    placeholder,\n    options,\n    selected,\n    handleUnselect,\n    inputRef,\n    search,\n    handleSearchChange,\n    handleKeyDown,\n    decentToggle,\n    useSheet,\n    label,\n    ariaLabel,\n  };\n\n  const multiSelectDropdownProps = {\n    id,\n    options,\n    selected,\n    onChange: (value: string | null) => {\n      onChange(value);\n      setSheetOpen(false); // Close the sheet when an option is selected\n    },\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    customAction,\n  };\n\n  return (\n    <>\n      {useSheet ? (\n        <Spacing direction=\"column\" gap=\"2\" wrap=\"nowrap\">\n          {label && (\n            <Label id={singleSelectLabelId(id)} htmlFor={id} aria-disabled={disabled || undefined} className={cn('leading-4', disabled && 'opacity-disabled')}>\n              {label}\n            </Label>\n          )}\n          <Sheet open={sheetOpen} onOpenChange={(isOpen) => setSheetOpen(isOpen)}>\n            <SheetTrigger asChild>\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 ? singleSelectLabelId(id) : undefined}\n                aria-disabled={disabled || undefined}\n                aria-label={label ? undefined : (ariaLabel ?? placeholder ?? l.selectOptions)}\n                aria-invalid={invalid || valueInvalid || undefined}\n                aria-describedby={ariaDescribedBy}\n                data-id={id}\n                id={id}\n                tabIndex={-1}\n                onFocus={(e) => {\n                  if (e.target === e.currentTarget) focusField();\n                }}\n                className={cn(\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                  'cursor-default',\n                  'rounded-default',\n                  'relative',\n                  'overflow-visible',\n                  { 'opacity-disabled cursor-not-allowed': disabled },\n                  className,\n                )}\n                onClick={() => setSheetOpen(true)}\n              >\n                <div\n                  className=\"flex gap-2 flex-wrap items-center flex-grow-default [&_[cmdk-input-wrapper]]:flex-grow-default\"\n                  onClick={() => setOpenId(id)}\n                >\n                  {selected ? (\n                    <div className={cn('flex items-center text-sm', valueInvalid && 'text-text-destructive')} data-invalid={valueInvalid || undefined}>\n                      {selectedLabel}\n                      {valueInvalid && <span className=\"sr-only\">{l.invalidSuffix}</span>}\n                      {!disableRemove && (\n                        <RemoveSelected label={selectedLabel ?? undefined} disabled={disabled} onRemove={handleUnselect} />\n                      )}\n                    </div>\n                  ) : (\n                    // Trigger is outside <Command>, so it must not render cmdk inputs; search lives inside the sheet\n                    <span className=\"text-text-subtle text-sm\">{placeholder}</span>\n                  )}\n                </div>\n                <Button\n                  type=\"button\"\n                  disabled={disabled}\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  {...toggleName(l.openOptions, id, label, ariaLabel ?? placeholder ?? l.selectOptions)}\n                  data-select-toggle=\"\"\n                  onKeyDown={(e) => {\n                    // Closed: Enter opens the list. Open: Enter stays with cmdk and picks the highlighted option.\n                    if (!open) letEnterThrough(e);\n                  }}\n                  className={cn(\n                    { 'hover:bg-transparent': decentToggle },\n                    'h-control-sm',\n                    '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                    setSheetOpen(!sheetOpen);\n                  }}\n                >\n                  <ChevronsUpDown className=\"h-4 w-4 min-w-[16px]\" />\n                </Button>\n              </div>\n            </SheetTrigger>\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            >\n              <SheetHeader className=\"mb-6\">\n                <Spacing direction=\"row\" justifyContent=\"between\" alignItems=\"center\">\n                  <SheetTitle>{label}</SheetTitle>\n                </Spacing>\n                {description && <span className=\"text-sm text-text-subtle\">{description}</span>}\n              </SheetHeader>\n              <Spacing direction=\"column\" gap=\"2\" className=\"relative\" wrap=\"nowrap\">\n                <Command className=\"w-full command\" vimBindings={false} {...{ [PERSISTENT_LIST]: '' }}>\n                  <CommandGroup className=\"[&_[cmdk-group-items]]:flex [&_[cmdk-group-items]]:flex-col [&_[cmdk-group-items]]:nowrap\">\n                    <SingleSelectInput {...multiSelectInputProps} useSheet={false} noButton />\n                    <SingleSelectDropdown {...multiSelectDropdownProps} />\n                  </CommandGroup>\n                </Command>\n              </Spacing>\n            </SheetContent>\n          </Sheet>\n        </Spacing>\n      ) : (\n        <Spacing direction=\"column\" gap=\"2\" className=\"relative\" wrap=\"nowrap\">\n          {label && (\n            <Label id={singleSelectLabelId(id)} htmlFor={id} aria-disabled={disabled || undefined} className={cn('leading-4', disabled && 'opacity-disabled')}>\n              {label}\n            </Label>\n          )}\n          <Command className=\"command w-full overflow-visible\" vimBindings={false}>\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 ? singleSelectLabelId(id) : undefined}\n              aria-disabled={disabled || undefined}\n              aria-label={label ? undefined : (ariaLabel ?? placeholder ?? l.selectOptions)}\n              aria-invalid={invalid || valueInvalid || undefined}\n              aria-describedby={ariaDescribedBy}\n              data-id={id}\n              id={id}\n              tabIndex={-1}\n              onFocus={(e) => {\n                if (e.target === e.currentTarget) focusField();\n              }}\n              className={cn(\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                'cursor-default',\n                'rounded-default',\n                'relative',\n                'overflow-visible',\n                { 'opacity-disabled cursor-not-allowed': disabled },\n                className,\n              )}\n            >\n              <div\n                className=\"flex gap-2 flex-wrap items-center flex-grow-default [&_[cmdk-input-wrapper]]:flex-grow-default\"\n                onClick={() => setOpenId(id)}\n              >\n                {selected ? (\n                  <div className={cn('flex items-center text-sm', valueInvalid && 'text-text-destructive')} data-invalid={valueInvalid || undefined}>\n                    {selectedLabel}\n                    {valueInvalid && <span className=\"sr-only\">{l.invalidSuffix}</span>}\n                    {!disableRemove && (\n                      <RemoveSelected label={selectedLabel ?? undefined} disabled={disabled} onRemove={handleUnselect} />\n                    )}\n                  </div>\n                ) : (\n                  <>{!enableSearch && <span className=\"text-text-subtle text-sm\">{placeholder}</span>}</>\n                )}\n                {enableSearch && (\n                  <CommandInput\n                    autoFocus={!isMobileDevice()}\n                    placeholder={placeholder}\n                    value={search}\n                    onValueChange={handleSearchChange}\n                    onKeyDown={handleKeyDown}\n                    ref={inputRef}\n                    disabled={disabled}\n                    className=\"flex-grow-default\"\n                  />\n                )}\n              </div>\n              <Button\n                type=\"button\"\n                disabled={disabled}\n                variant=\"ghost\"\n                size=\"sm\"\n                {...toggleName(l.openOptions, id, label, ariaLabel ?? placeholder ?? l.selectOptions)}\n                data-select-toggle=\"\"\n                onKeyDown={(e) => {\n                  // Closed: Enter opens the list. Open: Enter stays with cmdk and picks the highlighted option.\n                  if (!open) letEnterThrough(e);\n                }}\n                className={cn(\n                  { 'hover:bg-transparent': decentToggle },\n                  'h-control-sm',\n                  '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                <ChevronsUpDown className=\"h-4 w-4 min-w-[16px]\" />\n              </Button>\n            </div>\n            {open && (\n              <Card\n                className={`!gap-0 absolute !h-auto !p-2 z-20 transition-all duration-300 shadow-lg ${\n                  open ? 'animate-[fadeInGrow_0.3s_ease-out_forwards]' : 'animate-[fadeOutShrink_0.3s_ease-in_forwards]'\n                }`}\n                style={{\n                  top: topDistances(),\n                  bottom: dropdownDirection === 'up' ? height[0] : undefined,\n                }}\n                ref={dropdownRef}\n                shadow\n              >\n                {(!disableAddNew || customAction) && (\n                  <div>\n                    <Spacing alignItems=\"center\" direction=\"row\" justifyContent=\"between\">\n                      <Spacing direction=\"row\" className=\"self-end\" alignItems=\"center\" gap=\"2\">\n                        {!disableAddNew && !customAction && (\n                          <Button\n                            type=\"button\"\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={handleDialogSubmit}\n                            disabled={loading || disabled || !isNewItem(search)}\n                            className=\"px-2 text-sm\"\n                          >\n                            {loading ? (\n                              <Spacing direction=\"row\" gap=\"1\" alignItems=\"center\">\n                                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                {l.pleaseWait}\n                              </Spacing>\n                            ) : (\n                              l.addNew\n                            )}\n                          </Button>\n                        )}\n                        {customAction}\n                      </Spacing>\n                      <Button\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => setOpenId(null)}\n                        disabled={disabled}\n                        className=\"px-2 text-sm\"\n                      >\n                        {l.close}\n                      </Button>\n                    </Spacing>\n                    <hr className=\"mt-2 mb-4\" />\n                  </div>\n                )}\n                <div className=\"max-h-[250px] overflow-auto\">\n                  <CommandList>\n                    <CommandEmpty className=\"p-2 text-sm text-text-subtle text-center mt-2\">\n                      <Spacing direction=\"column\">\n                        <span>{l.notFound}</span>\n                        <span className=\"opacity-disabled\">{l.pressEnterToAdd}</span>\n                      </Spacing>\n                    </CommandEmpty>\n                    {options.map((option) => (\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).\n                          // The previous `aria-selected:!bg-slate-200` `!important` override\n                          // is dropped entirely: CommandItem (command.tsx) already applies\n                          // `aria-selected:bg-menu-background-hover aria-selected:text-foreground-accent`\n                          // for the keyboard/pointer-highlighted row, so no override is needed.\n                          'bg-background-accent text-foreground-accent': selected === option.id,\n                        })}\n                        key={option.id}\n                        onSelect={() => {\n                          if (!disabled) {\n                            onChange(option.id);\n                            setOpenId(null);\n                          }\n                        }}\n                        onKeyDown={(e) => {\n                          if (e.key === ' ' && !disabled) {\n                            // Space key - select item\n                            e.preventDefault();\n                            e.stopPropagation();\n                            onChange(option.id);\n                            setOpenId(null);\n                          } else if (e.key === 'Tab' && !e.shiftKey) {\n                            // Tab - return to search\n                            e.preventDefault();\n                            e.stopPropagation();\n                            if (enableSearch && inputRef.current) {\n                              inputRef.current.focus();\n                            }\n                          }\n                          // Let cmdk handle Enter and arrow keys\n                        }}\n                      >\n                        <Spacing direction=\"row\" alignItems=\"center\" gap=\"2\" wrap=\"nowrap\">\n                          {selected === option.id && <Check className=\"h-4 w-4\" />}\n                          <Label\n                            className={cn('text-left leading-5 p-1 !cursor-pointer', {\n                              'font-regular': selected !== option.id,\n                            })}\n                          >\n                            {option.label}\n                          </Label>\n                        </Spacing>\n                      </CommandItem>\n                    ))}\n                  </CommandList>\n                </div>\n              </Card>\n            )}\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            // The inline search field is gone once the list closed with Escape: focus goes to the field itself.\n            if (inputRef.current) inputRef.current.focus();\n            else focusField();\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    </>\n  );\n};\n\ninterface SingleSelectInputProps {\n  setSearch: Dispatch<SetStateAction<string>>;\n  onChange: (id: string | null) => void;\n  comboboxRef: RefObject<HTMLDivElement>;\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: string | null;\n  valueInvalid?: boolean;\n  handleUnselect: () => void;\n  inputRef: RefObject<HTMLInputElement>;\n  search: string;\n  handleSearchChange: (value: string) => void;\n  handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;\n  decentToggle?: boolean;\n  useSheet?: boolean;\n  noButton?: boolean;\n  label?: string;\n  ariaLabel?: string;\n}\n\nconst SingleSelectInput: FC<SingleSelectInputProps> = ({\n  setSearch,\n  onChange,\n  comboboxRef,\n  height,\n  setOpenId,\n  open,\n  id,\n  disabled,\n  placeholder,\n  options,\n  selected,\n  handleUnselect,\n  inputRef,\n  search,\n  handleSearchChange,\n  handleKeyDown,\n  decentToggle,\n  useSheet,\n  noButton = false,\n  label,\n  ariaLabel,\n  valueInvalid = false,\n}) => {\n  const l = useLabels();\n  const selectedLabel = options.find((option) => option.id === selected)?.label ?? selected;\n  const zero = usePreset() === 'zero';\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 ? singleSelectLabelId(id) : undefined}\n    aria-disabled={disabled || undefined}\n    aria-label={label ? undefined : (ariaLabel ?? placeholder ?? l.selectOptions)}\n    data-id={id}\n    className={cn(\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      'cursor-default',\n      'rounded-default',\n      'relative',\n      'overflow-visible',\n      { 'opacity-disabled cursor-not-allowed': disabled },\n    )}\n  >\n    <div\n      className=\"flex gap-2 flex-wrap items-center flex-grow-default [&_[cmdk-input-wrapper]]:flex-grow-default\"\n      onClick={() => setOpenId(id)}\n    >\n      {selected ? (\n        <div className={cn('flex items-center text-sm', valueInvalid && 'text-text-destructive')} data-invalid={valueInvalid || undefined}>\n          {selectedLabel}\n          {valueInvalid && <span className=\"sr-only\">{l.invalidSuffix}</span>}\n          {!disabled && (\n            <RemoveSelected label={selectedLabel ?? undefined} disabled={disabled} onRemove={handleUnselect} />\n          )}\n        </div>\n      ) : (\n        <>\n          {!useSheet && !disabled && !placeholder && (\n            <span className=\"text-text-subtle text-sm\">{placeholder}</span>\n          )}\n        </>\n      )}\n      {useSheet && <span className=\"flex-grow-default text-sm leading-button-sm text-text-subtle\">{l.addItems}</span>}\n      {!useSheet && (\n        <CommandInput\n          autoFocus={!isMobileDevice()}\n          placeholder={placeholder}\n          value={search}\n          onValueChange={handleSearchChange}\n          onKeyDown={handleKeyDown}\n          ref={inputRef}\n          disabled={disabled}\n          className=\"flex-grow-default\"\n        />\n      )}\n    </div>\n    {!noButton && (\n      <Button\n        type=\"button\"\n        disabled={disabled}\n        variant=\"ghost\"\n        size=\"sm\"\n        {...toggleName(l.openOptions, id, label, ariaLabel ?? placeholder ?? l.selectOptions)}\n        data-select-toggle=\"\"\n        onKeyDown={(e) => {\n          // Closed: Enter opens the list. Open: Enter stays with cmdk and picks the highlighted option.\n          if (!open) letEnterThrough(e);\n        }}\n        className={cn(\n          { 'hover:bg-transparent': decentToggle },\n          'h-control-sm',\n          '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        <ChevronsUpDown className=\"h-4 w-4 min-w-[16px]\" />\n      </Button>\n    )}\n  </div>\n  );\n};\n\ninterface SingleSelectDropdownProps {\n  id: string;\n  options: { label: string; id: string }[];\n  selected: string | null;\n  onChange: (id: string | null) => void;\n  handleDialogSubmit: () => void;\n  dropdownRef: 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: RefObject<HTMLInputElement>;\n  setOpenId: Dispatch<SetStateAction<string | null>>;\n  useSheet?: boolean;\n  setSheetOpen?: Dispatch<SetStateAction<boolean>>;\n  customAction?: React.ReactNode;\n}\n\nconst SingleSelectDropdown: FC<SingleSelectDropdownProps> = ({\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  setSheetOpen, // Receive the setSheetOpen prop\n  customAction,\n}) => {\n  const l = useLabels();\n  const isVisible = useSheet ? true : open;\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          { '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        {(!disableAddNew || customAction) && (\n          <div>\n            <Spacing alignItems=\"center\" direction=\"row\" justifyContent=\"between\">\n              <Spacing direction=\"row\" className=\"self-end\" alignItems=\"center\" gap=\"2\">\n                {!disableAddNew && !customAction && (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={handleDialogSubmit}\n                    disabled={loading || disabled || !isNewItem(search)}\n                    className=\"px-2 text-sm\"\n                  >\n                    {loading ? (\n                      <Spacing direction=\"row\" gap=\"1\" alignItems=\"center\">\n                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                        {l.pleaseWait}\n                      </Spacing>\n                    ) : (\n                      l.addNew\n                    )}\n                  </Button>\n                )}\n                {customAction}\n              </Spacing>\n              <SheetTrigger asChild>\n                <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={disabled} className=\"px-2 text-sm\">\n                  {l.close}\n                </Button>\n              </SheetTrigger>\n            </Spacing>\n            <hr className=\"mt-2 mb-4\" />\n          </div>\n        )}\n        <div>\n          <CommandList>\n            <CommandEmpty className=\"p-2 text-sm text-text-subtle text-center mt-2\">\n              <Spacing direction=\"column\">\n                <span>{l.notFound}</span>\n                <span className=\"opacity-disabled\">{l.pressEnterToAdd}</span>\n              </Spacing>\n            </CommandEmpty>\n            {options.map((option) => (\n              <CommandItem\n                className={cn({\n                  // Same fix as the non-sheet dropdown above (SELECT-MAT-04): token-based\n                  // selected treatment, no raw-palette `!important` override.\n                  'bg-background-accent text-foreground-accent': selected === option.id,\n                })}\n                key={option.id}\n                onSelect={() => {\n                  if (!disabled) {\n                    onChange(option.id);\n                    setSearch('');\n                    setOpenId(null);\n                    if (setSheetOpen) setSheetOpen(false); // Close the sheet if setSheetOpen is provided\n                  }\n                }}\n                onKeyDown={(e) => {\n                  if ((e.key === 'Enter' || e.key === ' ') && !disabled) {\n                    onChange(option.id);\n                    setSearch('');\n                    setOpenId(null);\n                    if (setSheetOpen) setSheetOpen(false); // Close the sheet if setSheetOpen is provided\n                  }\n                }}\n              >\n                <Spacing direction=\"row\" alignItems=\"center\" gap=\"2\" wrap=\"nowrap\">\n                  {selected === option.id && <Check className=\"h-4 w-4\" />}\n                  <Label\n                    className={cn('text-left leading-5 p-1 !cursor-pointer', {\n                      'font-regular': selected !== option.id,\n                    })}\n                  >\n                    {option.label}\n                  </Label>\n                </Spacing>\n              </CommandItem>\n            ))}\n          </CommandList>\n        </div>\n      </Card>\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 SingleSelect: FC<SingleSelectProps> = ({ readOnly = false, ...props }) => {\n  const l = useLabels();\n  const generatedId = useId();\n  const zero = usePreset() === 'zero';\n  if (!readOnly) {\n    return (\n      <LabelScope labels={labelScope(props)}>\n        <SingleSelectField {...props} />\n      </LabelScope>\n    );\n  }\n  const { label, options = [], selected = null, placeholder = l.searchEllipsis, className, invalid = false, ariaDescribedBy } = props;\n  const id = props.id ?? generatedId;\n  const text = options.find((option) => option.id === selected)?.label ?? selected;\n  const valueInvalid = isCurrentValueInvalid({ options, selected, invalidValues: props.invalidValues, isValueInvalid: props.isValueInvalid, loading: props.loading });\n  return (\n    <Spacing direction=\"column\" gap=\"2\" wrap=\"nowrap\">\n      {label && (\n        <Label id={singleSelectLabelId(id)} htmlFor={id} className=\"leading-4\">\n          {label}\n        </Label>\n      )}\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 ? singleSelectLabelId(id) : undefined}\n        aria-label={label ? undefined : (props.ariaLabel ?? placeholder)}\n        aria-invalid={invalid || valueInvalid || undefined}\n        aria-describedby={ariaDescribedBy}\n        data-readonly=\"\"\n        className={cn(\n          'flex w-full items-center border border-solid bg-muted text-sm',\n          zero ? 'h-control-md border-input px-control-px-sm' : 'h-10 border-form-border-default px-2',\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        {text ? (\n          <span className={cn(valueInvalid && 'text-text-destructive')} data-invalid={valueInvalid || undefined}>\n            {text}\n            {valueInvalid && <span className=\"sr-only\">{l.invalidSuffix}</span>}\n          </span>\n        ) : (\n          <span className=\"text-text-subtle\">{placeholder}</span>\n        )}\n      </div>\n    </Spacing>\n  );\n};\n\nexport { SingleSelect, SingleSelectProvider };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}