{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "type": "registry:ui",
  "title": "Select",
  "description": "정해진 목록에서 하나를 고르게 하는 선택 컴포넌트입니다. 목록은 같은 자리에서 펼쳐집니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/icons",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-field-context",
    "@atom/lib-slot",
    "@atom/lib-use-anchored-position",
    "@atom/lib-use-controllable-state",
    "@atom/lib-use-dismiss"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/select.tsx",
      "content": "\"use client\";\n\nimport {\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactNode,\n  type Ref,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { useFieldControl } from \"../lib/field-context\";\nimport { Slot } from \"../lib/slot\";\nimport { useAnchoredPosition } from \"../lib/use-anchored-position\";\nimport { useControllableState } from \"../lib/use-controllable-state\";\nimport { useDismiss } from \"../lib/use-dismiss\";\nimport { CheckIcon, ChevronDownIcon } from \"./icons\";\n\n/* -------------------------------------------------------------------------- */\n/*  Select (components/select.md, styles/atom/elements/select.css)            */\n/*    <Select                                                                 */\n/*      placeholder=\"선택해주세요\"                                             */\n/*      options={[                                                            */\n/*        { value: \"latest\", label: \"최신 순\" },                               */\n/*        { label: \"옵션그룹\", options: [{ value: \"a\", label: \"옵션1\" }] },     */\n/*      ]}                                                                    */\n/*      value={value}                                                         */\n/*      onValueChange={setValue}                                              */\n/*    />                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport type SelectSize = \"small\" | \"medium\" | \"large\";\nexport type SelectVariant = \"weak\" | \"outline\";\n\n/** 선택지 하나 — 원본 Props `options` 의 `{ value, label, description?, icon?, disabled? }` */\nexport type SelectOption = {\n  value: string;\n  label: string;\n  description?: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n};\n\n/** Group label 로 묶인 선택지 묶음 (원본 Anatomy \"Group label\") */\nexport type SelectOptionGroup = {\n  label: string;\n  options: SelectOption[];\n};\n\nexport type SelectProps = Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> & {\n  ref?: Ref<HTMLDivElement>;\n  /** 선택된 항목의 값입니다. */\n  value?: string;\n  /** uncontrolled 일 때 처음 선택된 값입니다. */\n  defaultValue?: string;\n  /** 값이 없을 때 Trigger 에 보이는 문구입니다. */\n  placeholder?: string;\n  /** 선택지 목록입니다. 그룹(`{ label, options }`)을 섞어 넣으면 Group label 이 붙습니다. */\n  options: Array<SelectOption | SelectOptionGroup>;\n  /** Trigger 높이와 폰트 크기를 결정합니다. 기본 \"medium\" — 지정하지 않으면 Field 의 size 를 따릅니다. */\n  size?: SelectSize;\n  /** Trigger 의 배경과 테두리 처리입니다. */\n  variant?: SelectVariant;\n  /** Trigger 테두리를 오류 색으로 바꿉니다. */\n  invalid?: boolean;\n  /** 열 수 없게 만듭니다. */\n  disabled?: boolean;\n  /** 선택지가 비었을 때 Listbox 에 보일 문구입니다. */\n  emptyText?: string;\n  /** 선택이 바뀔 때 호출됩니다. */\n  onValueChange?: (value: string) => void;\n  /** 열림 상태 (controlled) */\n  open?: boolean;\n  /** 처음 열림 상태 (uncontrolled) */\n  defaultOpen?: boolean;\n  /** 열림 상태가 바뀔 때 호출됩니다. */\n  onOpenChange?: (open: boolean) => void;\n  /** 폼 제출용 이름. 지정하면 hidden input 으로 값을 함께 보냅니다. */\n  name?: string;\n  /** Trigger(<button>) 의 id. Field 안에서는 Field 가 정합니다. */\n  triggerId?: string;\n};\n\nconst B = \"atom-select\";\nconst TYPEAHEAD_MS = 500;\n\ntype Row =\n  | { kind: \"group\"; label: string; key: string; options: { option: SelectOption; index: number }[] }\n  | { kind: \"option\"; option: SelectOption; index: number };\n\nfunction isGroup(item: SelectOption | SelectOptionGroup): item is SelectOptionGroup {\n  return Array.isArray((item as SelectOptionGroup).options);\n}\n\nexport function Select({\n  ref,\n  value: valueProp,\n  defaultValue,\n  placeholder,\n  options,\n  size: sizeProp,\n  variant = \"weak\",\n  invalid = false,\n  disabled = false,\n  emptyText = \"결과가 없어요\",\n  onValueChange,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  name,\n  triggerId,\n  className,\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  ...rest\n}: SelectProps) {\n  const field = useFieldControl();\n  const size = sizeProp ?? field?.size ?? \"medium\";\n  const isInvalid = invalid || !!field?.invalid;\n  const isDisabled = disabled || !!field?.disabled;\n\n  const [value, setValue] = useControllableState<string | undefined>({\n    value: valueProp,\n    defaultValue,\n    onChange: (v) => onValueChange?.(v ?? \"\"),\n  });\n  const [open, setOpenState] = useControllableState<boolean>({\n    value: openProp,\n    defaultValue: defaultOpen,\n    onChange: onOpenChange,\n  });\n\n  const baseId = useId();\n  const listboxId = `${baseId}-listbox`;\n  const valueId = `${baseId}-value`;\n  const optionId = (i: number) => `${baseId}-option-${i}`;\n\n  // 평탄화된 선택지 목록 (키보드 이동 순서) + 렌더용 행\n  const { flat, rows } = useMemo(() => {\n    const flat: SelectOption[] = [];\n    const rows: Row[] = [];\n    options.forEach((item, gi) => {\n      if (isGroup(item)) {\n        rows.push({\n          kind: \"group\",\n          label: item.label,\n          key: `g${gi}`,\n          options: item.options.map((option) => ({ option, index: flat.push(option) - 1 })),\n        });\n      } else {\n        rows.push({ kind: \"option\", option: item, index: flat.push(item) - 1 });\n      }\n    });\n    return { flat, rows };\n  }, [options]);\n\n  const selectedIndex = flat.findIndex((o) => o.value === value);\n  const selected = selectedIndex >= 0 ? flat[selectedIndex] : undefined;\n\n  const [active, setActive] = useState(-1);\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const triggerRef = useRef<HTMLButtonElement | null>(null);\n  const typeahead = useRef({ text: \"\", time: 0 });\n\n  function setOpen(next: boolean, nextActive?: number) {\n    if (next && isDisabled) return;\n    if (next) setActive(nextActive ?? (selectedIndex >= 0 ? selectedIndex : flat.length ? 0 : -1));\n    if (next !== open) setOpenState(next);\n  }\n\n  function close() {\n    setOpen(false);\n    triggerRef.current?.focus();\n  }\n\n  function choose(index: number) {\n    const option = flat[index];\n    if (!option || option.disabled) return;\n    if (option.value !== value) setValue(option.value);\n    close();\n  }\n\n  // 바깥 클릭 · Esc — 선택하지 않고 닫고 포커스는 Trigger 로\n  useDismiss(open, [rootRef], close);\n  const listboxRef = useRef<HTMLDivElement | null>(null);\n  useAnchoredPosition(open, rootRef, listboxRef);\n\n  // 열린 상태로 시작(defaultOpen / open)했을 때도 현재 항목을 맞춤\n  useEffect(() => {\n    if (open && active < 0 && flat.length) setActive(selectedIndex >= 0 ? selectedIndex : 0);\n  }, [open, active, flat.length, selectedIndex]);\n\n  // 현재 항목이 보이도록\n  useEffect(() => {\n    if (!open || active < 0) return;\n    document.getElementById(optionId(active))?.scrollIntoView?.({ block: \"nearest\" });\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open, active]);\n\n  /** 글자 입력 — 해당 글자로 시작하는 항목으로 점프 */\n  function findByTypeahead(key: string): number {\n    const now = Date.now();\n    const t = typeahead.current;\n    t.text = now - t.time > TYPEAHEAD_MS ? key : t.text + key;\n    t.time = now;\n    const query = t.text.toLocaleLowerCase();\n    // 같은 글자를 반복하면 다음 항목으로 순환\n    const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n    const needle = repeated ? query[0]! : query;\n    const start = repeated || query.length === 1 ? active + 1 : Math.max(active, 0);\n    for (let n = 0; n < flat.length; n++) {\n      const i = (start + n) % flat.length;\n      if (flat[i]!.label.toLocaleLowerCase().startsWith(needle)) return i;\n    }\n    return -1;\n  }\n\n  function onTriggerKeyDown(e: KeyboardEvent<HTMLButtonElement>) {\n    const last = flat.length - 1;\n    const isChar = e.key.length === 1 && e.key !== \" \" && !e.ctrlKey && !e.metaKey && !e.altKey;\n\n    if (!open) {\n      switch (e.key) {\n        case \"Enter\":\n        case \" \":\n        case \"ArrowDown\":\n        case \"ArrowUp\":\n          e.preventDefault();\n          setOpen(true);\n          return;\n        case \"Home\":\n          e.preventDefault();\n          setOpen(true, 0);\n          return;\n        case \"End\":\n          e.preventDefault();\n          setOpen(true, last);\n          return;\n      }\n      if (isChar) {\n        const i = findByTypeahead(e.key);\n        setOpen(true, i >= 0 ? i : undefined);\n      }\n      return;\n    }\n\n    switch (e.key) {\n      case \"ArrowDown\":\n        e.preventDefault();\n        setActive((i) => Math.min(i + 1, last));\n        return;\n      case \"ArrowUp\":\n        e.preventDefault();\n        setActive((i) => Math.max(i - 1, 0));\n        return;\n      case \"Home\":\n        e.preventDefault();\n        setActive(0);\n        return;\n      case \"End\":\n        e.preventDefault();\n        setActive(last);\n        return;\n      case \"Enter\":\n      case \" \":\n        e.preventDefault();\n        if (active >= 0) choose(active);\n        return;\n      case \"Tab\":\n        // 닫고 다음 요소로 이동 (기본 동작 유지)\n        setOpen(false);\n        return;\n    }\n    if (isChar) {\n      e.preventDefault();\n      const i = findByTypeahead(e.key);\n      if (i >= 0) setActive(i);\n    }\n  }\n\n  function renderOption(option: SelectOption, index: number) {\n    return (\n      <div\n        key={`${option.value}-${index}`}\n        id={optionId(index)}\n        className={`${B}__option`}\n        role=\"option\"\n        aria-selected={index === selectedIndex}\n        aria-disabled={option.disabled || undefined}\n        data-active={index === active ? \"\" : undefined}\n        // 포커스는 Trigger 에 머뭅니다\n        onMouseDown={(e) => e.preventDefault()}\n        onMouseMove={() => {\n          if (active !== index) setActive(index);\n        }}\n        onClick={() => choose(index)}\n      >\n        {option.icon !== undefined && (\n          <Slot aria-hidden=\"true\" className={`${B}__option-icon`}>\n            {option.icon}\n          </Slot>\n        )}\n        <span className={`${B}__option-text`}>\n          <span className={`${B}__option-label`}>{option.label}</span>\n          {option.description !== undefined && (\n            <span className={`${B}__option-description`}>{option.description}</span>\n          )}\n        </span>\n        <CheckIcon className={`${B}__option-check`} />\n      </div>\n    );\n  }\n\n  // 접근 가능한 이름은 트리거(button)에: 이름 + 현재 값. aria-label 은 숨은 span 으로 바꿔 함께 읽힘\n  const nameId = `${valueId}-name`;\n  const nameSource = ariaLabelledBy ?? (ariaLabel !== undefined ? nameId : field?.labelId);\n  const labelledBy = nameSource ? `${nameSource} ${valueId}` : undefined;\n  const describedBy = cn(field?.describedBy, ariaDescribedBy) || undefined;\n\n  return (\n    <div\n      {...rest}\n      ref={(node) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") return ref(node);\n        if (ref) ref.current = node;\n      }}\n      className={cn(\n        B,\n        mod(B, \"size\", size, \"medium\"),\n        mod(B, \"variant\", variant, \"weak\"),\n        !selected && `${B}--empty`,\n        className,\n      )}\n      data-state={open ? \"open\" : \"closed\"}\n      data-invalid={isInvalid ? \"\" : undefined}\n      data-disabled={isDisabled ? \"\" : undefined}\n    >\n      <button\n        ref={triggerRef}\n        id={triggerId ?? field?.controlId}\n        className={`${B}__trigger`}\n        type=\"button\"\n        disabled={isDisabled}\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        aria-controls={open ? listboxId : undefined}\n        aria-activedescendant={open && active >= 0 ? optionId(active) : undefined}\n        aria-labelledby={labelledBy}\n        aria-describedby={describedBy}\n        onClick={() => (open ? close() : setOpen(true))}\n        onKeyDown={onTriggerKeyDown}\n      >\n        <span id={valueId} className={`${B}__value`}>\n          {selected ? selected.label : placeholder}\n        </span>\n        <ChevronDownIcon className={`${B}__caret`} />\n        {ariaLabel !== undefined && ariaLabelledBy === undefined && (\n          <span id={nameId} className=\"atom-sr-only\">\n            {ariaLabel}\n          </span>\n        )}\n      </button>\n\n      {open && (\n        <div\n          ref={listboxRef}\n          id={listboxId}\n          className={`${B}__listbox`}\n          role=\"listbox\"\n          aria-labelledby={field?.labelId}\n          tabIndex={-1}\n        >\n          {flat.length === 0 ? (\n            <p className={`${B}__empty`}>{emptyText}</p>\n          ) : (\n            rows.map((row) =>\n              row.kind === \"group\" ? (\n                <div key={row.key} role=\"group\" aria-labelledby={`${baseId}-${row.key}`}>\n                  <div id={`${baseId}-${row.key}`} className={`${B}__group-label`} role=\"presentation\">\n                    {row.label}\n                  </div>\n                  {row.options.map(({ option, index }) => renderOption(option, index))}\n                </div>\n              ) : (\n                renderOption(row.option, row.index)\n              ),\n            )\n          )}\n        </div>\n      )}\n\n      {name !== undefined && <input type=\"hidden\" name={name} value={value ?? \"\"} />}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/select.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/field.css",
      "content": "/* ============================================================================\n   Element — Field\n   ----------------------------------------------------------------------------\n   The label / description / error / counter wrapper every input-ish component\n   is dropped into. Field owns the surrounding text, never the control itself.\n\n   atom-field\n     --layout_horizontal                     label to the left of the control\n     --size_{small|medium}                   medium is the default\n     [data-invalid]  [data-disabled]\n     __header  __label  __required  __optional\n     __control                               slot for the actual control\n     __separator                             the text between a range's inputs\n     __footer  __description  __error  __counter\n\n   __control is a row, not a single box. One input, an input plus a Button, or\n   two inputs joined by __separator for a range — all of them are the same slot,\n   so the 4px rhythm and the single row height are defined here once instead of\n   in every page that needs a combination.\n   ========================================================================== */\n\n.atom-field {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  width: 100%;\n  min-width: 0;\n}\n\n.atom-field__header {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n}\n\n.atom-field__label {\n  font-size: 13px;\n  font-weight: 600;\n  line-height: 1.4;\n  color: var(--atom-fg-neutral);\n}\n\n.atom-field__required {\n  margin-left: 2px;\n  color: var(--atom-primary-600);\n}\n\n.atom-field__optional {\n  font-size: 12px;\n  font-weight: 400;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- control slot -------------------------------------------------------- */\n/* Members sit 4px apart and share one height. Every control already agrees on\n   that height — `--atom-control-h`, see the Control surface block in\n   tokens.css — so the only member that needs handling is Button, which keeps\n   its own 47px: `stretch` cannot pull a fixed height, so the height is\n   released and the row decides. */\n.atom-field__control {\n  display: flex;\n  align-items: stretch;\n  gap: var(--atom-x1);\n  min-width: 0;\n}\n\n/* Every member starts at `width: 100%`, so no explicit `flex` is needed — they\n   all ask for the full row and shrink together. What must not shrink says so\n   below; what is left over goes to the typing surfaces, and two inputs in a\n   range land on the same width without being told. */\n.atom-field__control > * {\n  min-width: 0;\n}\n\n/* `:not(:only-child)` matters: a Button alone in the slot has nothing taller to\n   stretch against, so releasing its height would collapse it to its own text.\n   The height only gives way once there is a row to match. Every other control\n   is already `--atom-control-h` and needs nothing here. */\n.atom-field__control > .atom-button:not(:only-child) {\n  height: auto;\n  flex-shrink: 0;\n}\n\n\n/* Focus follows the control ring too. base.css draws its ring *outside* the\n   element at a 2px offset, which lands on the neighbour across a 4px gap, so a\n   Button in a combination borrows the inset ring every other control uses. */\n.atom-field__control > .atom-button:not(:only-child):focus-visible {\n  outline: none;\n  box-shadow: var(--atom-control-border-focus);\n}\n\n/* An icon button in the slot is square. Its own `--layout_icon-only` widths\n   (32 / 40 / 47 / 54px) follow Button's heights, and in the slot the row sets\n   the height instead — so the width is re-pointed at the control heights the\n   row can take, picked by the size modifier the author already put on the\n   button to match the control beside it.\n\n   `aspect-ratio` cannot do this: flex resolves an item's width before the row\n   stretches its height, so with `width: auto` there is no definite height for\n   the ratio to work from and the button collapses to its icon (0 wide, once\n   the icon is a percentage of it).\n\n   The icon is a share of the box rather than a fixed size, so it tracks all\n   three heights — 40 → 16px, 48 → 19.2px, 56 → 22.4px — which is the 38–42%\n   band Button's own sizes already sit in. */\n.atom-field__control > .atom-button--layout_icon-only:not(:only-child) {\n  width: var(--atom-control-h);\n  padding-inline: 0;\n}\n\n.atom-field__control\n  > .atom-button--layout_icon-only.atom-button--size_xsmall:not(:only-child),\n.atom-field__control\n  > .atom-button--layout_icon-only.atom-button--size_small:not(:only-child) {\n  width: var(--atom-control-h-small);\n}\n\n.atom-field__control\n  > .atom-button--layout_icon-only.atom-button--size_large:not(:only-child) {\n  width: var(--atom-control-h-large);\n}\n\n.atom-field__control\n  > .atom-button--layout_icon-only:not(:only-child)\n  .atom-button__icon {\n  width: 40%;\n  /* Not `height: 40%` — the row's height is auto, so a percentage height falls\n     back to the svg's 150px intrinsic height and the row grows to fit it. */\n  height: auto;\n  aspect-ratio: 1;\n}\n\n/* Checkbox, Radio and Switch are a line of text with a marker, not a box: they\n   have no control height, so a slot holding only those is as tall as its text\n   and sits above the 48px inputs in the fields beside it. Give that slot the\n   control height and centre the group in it — the marker then lines up with\n   the text inside those inputs. `:has` because the slot, not the group, is the\n   box being sized. */\n.atom-field__control:has(> .atom-checkbox-group:only-child),\n.atom-field__control:has(> .atom-radio-group:only-child),\n.atom-field__control:has(> .atom-checkbox:only-child),\n.atom-field__control:has(> .atom-radio:only-child),\n.atom-field__control:has(> .atom-switch:only-child) {\n  align-items: center;\n  min-height: var(--atom-control-h);\n}\n\n.atom-field--size_small\n  .atom-field__control:has(> .atom-checkbox-group:only-child),\n.atom-field--size_small\n  .atom-field__control:has(> .atom-radio-group:only-child),\n.atom-field--size_small .atom-field__control:has(> .atom-checkbox:only-child),\n.atom-field--size_small .atom-field__control:has(> .atom-radio:only-child),\n.atom-field--size_small .atom-field__control:has(> .atom-switch:only-child) {\n  min-height: var(--atom-control-h-small);\n}\n\n/* The ‘~’ or ‘부터’ between a range's two inputs. It is a label, not a control,\n   so it keeps its own size and centres against the row instead of stretching. */\n.atom-field__separator {\n  flex-shrink: 0;\n  align-self: center;\n  padding-inline: 2px;\n  font-size: 15px;\n  color: var(--atom-fg-neutral-muted);\n  white-space: nowrap;\n}\n\n.atom-field__footer {\n  display: flex;\n  align-items: flex-start;\n  justify-content: space-between;\n  gap: var(--atom-x4);\n  font-size: 12px;\n  line-height: 1.5;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-field__description,\n.atom-field__error {\n  min-width: 0;\n  word-break: keep-all;\n}\n\n.atom-field__error {\n  display: flex;\n  align-items: flex-start;\n  gap: 4px;\n  color: var(--atom-fg-critical);\n}\n\n.atom-field__error-icon {\n  width: 13px;\n  height: 13px;\n  flex-shrink: 0;\n  margin-top: 2px;\n}\n\n.atom-field__counter {\n  flex-shrink: 0;\n  margin-left: auto;\n  font-variant-numeric: tabular-nums;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-field--size_small .atom-field__label {\n  font-size: 12px;\n}\n\n.atom-field--size_small .atom-field__footer {\n  font-size: 11px;\n}\n\n/* --- horizontal layout --------------------------------------------------- */\n.atom-field--layout_horizontal {\n  flex-direction: row;\n  align-items: flex-start;\n  gap: var(--atom-x5);\n}\n\n.atom-field--layout_horizontal .atom-field__header {\n  flex-shrink: 0;\n  width: 120px;\n  padding-top: 12px;\n}\n\n.atom-field--layout_horizontal .atom-field__body {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  flex: 1;\n  min-width: 0;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-field[data-invalid] .atom-field__description {\n  color: var(--atom-fg-critical);\n}\n\n.atom-field[data-disabled] {\n  opacity: 0.45;\n}\n\n/* Legacy alias — the old card preview marked invalid with a modifier class. */\n.atom-field--invalid .atom-field__description {\n  color: var(--atom-fg-critical);\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/field.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/select.css",
      "content": "/* ============================================================================\n   Element — Select\n   ----------------------------------------------------------------------------\n   A trigger that opens a listbox. Options carry an optional leading icon, a\n   label, a description and a selected check.\n\n   atom-select\n     --size_{small|medium|large}             medium is the default\n     --variant_{weak|outline}                weak is the default\n     [data-state=\"closed|open\"]  [data-invalid]  [data-disabled]\n     __trigger  __value  __caret\n     __listbox  __group-label  __option  __option-icon  __option-text\n     __option-label  __option-description  __option-check\n     __empty\n   ========================================================================== */\n\n.atom-select {\n  position: relative;\n  width: 100%;\n  min-width: 0;\n}\n\n/* --- trigger ------------------------------------------------------------- */\n.atom-select__trigger {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x3);\n  width: 100%;\n  height: var(--atom-control-h);\n  padding-inline: var(--atom-x4);\n  border-radius: var(--atom-radius-r2);\n  background: var(--atom-bg-neutral-weak);\n  font-size: 15px;\n  color: var(--atom-fg-neutral);\n  text-align: left;\n  transition: box-shadow var(--atom-duration-fast) ease,\n    filter var(--atom-duration-fast) ease;\n}\n\n.atom-select__value {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.atom-select--empty .atom-select__value,\n.atom-select__trigger--empty .atom-select__value {\n  color: var(--atom-fg-neutral-subtle);\n}\n\n.atom-select__caret {\n  width: 24px;\n  height: 24px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral-muted);\n  transition: transform var(--atom-duration-base) var(--atom-ease-out);\n}\n\n/* --- listbox ------------------------------------------------------------- */\n.atom-select__listbox {\n  margin-top: 6px;\n  padding: 6px;\n  border-radius: var(--atom-radius-r3);\n  background: var(--atom-bg-layer);\n  box-shadow: var(--atom-shadow-s2), inset 0 0 0 1px var(--atom-border-subtle);\n}\n\n.atom-select__group-label {\n  padding: 6px 10px 4px;\n  font-size: 11px;\n  font-weight: 600;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n.atom-select__option {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 9px 10px;\n  border-radius: var(--atom-radius-r2);\n  font-size: 14px;\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease;\n}\n\n.atom-select__option-icon {\n  width: 20px;\n  height: 20px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-select__option-text {\n  display: flex;\n  flex-direction: column;\n  min-width: 0;\n  flex: 1;\n}\n\n.atom-select__option-label {\n  font-weight: 500;\n  color: var(--atom-fg-neutral);\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.atom-select__option-description {\n  font-size: 12px;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-select__option-check {\n  width: 18px;\n  height: 18px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral);\n  opacity: 0;\n}\n\n.atom-select__empty {\n  padding: var(--atom-x5) 10px;\n  text-align: center;\n  font-size: 13px;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-select--size_small .atom-select__trigger {\n  height: var(--atom-control-h-small);\n  font-size: 14px;\n}\n\n.atom-select--size_small .atom-select__caret {\n  width: 24px;\n  height: 24px;\n}\n\n.atom-select--size_large .atom-select__trigger {\n  height: var(--atom-control-h-large);\n  border-radius: var(--atom-radius-r3);\n  font-size: 16px;\n}\n\n/* --- variants ------------------------------------------------------------ */\n.atom-select--variant_outline .atom-select__trigger {\n  background: var(--atom-bg-layer);\n  box-shadow: var(--atom-control-border);\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-select__trigger:hover {\n  filter: brightness(0.96);\n}\n\n/* One ring for every control — see the Control surface block in tokens.css.\n   The trigger is the focusable element here (Input hands focus to an inner\n   <input>), so base.css's outset ring is replaced rather than layered: an\n   outset ring with a 2px offset crosses the 4px gap to its neighbour when a\n   field puts this next to another control. Placed above the invalid rule so\n   invalid keeps outranking focus. */\n.atom-select__trigger:focus-visible {\n  outline: none;\n  box-shadow: var(--atom-control-border-focus);\n}\n\n.atom-select[data-state=\"open\"] .atom-select__caret {\n  transform: rotate(180deg);\n}\n\n.atom-select[data-state=\"closed\"] .atom-select__listbox {\n  display: none;\n}\n\n.atom-select__option[aria-selected=\"true\"] {\n  background: var(--atom-bg-neutral-weak);\n}\n\n.atom-select__option[aria-selected=\"true\"] .atom-select__option-check {\n  opacity: 1;\n}\n\n.atom-select__option:hover:not([aria-disabled=\"true\"]) {\n  background: var(--atom-bg-neutral-weak);\n}\n\n.atom-select__option[aria-disabled=\"true\"] {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n.atom-select[data-invalid] .atom-select__trigger {\n  box-shadow: var(--atom-control-border-invalid);\n}\n\n.atom-select[data-disabled] {\n  opacity: 0.4;\n  pointer-events: none;\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/select.css"
    }
  ],
  "categories": [
    "selection"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "element",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "Select"
    ],
    "docs": "poc/design-system/components/select.md"
  }
}
