{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "type": "registry:ui",
  "title": "MultiSelect",
  "description": "React Aria backed searchable multi-select with removable chips, repeated form values, disabled items, invalid state, density, dark mode, and provider-aware popover styling.",
  "dependencies": [
    "react-aria@^3.52.1",
    "react-aria-components@^1.21.1"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/multi-select/multi-select.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/multi-select/multi-select.tsx",
      "content": "import {\n  Button as AriaButton,\n  ComboBox as AriaCombobox,\n  FieldError,\n  Input as AriaInput,\n  Label,\n  ListBox,\n  ListBoxItem,\n  Popover,\n  Text,\n  type ComboBoxProps as AriaComboboxProps,\n  type ListBoxItemProps as AriaListBoxItemProps,\n} from \"react-aria-components\";\nimport {\n  Children,\n  cloneElement,\n  type ForwardedRef,\n  forwardRef,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  type RefAttributes,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { toPlainText } from \"../../utils/to-plain-text\";\nimport {\n  DethinkPortalProvider,\n  useProviderPortalRoot,\n} from \"../../utils/provider-portal\";\n\nexport type MultiSelectValue = string;\nexport type MultiSelectControlSize = \"sm\" | \"md\" | \"lg\";\n\nexport type MultiSelectItemData = {\n  label?: ReactNode;\n  textValue?: string;\n  value: MultiSelectValue;\n};\n\nexport interface MultiSelectProps<\n  T extends MultiSelectItemData = MultiSelectItemData,\n> extends Omit<\n  AriaComboboxProps<T, \"multiple\">,\n  | \"children\"\n  | \"className\"\n  | \"defaultInputValue\"\n  | \"defaultValue\"\n  | \"disabledKeys\"\n  | \"inputValue\"\n  | \"isDisabled\"\n  | \"isInvalid\"\n  | \"isReadOnly\"\n  | \"isRequired\"\n  | \"items\"\n  | \"menuTrigger\"\n  | \"name\"\n  | \"onChange\"\n  | \"onInputChange\"\n  | \"onOpenChange\"\n  | \"placeholder\"\n  | \"selectionMode\"\n  | \"validationBehavior\"\n  | \"value\"\n> {\n  \"aria-invalid\"?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\";\n  \"data-slot\"?: string;\n  children?: ReactNode | ((item: T) => ReactNode);\n  className?: string;\n  clearLabel?: string;\n  controlSize?: MultiSelectControlSize;\n  defaultInputValue?: string;\n  defaultValue?: MultiSelectValue[];\n  description?: ReactNode;\n  disabled?: boolean;\n  disabledKeys?: Iterable<MultiSelectValue>;\n  emptyMessage?: ReactNode;\n  errorMessage?: ReactNode;\n  inputValue?: string;\n  invalid?: boolean;\n  items?: Iterable<T>;\n  label?: ReactNode;\n  name?: string;\n  onInputValueChange?: (value: string) => void;\n  onOpenChange?: (open: boolean) => void;\n  onValueChange?: (value: MultiSelectValue[]) => void;\n  placeholder?: string;\n  readOnly?: boolean;\n  required?: boolean;\n  searchLabel?: string;\n  searchPlaceholder?: string;\n  selectedItems?: Iterable<T>;\n  selectedLabel?: string;\n  /** Disable when items have already been filtered by a server. */\n  shouldFilter?: boolean;\n  value?: MultiSelectValue[];\n}\n\nexport interface MultiSelectItemProps extends Omit<\n  AriaListBoxItemProps<MultiSelectItemData>,\n  \"children\" | \"className\" | \"id\" | \"isDisabled\" | \"value\"\n> {\n  children?: ReactNode;\n  className?: string;\n  disabled?: boolean;\n  value: MultiSelectValue;\n}\n\ntype MultiSelectResolvedItem = {\n  label: ReactNode;\n  textValue: string;\n  value: MultiSelectValue;\n};\n\nconst multiSelectRootBaseClasses =\n  \"group/multi-select grid w-full min-w-0 gap-[var(--dt-space-2)] text-foreground\";\n\nconst multiSelectLabelClasses =\n  \"text-sm font-medium leading-none text-foreground data-[disabled=true]:opacity-60 data-[invalid=true]:text-destructive\";\n\nconst multiSelectControlBaseClasses =\n  \"flex w-full min-w-0 items-center gap-[var(--dt-space-2)] rounded-md border border-input bg-background text-foreground shadow-sm outline-none motion-safe:transition-[background-color,border-color,box-shadow,color] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/20 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-60 data-[invalid=true]:border-destructive data-[invalid=true]:ring-2 data-[invalid=true]:ring-destructive/15 data-[readonly=true]:bg-muted/40 data-[readonly=true]:text-muted-foreground group-data-[open]/multi-select:border-ring\";\n\nconst multiSelectControlSizeClasses: Record<MultiSelectControlSize, string> = {\n  sm: \"min-h-8 px-[var(--dt-space-2)] py-[var(--dt-space-1)] text-base sm:text-sm\",\n  md: \"min-h-density-control px-[var(--dt-space-2-5)] py-[var(--dt-space-1)] text-base sm:text-sm\",\n  lg: \"min-h-11 px-[var(--dt-space-3)] py-[var(--dt-space-1-5)] text-base\",\n};\n\nconst multiSelectValueClasses =\n  \"flex min-w-0 flex-1 flex-wrap items-center gap-[var(--dt-space-1-5)]\";\n\nconst multiSelectInputClasses =\n  \"min-w-[7rem] flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60 read-only:text-muted-foreground\";\n\nconst multiSelectChipClasses =\n  \"inline-flex max-w-full items-center gap-[var(--dt-space-1)] motion-safe:animate-feedback-in rounded-sm border border-border bg-muted px-[var(--dt-space-1-5)] py-0.5 text-xs font-medium leading-5 text-foreground\";\n\nconst multiSelectChipTextClasses = \"min-w-0 truncate\";\n\nconst multiSelectChipRemoveClasses =\n  \"inline-flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none motion-safe:transition-[background-color,color] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control hover:bg-background hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\";\n\nconst multiSelectTriggerClasses =\n  \"inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none motion-safe:transition-[background-color,color,translate,scale] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[hovered]:bg-muted data-[hovered]:text-foreground data-[pressed]:translate-y-px data-[pressed]:bg-muted\";\n\nconst multiSelectClearClasses =\n  \"inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none motion-safe:transition-[background-color,color] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\";\n\nconst multiSelectIconClasses =\n  \"size-4 motion-safe:transition-transform motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control group-data-[open]/multi-select:rotate-180\";\n\nconst multiSelectHelpClasses = \"text-xs leading-5 text-muted-foreground\";\n\nconst multiSelectErrorClasses =\n  \"text-xs font-medium leading-5 text-destructive motion-safe:animate-feedback-in\";\n\nconst multiSelectPopoverClasses =\n  \"z-50 max-h-80 min-w-[var(--trigger-width)] overflow-hidden rounded-md border border-border bg-background text-foreground shadow-lg outline-none origin-top data-[placement=top]:origin-bottom data-[placement=left]:origin-right data-[placement=right]:origin-left motion-safe:data-[entering]:animate-overlay-in motion-safe:data-[exiting]:animate-overlay-out motion-reduce:animate-none\";\n\nconst multiSelectListBoxClasses =\n  \"max-h-64 overflow-auto p-[var(--dt-space-1)] outline-none\";\n\nconst multiSelectItemBaseClasses =\n  \"grid cursor-default grid-cols-[1rem_minmax(0,1fr)] items-center gap-[var(--dt-space-2)] rounded-sm px-[var(--dt-space-2)] py-[var(--dt-space-1-5)] text-sm text-foreground outline-none motion-safe:transition-[background-color,color,box-shadow] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focused]:bg-muted data-[hovered]:bg-muted data-[pressed]:bg-muted/80 data-[selected]:bg-primary/15 data-[selected]:text-foreground\";\n\nconst multiSelectItemIndicatorClasses =\n  \"flex size-4 items-center justify-center text-primary\";\n\nconst multiSelectItemContentClasses = \"min-w-0 truncate\";\n\nconst multiSelectEmptyClasses =\n  \"px-[var(--dt-space-3)] py-[var(--dt-space-4)] text-sm text-muted-foreground\";\n\nconst multiSelectSrOnlyClasses = \"sr-only\";\n\ntype MultiSelectComponent = (<\n  T extends MultiSelectItemData = MultiSelectItemData,\n>(\n  props: MultiSelectProps<T> & RefAttributes<HTMLDivElement>,\n) => ReactElement | null) & { displayName?: string };\n\nfunction isAriaInvalid(value: MultiSelectProps[\"aria-invalid\"]) {\n  return (\n    value === true ||\n    value === \"true\" ||\n    value === \"grammar\" ||\n    value === \"spelling\"\n  );\n}\n\nfunction toDisabledKeys(disabledKeys: Iterable<MultiSelectValue> | undefined) {\n  return disabledKeys ? Array.from(disabledKeys) : undefined;\n}\n\nfunction getItemLabel(item: MultiSelectItemData) {\n  return item.label ?? item.textValue ?? item.value;\n}\n\nfunction getItemTextValue(item: MultiSelectItemData) {\n  return item.textValue || toPlainText(item.label) || item.value;\n}\n\nfunction collectDataItems<T extends MultiSelectItemData>(\n  items: Iterable<T> | undefined,\n) {\n  if (!items) {\n    return [];\n  }\n\n  return Array.from(items, (item) => ({\n    label: getItemLabel(item),\n    textValue: getItemTextValue(item),\n    value: item.value,\n  }));\n}\n\nfunction collectStaticItems(children: ReactNode): MultiSelectResolvedItem[] {\n  const collected: MultiSelectResolvedItem[] = [];\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement<MultiSelectItemProps>(child)) {\n      return;\n    }\n\n    const { children: itemChildren, textValue, value } = child.props;\n\n    if (!value) {\n      return;\n    }\n\n    collected.push({\n      label: itemChildren ?? textValue ?? value,\n      textValue: textValue || toPlainText(itemChildren) || value,\n      value,\n    });\n  });\n\n  return collected;\n}\n\nfunction filterItems<T extends MultiSelectItemData>({\n  inputValue,\n  items,\n}: {\n  inputValue: string;\n  items: T[];\n}) {\n  const query = inputValue.trim().toLocaleLowerCase();\n\n  if (!query) {\n    return items;\n  }\n\n  return items.filter((item) =>\n    getItemTextValue(item).toLocaleLowerCase().includes(query),\n  );\n}\n\nfunction filterStaticChildren({\n  children,\n  inputValue,\n}: {\n  children: ReactNode;\n  inputValue: string;\n}) {\n  const query = inputValue.trim().toLocaleLowerCase();\n\n  if (!query) {\n    return children;\n  }\n\n  return Children.toArray(children).filter((child) => {\n    if (!isValidElement<MultiSelectItemProps>(child)) {\n      return true;\n    }\n\n    const { children: itemChildren, textValue, value } = child.props;\n    const candidate = textValue || toPlainText(itemChildren) || value;\n\n    return candidate.toLocaleLowerCase().includes(query);\n  });\n}\n\nfunction renderMultiSelectChildren<T extends MultiSelectItemData>({\n  children,\n  inputValue,\n  items,\n  onItemAction,\n}: Pick<MultiSelectProps<T>, \"children\" | \"items\"> & {\n  inputValue: string;\n  onItemAction: (value: MultiSelectValue) => void;\n}) {\n  if (items && typeof children === \"function\") {\n    return filterItems({ inputValue, items: Array.from(items) }).map((item) => {\n      const child = children(item);\n\n      if (isValidElement(child)) {\n        const typedChild = child as ReactElement<MultiSelectItemProps>;\n        const existingOnAction = typedChild.props.onAction;\n\n        return cloneElement(typedChild, {\n          key: child.key ?? item.value,\n          textValue:\n            typedChild.props.textValue ||\n            toPlainText(typedChild.props.children) ||\n            typedChild.props.value,\n          onAction: () => {\n            existingOnAction?.();\n            onItemAction(item.value);\n          },\n        });\n      }\n\n      return child;\n    });\n  }\n\n  return Children.toArray(\n    filterStaticChildren({\n      children: children as ReactNode,\n      inputValue,\n    }),\n  ).map((child) => {\n    if (!isValidElement<MultiSelectItemProps>(child)) {\n      return child;\n    }\n\n    const existingOnAction = child.props.onAction;\n\n    return cloneElement(child, {\n      textValue:\n        child.props.textValue ||\n        toPlainText(child.props.children) ||\n        child.props.value,\n      onAction: () => {\n        existingOnAction?.();\n        onItemAction(child.props.value);\n      },\n    });\n  });\n}\n\nfunction syncAriaInvalidAttribute(\n  element: HTMLElement | null,\n  value: MultiSelectProps[\"aria-invalid\"],\n) {\n  if (!element) {\n    return;\n  }\n\n  if (value === undefined) {\n    element.removeAttribute(\"aria-invalid\");\n    return;\n  }\n\n  element.setAttribute(\"aria-invalid\", String(value));\n}\n\nexport function multiSelectClassNames({\n  className,\n}: Pick<MultiSelectProps, \"className\"> = {}) {\n  return cn(multiSelectRootBaseClasses, className);\n}\n\nexport function multiSelectItemClassNames({\n  className,\n}: Pick<MultiSelectItemProps, \"className\"> = {}) {\n  return cn(multiSelectItemBaseClasses, className);\n}\n\nfunction ChevronDownIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-full\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n    >\n      <path\n        d=\"m4 6 4 4 4-4\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction CheckIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-3.5\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n    >\n      <path\n        d=\"m3.5 8 3 3 6-6.5\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2\"\n      />\n    </svg>\n  );\n}\n\nfunction XIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-3.5\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n    >\n      <path\n        d=\"m4.5 4.5 7 7m0-7-7 7\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction MultiSelectRoot<T extends MultiSelectItemData = MultiSelectItemData>(\n  {\n    \"aria-invalid\": ariaInvalid,\n    \"data-slot\": dataSlot,\n    children,\n    className,\n    clearLabel = \"Clear selected options\",\n    controlSize = \"md\",\n    defaultInputValue = \"\",\n    defaultValue = [],\n    description,\n    disabled = false,\n    disabledKeys,\n    emptyMessage = \"No options found.\",\n    errorMessage,\n    inputValue,\n    invalid = false,\n    items,\n    label,\n    name,\n    onInputValueChange,\n    onOpenChange,\n    onValueChange,\n    placeholder = \"Select options\",\n    readOnly = false,\n    required = false,\n    searchLabel,\n    searchPlaceholder = \"Search options\",\n    selectedItems,\n    selectedLabel,\n    shouldFilter = true,\n    value,\n    ...props\n  }: MultiSelectProps<T>,\n  ref: ForwardedRef<HTMLDivElement>,\n) {\n  const resolvedInvalid = invalid || isAriaInvalid(ariaInvalid);\n  const resolvedAriaInvalid = invalid ? true : ariaInvalid;\n  const isControlled = value !== undefined;\n  const [uncontrolledValue, setUncontrolledValue] =\n    useState<MultiSelectValue[]>(defaultValue);\n  const selectedValues = value ?? uncontrolledValue;\n  const inputControlled = inputValue !== undefined;\n  const [uncontrolledInputValue, setUncontrolledInputValue] =\n    useState(defaultInputValue);\n  const resolvedInputValue = inputValue ?? uncontrolledInputValue;\n  const dataItems = collectDataItems(items);\n  const selectedDataItems = collectDataItems(selectedItems);\n  const staticItems = useMemo(\n    () =>\n      typeof children === \"function\"\n        ? []\n        : collectStaticItems(children as ReactNode),\n    [children],\n  );\n  const resolvedItems = useMemo(\n    () => [\n      ...(dataItems.length > 0 ? dataItems : staticItems),\n      ...selectedDataItems,\n    ],\n    [dataItems, staticItems, selectedDataItems],\n  );\n  const itemLookup = useMemo(() => {\n    const map = new Map<MultiSelectValue, MultiSelectResolvedItem>();\n\n    for (const item of resolvedItems) {\n      map.set(item.value, item);\n    }\n\n    return map;\n  }, [resolvedItems]);\n  const controlRef = useRef<HTMLDivElement | null>(null);\n  const { portalContainer, rootRef } = useProviderPortalRoot<HTMLDivElement>({\n    forwardedRef: ref,\n    portalSlot: \"multi-select-portal-container\",\n  });\n  const setControlRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      controlRef.current = node;\n      syncAriaInvalidAttribute(node, resolvedAriaInvalid);\n    },\n    [resolvedAriaInvalid],\n  );\n  const selectedCount = selectedValues.length;\n  const resolvedSelectedLabel =\n    selectedLabel ??\n    `${selectedCount} option${selectedCount === 1 ? \"\" : \"s\"} selected`;\n  const disabledOrReadOnly = disabled || readOnly;\n  const updateValue = useCallback(\n    (nextValue: MultiSelectValue[]) => {\n      if (readOnly) {\n        return;\n      }\n\n      if (!isControlled) {\n        setUncontrolledValue(nextValue);\n      }\n\n      onValueChange?.(nextValue);\n    },\n    [isControlled, onValueChange, readOnly],\n  );\n  const toggleValue = useCallback(\n    (selectedValue: MultiSelectValue) => {\n      if (selectedValues.includes(selectedValue)) {\n        updateValue(\n          selectedValues.filter(\n            (currentValue) => currentValue !== selectedValue,\n          ),\n        );\n        return;\n      }\n\n      updateValue([...selectedValues, selectedValue]);\n    },\n    [selectedValues, updateValue],\n  );\n  const renderedChildren = renderMultiSelectChildren({\n    children,\n    inputValue: shouldFilter ? resolvedInputValue : \"\",\n    items,\n    onItemAction: toggleValue,\n  });\n  const renderedChildrenCount = Children.count(renderedChildren);\n\n  useEffect(() => {\n    syncAriaInvalidAttribute(controlRef.current, resolvedAriaInvalid);\n  }, [resolvedAriaInvalid]);\n\n  return (\n    <DethinkPortalProvider container={portalContainer}>\n      <AriaCombobox<T, \"multiple\">\n        {...props}\n        ref={rootRef}\n        value={selectedValues}\n        selectionMode=\"multiple\"\n        inputValue={resolvedInputValue}\n        onInputChange={(nextValue) => {\n          if (!inputControlled) {\n            setUncontrolledInputValue(nextValue);\n          }\n\n          onInputValueChange?.(nextValue);\n        }}\n        onOpenChange={(isOpen) => {\n          if (!readOnly) {\n            onOpenChange?.(isOpen);\n          }\n        }}\n        disabledKeys={toDisabledKeys(disabledKeys)}\n        isDisabled={disabled}\n        isReadOnly={readOnly}\n        isRequired={required}\n        isInvalid={resolvedInvalid}\n        aria-invalid={resolvedAriaInvalid}\n        menuTrigger=\"input\"\n        allowsEmptyCollection\n        validationBehavior=\"aria\"\n        data-slot={dataSlot ?? \"multi-select\"}\n        data-size={controlSize}\n        data-readonly={readOnly ? \"true\" : undefined}\n        className={multiSelectClassNames({ className })}\n      >\n        {label ? (\n          <Label\n            data-slot=\"multi-select-label\"\n            data-disabled={disabled ? \"true\" : undefined}\n            data-invalid={resolvedInvalid ? \"true\" : undefined}\n            data-required={required ? \"true\" : undefined}\n            className={multiSelectLabelClasses}\n          >\n            {label}\n            {required ? <span aria-hidden=\"true\"> *</span> : null}\n          </Label>\n        ) : null}\n        <div\n          ref={setControlRef}\n          aria-invalid={resolvedAriaInvalid}\n          data-slot=\"multi-select-control\"\n          data-size={controlSize}\n          data-disabled={disabled ? \"true\" : undefined}\n          data-invalid={resolvedInvalid ? \"true\" : undefined}\n          data-readonly={readOnly ? \"true\" : undefined}\n          data-required={required ? \"true\" : undefined}\n          className={cn(\n            multiSelectControlBaseClasses,\n            multiSelectControlSizeClasses[controlSize],\n          )}\n        >\n          <div\n            role=\"group\"\n            aria-label={resolvedSelectedLabel}\n            data-slot=\"multi-select-value\"\n            className={multiSelectValueClasses}\n          >\n            {selectedValues.length > 0\n              ? selectedValues.map((selectedValue) => {\n                  const item = itemLookup.get(selectedValue);\n                  const labelNode = item?.label ?? selectedValue;\n                  const textValue = item?.textValue ?? selectedValue;\n\n                  return (\n                    <span\n                      key={selectedValue}\n                      data-slot=\"multi-select-chip\"\n                      data-value={selectedValue}\n                      className={multiSelectChipClasses}\n                    >\n                      <span\n                        data-slot=\"multi-select-chip-text\"\n                        className={multiSelectChipTextClasses}\n                      >\n                        {labelNode}\n                      </span>\n                      <button\n                        aria-label={`Remove ${textValue}`}\n                        className={multiSelectChipRemoveClasses}\n                        disabled={disabledOrReadOnly}\n                        onClick={(event) => {\n                          event.preventDefault();\n                          event.stopPropagation();\n                          updateValue(\n                            selectedValues.filter(\n                              (currentValue) => currentValue !== selectedValue,\n                            ),\n                          );\n                        }}\n                        type=\"button\"\n                      >\n                        <XIcon />\n                      </button>\n                    </span>\n                  );\n                })\n              : null}\n            <AriaInput\n              aria-invalid={resolvedAriaInvalid}\n              data-slot=\"multi-select-input\"\n              data-size={controlSize}\n              placeholder={\n                selectedValues.length > 0 ? searchPlaceholder : placeholder\n              }\n              className={multiSelectInputClasses}\n            />\n          </div>\n          {selectedValues.length > 0 ? (\n            <button\n              aria-label={clearLabel}\n              className={multiSelectClearClasses}\n              disabled={disabledOrReadOnly}\n              onClick={(event) => {\n                event.preventDefault();\n                event.stopPropagation();\n                updateValue([]);\n              }}\n              type=\"button\"\n            >\n              <XIcon />\n            </button>\n          ) : null}\n          <AriaButton\n            aria-label=\"Show options\"\n            data-slot=\"multi-select-trigger\"\n            className={multiSelectTriggerClasses}\n          >\n            <span\n              aria-hidden=\"true\"\n              data-slot=\"multi-select-icon\"\n              className={multiSelectIconClasses}\n            >\n              <ChevronDownIcon />\n            </span>\n          </AriaButton>\n        </div>\n        {name && !disabled\n          ? selectedValues.map((selectedValue) => (\n              <input\n                key={selectedValue}\n                name={name}\n                type=\"hidden\"\n                value={selectedValue}\n              />\n            ))\n          : null}\n        <span\n          aria-live=\"polite\"\n          data-slot=\"multi-select-status\"\n          className={multiSelectSrOnlyClasses}\n        >\n          {resolvedSelectedLabel}\n        </span>\n        {description ? (\n          <Text\n            slot=\"description\"\n            data-slot=\"multi-select-description\"\n            className={multiSelectHelpClasses}\n          >\n            {description}\n          </Text>\n        ) : null}\n        {errorMessage ? (\n          <FieldError\n            data-slot=\"multi-select-error\"\n            className={multiSelectErrorClasses}\n          >\n            {errorMessage}\n          </FieldError>\n        ) : null}\n        <Popover\n          triggerRef={controlRef}\n          data-slot=\"multi-select-popover\"\n          className={multiSelectPopoverClasses}\n        >\n          {renderedChildrenCount > 0 ? (\n            <ListBox\n              aria-label={\n                searchLabel ??\n                `Options for ${toPlainText(label) || \"multi-select\"}`\n              }\n              data-slot=\"multi-select-listbox\"\n              className={multiSelectListBoxClasses}\n            >\n              {renderedChildren}\n            </ListBox>\n          ) : (\n            <div\n              data-slot=\"multi-select-empty\"\n              className={multiSelectEmptyClasses}\n            >\n              {emptyMessage}\n            </div>\n          )}\n        </Popover>\n      </AriaCombobox>\n    </DethinkPortalProvider>\n  );\n}\n\nexport const MultiSelect = forwardRef(MultiSelectRoot) as MultiSelectComponent;\n\nMultiSelect.displayName = \"MultiSelect\";\n\nexport const MultiSelectItem = forwardRef<HTMLDivElement, MultiSelectItemProps>(\n  (\n    { children, className, disabled = false, textValue, value, ...props },\n    ref,\n  ) => (\n    <ListBoxItem\n      {...props}\n      ref={ref}\n      id={value}\n      isDisabled={disabled}\n      textValue={textValue || toPlainText(children) || value}\n      data-slot=\"multi-select-item\"\n      data-value={value}\n      className={multiSelectItemClassNames({ className })}\n    >\n      {({ isSelected }) => (\n        <>\n          <span\n            aria-hidden=\"true\"\n            data-slot=\"multi-select-item-indicator\"\n            data-selected={isSelected ? \"true\" : undefined}\n            className={multiSelectItemIndicatorClasses}\n          >\n            {isSelected ? <CheckIcon /> : null}\n          </span>\n          <span\n            data-slot=\"multi-select-item-content\"\n            className={multiSelectItemContentClasses}\n          >\n            {children}\n          </span>\n        </>\n      )}\n    </ListBoxItem>\n  ),\n);\n\nMultiSelectItem.displayName = \"MultiSelectItem\";\n"
    },
    {
      "path": "packages/components/src/components/multi-select/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/multi-select/index.ts",
      "content": "export {\n  MultiSelect,\n  MultiSelectItem,\n  multiSelectClassNames,\n  multiSelectItemClassNames,\n  type MultiSelectControlSize,\n  type MultiSelectItemData,\n  type MultiSelectItemProps,\n  type MultiSelectProps,\n  type MultiSelectValue,\n} from \"./multi-select\";\n"
    },
    {
      "path": "packages/components/src/utils/provider-portal.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/utils/provider-portal.tsx",
      "content": "import {\n  createContext,\n  type ForwardedRef,\n  type ReactNode,\n  type RefCallback,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { UNSAFE_PortalProvider } from \"react-aria/PortalProvider\";\nimport { setDethinkPortalOwner } from \"./portal-owner\";\n\nconst providerPortalDefaultClasses = \"bg-background font-sans text-foreground\";\nconst providerPortalMirroredAttributes = [\n  \"data-theme\",\n  \"data-density\",\n  \"dir\",\n] as const;\n\nexport type ProviderPortalMirroredAttribute =\n  (typeof providerPortalMirroredAttributes)[number];\n\nexport interface ProviderPortalRootOptions<T extends HTMLElement> {\n  forwardedRef?: ForwardedRef<T>;\n  portalSlot: string;\n}\n\nexport interface ProviderPortalRootState<T extends HTMLElement> {\n  portalContainer: HTMLElement | null;\n  rootRef: RefCallback<T>;\n}\n\nexport interface DethinkPortalProviderProps {\n  children?: ReactNode;\n  container: HTMLElement | null;\n}\n\nconst DethinkPortalContext = createContext(false);\nconst OverlayVisibilityContext = createContext(true);\n\n/** Persistent surfaces can dismiss overlays without unmounting their content. */\nexport function DethinkOverlayScope({\n  visible,\n  children,\n}: {\n  visible: boolean;\n  children?: ReactNode;\n}) {\n  const parentVisible = useContext(OverlayVisibilityContext);\n  return (\n    <OverlayVisibilityContext.Provider value={visible && parentVisible}>\n      {children}\n    </OverlayVisibilityContext.Provider>\n  );\n}\n\nexport function useScopedOverlayState({\n  open,\n  defaultOpen = false,\n  onOpenChange,\n}: {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}): readonly [boolean, (open: boolean) => void] {\n  const visible = useContext(OverlayVisibilityContext);\n  const [localOpen, setLocalOpen] = useState(defaultOpen);\n  const requestedOpen = open ?? localOpen;\n  const closeRequested = useRef(false);\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (next && !visible) return;\n      if (open === undefined) setLocalOpen(next);\n      onOpenChange?.(next);\n    },\n    [open, onOpenChange, visible],\n  );\n  useEffect(() => {\n    if (visible) closeRequested.current = false;\n    else if (requestedOpen && !closeRequested.current) {\n      closeRequested.current = true;\n      // Synchronize overlay state with the containing surface's lifecycle.\n      setOpen(false);\n    }\n  }, [visible, requestedOpen, setOpen]);\n  return [visible && requestedOpen, setOpen];\n}\n\nfunction assignForwardedRef<T>(\n  ref: ForwardedRef<T> | undefined,\n  value: T | null,\n) {\n  if (typeof ref === \"function\") {\n    ref(value);\n  } else if (ref) {\n    ref.current = value;\n  }\n}\n\nfunction fallbackProviderAttribute(name: ProviderPortalMirroredAttribute) {\n  if (typeof document === \"undefined\") {\n    return null;\n  }\n\n  if (name === \"data-density\") {\n    return \"default\";\n  }\n\n  if (name === \"data-theme\") {\n    return document.documentElement.getAttribute(name) ?? \"system\";\n  }\n\n  return document.documentElement.getAttribute(\"dir\") ?? document.dir ?? \"ltr\";\n}\n\nexport function syncDethinkPortalContainer({\n  container,\n  portalSlot,\n  provider,\n}: {\n  container: HTMLElement;\n  portalSlot: string;\n  provider: HTMLElement | null;\n}) {\n  const source =\n    provider ??\n    (typeof document === \"undefined\" ? null : document.documentElement);\n\n  container.setAttribute(\"data-slot\", portalSlot);\n  container.setAttribute(\"data-dethink-provider\", \"\");\n  container.className = provider?.className || providerPortalDefaultClasses;\n  container.style.cssText = provider?.getAttribute(\"style\") ?? \"\";\n  container.style.display = \"contents\";\n\n  for (const attribute of providerPortalMirroredAttributes) {\n    const value =\n      source?.getAttribute(attribute) ?? fallbackProviderAttribute(attribute);\n\n    if (value) {\n      container.setAttribute(attribute, value);\n    } else {\n      container.removeAttribute(attribute);\n    }\n  }\n}\n\nexport function useProviderPortalRoot<T extends HTMLElement>({\n  forwardedRef,\n  portalSlot,\n}: ProviderPortalRootOptions<T>): ProviderPortalRootState<T> {\n  const [portalContainer] = useState<HTMLElement | null>(() => {\n    if (typeof document === \"undefined\") {\n      return null;\n    }\n\n    return document.createElement(\"div\");\n  });\n  const [rootElement, setRootElement] = useState<T | null>(null);\n\n  const rootRef = useCallback(\n    (node: T | null) => {\n      setRootElement(node);\n      assignForwardedRef(forwardedRef, node);\n\n      if (node && portalContainer && typeof document !== \"undefined\") {\n        setDethinkPortalOwner(portalContainer, node);\n        const provider =\n          node.closest<HTMLElement>(\"[data-dethink-provider]\") ?? null;\n\n        syncDethinkPortalContainer({\n          container: portalContainer,\n          portalSlot,\n          provider,\n        });\n\n        if (!portalContainer.isConnected) {\n          document.body.appendChild(portalContainer);\n        }\n      }\n    },\n    [forwardedRef, portalContainer, portalSlot],\n  );\n\n  useEffect(() => {\n    if (!portalContainer || typeof document === \"undefined\") {\n      return undefined;\n    }\n\n    const provider =\n      rootElement?.closest<HTMLElement>(\"[data-dethink-provider]\") ?? null;\n    const syncContainer = () => {\n      syncDethinkPortalContainer({\n        container: portalContainer,\n        portalSlot,\n        provider,\n      });\n    };\n\n    syncContainer();\n    document.body.appendChild(portalContainer);\n\n    const observerTarget = provider ?? document.documentElement;\n    const observer = new MutationObserver(syncContainer);\n    observer.observe(observerTarget, {\n      attributeFilter: [\"class\", \"data-density\", \"data-theme\", \"dir\", \"style\"],\n      attributes: true,\n    });\n\n    return () => {\n      observer.disconnect();\n      portalContainer.remove();\n    };\n  }, [portalContainer, portalSlot, rootElement]);\n\n  return {\n    portalContainer,\n    rootRef,\n  };\n}\n\nexport function DethinkPortalProvider({\n  children,\n  container,\n}: DethinkPortalProviderProps) {\n  const hasDethinkPortalParent = useContext(DethinkPortalContext);\n  const content = hasDethinkPortalParent ? (\n    children\n  ) : (\n    <UNSAFE_PortalProvider getContainer={() => container}>\n      {children}\n    </UNSAFE_PortalProvider>\n  );\n\n  return (\n    <DethinkPortalContext.Provider value>\n      {content}\n    </DethinkPortalContext.Provider>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/utils/portal-owner.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/portal-owner.ts",
      "content": "/**\n * Links a Dethink portal container on <body> to the component root that owns\n * it, so a modal surface (the mobile sidebar) can keep overlays opened from\n * inside it usable while it makes the rest of the page inert. A global symbol\n * key, so components copied from the registry agree without sharing imports.\n */\nconst PORTAL_OWNER: unique symbol = Symbol.for(\"dethink.portal-owner\");\n\ntype OwnedElement = Element & { [PORTAL_OWNER]?: Element };\n\nexport function setDethinkPortalOwner(container: Element, owner: Element) {\n  (container as OwnedElement)[PORTAL_OWNER] = owner;\n}\n\n/** The element owning a Dethink portal container, or null for any other element. */\nexport function getDethinkPortalOwner(element: Element): Element | null {\n  return (element as OwnedElement)[PORTAL_OWNER] ?? null;\n}\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
