{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "type": "registry:ui",
  "title": "Combobox",
  "description": "React Aria backed autocomplete primitive with token-backed input, button, popover, item, form, invalid, disabled, read-only, density, dark mode, and provider-theme 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/combobox/combobox.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/combobox/combobox.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 Key,\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  useRef,\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 ComboboxValue = string;\nexport type ComboboxControlSize = \"sm\" | \"md\" | \"lg\";\nexport type ComboboxFormValue = \"key\" | \"text\";\nexport type ComboboxMenuTrigger = \"focus\" | \"input\" | \"manual\";\n\nexport type ComboboxItemData = {\n  label?: ReactNode;\n  textValue?: string;\n  value: ComboboxValue;\n};\n\nexport interface ComboboxProps<\n  T extends ComboboxItemData = ComboboxItemData,\n> extends Omit<\n  AriaComboboxProps<T, \"single\">,\n  | \"children\"\n  | \"className\"\n  | \"defaultInputValue\"\n  | \"defaultItems\"\n  | \"defaultSelectedKey\"\n  | \"defaultValue\"\n  | \"disabledKeys\"\n  | \"formValue\"\n  | \"inputValue\"\n  | \"isDisabled\"\n  | \"isInvalid\"\n  | \"isReadOnly\"\n  | \"isRequired\"\n  | \"items\"\n  | \"label\"\n  | \"menuTrigger\"\n  | \"onChange\"\n  | \"onInputChange\"\n  | \"onOpenChange\"\n  | \"onSelectionChange\"\n  | \"placeholder\"\n  | \"selectedKey\"\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  controlSize?: ComboboxControlSize;\n  defaultInputValue?: string;\n  defaultItems?: Iterable<T>;\n  defaultValue?: ComboboxValue | null;\n  description?: ReactNode;\n  disabled?: boolean;\n  disabledKeys?: Iterable<ComboboxValue>;\n  errorMessage?: ReactNode;\n  /** Feedback displayed beside the results inside the popup. */\n  popupContent?: ReactNode;\n  formValue?: ComboboxFormValue;\n  inputValue?: string;\n  invalid?: boolean;\n  items?: Iterable<T>;\n  label?: ReactNode;\n  menuTrigger?: ComboboxMenuTrigger;\n  onInputValueChange?: (value: string) => void;\n  onOpenChange?: (open: boolean, trigger?: ComboboxMenuTrigger) => void;\n  onValueChange?: (value: ComboboxValue | null) => void;\n  placeholder?: string;\n  readOnly?: boolean;\n  required?: boolean;\n  value?: ComboboxValue | null;\n}\n\nexport interface ComboboxItemProps extends Omit<\n  AriaListBoxItemProps<ComboboxItemData>,\n  \"children\" | \"className\" | \"id\" | \"isDisabled\" | \"value\"\n> {\n  children?: ReactNode;\n  className?: string;\n  disabled?: boolean;\n  value: ComboboxValue;\n}\n\nconst comboboxRootBaseClasses =\n  \"group/combobox grid w-full min-w-0 gap-[var(--dt-space-2)] text-foreground\";\n\nconst comboboxLabelClasses =\n  \"text-sm font-medium leading-none text-foreground data-[disabled=true]:opacity-60 data-[invalid=true]:text-destructive\";\n\nconst comboboxControlBaseClasses =\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]/combobox:border-ring\";\n\nconst comboboxControlSizeClasses: Record<ComboboxControlSize, string> = {\n  sm: \"h-8 ps-[var(--dt-space-2-5)] pe-[var(--dt-space-1)] text-base sm:text-sm\",\n  md: \"h-density-control ps-[var(--dt-space-3)] pe-[var(--dt-space-1)] text-base sm:text-sm\",\n  lg: \"h-11 ps-[var(--dt-space-4)] pe-[var(--dt-space-1)] text-base\",\n};\n\nconst comboboxInputClasses =\n  \"min-w-0 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 comboboxButtonClasses =\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 comboboxIconClasses =\n  \"size-4 motion-safe:transition-transform motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control group-data-[open]/combobox:rotate-180\";\n\nconst comboboxHelpClasses = \"text-xs leading-5 text-muted-foreground\";\n\nconst comboboxErrorClasses =\n  \"text-xs font-medium leading-5 text-destructive motion-safe:animate-feedback-in\";\n\nconst comboboxPopoverClasses =\n  \"z-50 max-h-72 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 comboboxListBoxClasses =\n  \"max-h-72 overflow-auto p-[var(--dt-space-1)] outline-none\";\n\nconst comboboxItemBaseClasses =\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 data-[selected]:text-primary-foreground\";\n\nconst comboboxItemIndicatorClasses =\n  \"flex size-4 items-center justify-center text-current\";\n\nconst comboboxItemContentClasses = \"min-w-0 truncate\";\n\ntype ComboboxComponent = (<T extends ComboboxItemData = ComboboxItemData>(\n  props: ComboboxProps<T> & RefAttributes<HTMLDivElement>,\n) => ReactElement | null) & { displayName?: string };\n\nfunction isAriaInvalid(value: ComboboxProps[\"aria-invalid\"]) {\n  return (\n    value === true ||\n    value === \"true\" ||\n    value === \"grammar\" ||\n    value === \"spelling\"\n  );\n}\n\nfunction toSelectionKey(value: ComboboxValue | null | undefined) {\n  return value;\n}\n\nfunction toDisabledKeys(disabledKeys: Iterable<ComboboxValue> | undefined) {\n  return disabledKeys ? Array.from(disabledKeys) : undefined;\n}\n\nfunction syncAriaInvalidAttribute(\n  element: HTMLElement | null,\n  value: ComboboxProps[\"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 comboboxClassNames({\n  className,\n}: Pick<ComboboxProps, \"className\"> = {}) {\n  return cn(comboboxRootBaseClasses, className);\n}\n\nexport function comboboxItemClassNames({\n  className,\n}: Pick<ComboboxItemProps, \"className\"> = {}) {\n  return cn(comboboxItemBaseClasses, 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 renderComboboxChildren<T extends ComboboxItemData>({\n  children,\n  defaultItems,\n  items,\n}: Pick<ComboboxProps<T>, \"children\" | \"defaultItems\" | \"items\">): ReactNode {\n  const sourceItems = items ?? defaultItems;\n\n  if (sourceItems && typeof children === \"function\") {\n    return Array.from(sourceItems, (item) => {\n      const child = children(item);\n\n      return withComboboxItemTextValue(child, item.value);\n    });\n  }\n\n  return Children.map(children as ReactNode, (child) =>\n    withComboboxItemTextValue(child),\n  );\n}\n\nfunction withComboboxItemTextValue(child: ReactNode, key?: string) {\n  if (!isValidElement<ComboboxItemProps>(child)) {\n    return child;\n  }\n\n  const { children, textValue, value } = child.props;\n\n  if (typeof value !== \"string\") {\n    return child;\n  }\n\n  return cloneElement(child, {\n    key: child.key ?? key,\n    textValue: textValue || toPlainText(children) || value,\n  });\n}\n\nfunction ComboboxRoot<T extends ComboboxItemData = ComboboxItemData>(\n  {\n    \"aria-invalid\": ariaInvalid,\n    \"data-slot\": dataSlot,\n    children,\n    className,\n    controlSize = \"md\",\n    defaultInputValue,\n    defaultItems,\n    defaultValue,\n    description,\n    disabled = false,\n    disabledKeys,\n    errorMessage,\n    popupContent,\n    formValue = \"key\",\n    inputValue,\n    invalid = false,\n    items,\n    label,\n    menuTrigger = \"input\",\n    name,\n    onInputValueChange,\n    onOpenChange,\n    onValueChange,\n    placeholder = \"Search options\",\n    readOnly = false,\n    required = false,\n    value,\n    ...props\n  }: ComboboxProps<T>,\n  ref: ForwardedRef<HTMLDivElement>,\n) {\n  const resolvedInvalid = invalid || isAriaInvalid(ariaInvalid);\n  const resolvedAriaInvalid = invalid ? true : ariaInvalid;\n  const renderedChildren = renderComboboxChildren({\n    children,\n    defaultItems,\n    items,\n  });\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const { portalContainer, rootRef } = useProviderPortalRoot<HTMLDivElement>({\n    forwardedRef: ref,\n    portalSlot: \"combobox-portal-container\",\n  });\n  const setInputRef = useCallback(\n    (node: HTMLInputElement | null) => {\n      inputRef.current = node;\n      syncAriaInvalidAttribute(node, resolvedAriaInvalid);\n    },\n    [resolvedAriaInvalid],\n  );\n\n  useEffect(() => {\n    syncAriaInvalidAttribute(inputRef.current, resolvedAriaInvalid);\n  }, [resolvedAriaInvalid]);\n\n  return (\n    <DethinkPortalProvider container={portalContainer}>\n      <AriaCombobox\n        {...props}\n        ref={rootRef}\n        selectedKey={toSelectionKey(value)}\n        defaultSelectedKey={toSelectionKey(defaultValue)}\n        name={disabled ? undefined : name}\n        inputValue={inputValue}\n        defaultInputValue={defaultInputValue}\n        onInputChange={onInputValueChange}\n        onOpenChange={(isOpen, trigger) => {\n          if (!readOnly) {\n            onOpenChange?.(isOpen, trigger);\n          }\n        }}\n        onSelectionChange={(key: Key | null) => {\n          if (!readOnly) {\n            onValueChange?.(key === null ? null : String(key));\n          }\n        }}\n        disabledKeys={toDisabledKeys(disabledKeys)}\n        isDisabled={disabled}\n        isReadOnly={readOnly}\n        isRequired={required}\n        isInvalid={resolvedInvalid}\n        aria-invalid={resolvedAriaInvalid}\n        menuTrigger={menuTrigger}\n        formValue={formValue}\n        validationBehavior=\"aria\"\n        data-slot={dataSlot ?? \"combobox\"}\n        data-size={controlSize}\n        data-readonly={readOnly ? \"true\" : undefined}\n        className={comboboxClassNames({ className })}\n      >\n        {label ? (\n          <Label\n            data-slot=\"combobox-label\"\n            data-disabled={disabled ? \"true\" : undefined}\n            data-invalid={resolvedInvalid ? \"true\" : undefined}\n            data-required={required ? \"true\" : undefined}\n            className={comboboxLabelClasses}\n          >\n            {label}\n            {required ? <span aria-hidden=\"true\"> *</span> : null}\n          </Label>\n        ) : null}\n        <div\n          data-slot=\"combobox-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            comboboxControlBaseClasses,\n            comboboxControlSizeClasses[controlSize],\n          )}\n        >\n          <AriaInput\n            ref={setInputRef}\n            aria-invalid={resolvedAriaInvalid}\n            data-slot=\"combobox-input\"\n            data-size={controlSize}\n            placeholder={placeholder}\n            className={comboboxInputClasses}\n          />\n          <AriaButton\n            aria-label=\"Show options\"\n            data-slot=\"combobox-button\"\n            className={comboboxButtonClasses}\n          >\n            <span\n              aria-hidden=\"true\"\n              data-slot=\"combobox-icon\"\n              className={comboboxIconClasses}\n            >\n              <ChevronDownIcon />\n            </span>\n          </AriaButton>\n        </div>\n        {description ? (\n          <Text\n            slot=\"description\"\n            data-slot=\"combobox-description\"\n            className={comboboxHelpClasses}\n          >\n            {description}\n          </Text>\n        ) : null}\n        {errorMessage ? (\n          <FieldError\n            data-slot=\"combobox-error\"\n            className={comboboxErrorClasses}\n          >\n            {errorMessage}\n          </FieldError>\n        ) : null}\n        <Popover\n          data-slot=\"combobox-popover\"\n          className={comboboxPopoverClasses}\n        >\n          <ListBox\n            data-slot=\"combobox-listbox\"\n            className={comboboxListBoxClasses}\n          >\n            {renderedChildren}\n          </ListBox>\n          {popupContent}\n        </Popover>\n      </AriaCombobox>\n    </DethinkPortalProvider>\n  );\n}\n\nexport const Combobox = forwardRef(ComboboxRoot) as ComboboxComponent;\n\nCombobox.displayName = \"Combobox\";\n\nexport const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(\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=\"combobox-item\"\n      data-value={value}\n      className={comboboxItemClassNames({ className })}\n    >\n      {({ isSelected }) => (\n        <>\n          <span\n            aria-hidden=\"true\"\n            data-slot=\"combobox-item-indicator\"\n            data-selected={isSelected ? \"true\" : undefined}\n            className={comboboxItemIndicatorClasses}\n          >\n            {isSelected ? <CheckIcon /> : null}\n          </span>\n          <span\n            data-slot=\"combobox-item-content\"\n            className={comboboxItemContentClasses}\n          >\n            {children}\n          </span>\n        </>\n      )}\n    </ListBoxItem>\n  ),\n);\n\nComboboxItem.displayName = \"ComboboxItem\";\n"
    },
    {
      "path": "packages/components/src/components/combobox/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/combobox/index.ts",
      "content": "export {\n  Combobox,\n  ComboboxItem,\n  comboboxClassNames,\n  comboboxItemClassNames,\n  type ComboboxControlSize,\n  type ComboboxFormValue,\n  type ComboboxItemData,\n  type ComboboxItemProps,\n  type ComboboxMenuTrigger,\n  type ComboboxProps,\n  type ComboboxValue,\n} from \"./combobox\";\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"
    }
  ]
}
