# Selector Contract

`@orioncactuscorp/ui` 컴포넌트를 consumer CSS에서 커스터마이즈할 때 사용하는 안정 selector 계약입니다. CSS Module 해시 클래스는 빌드마다 바뀌므로 selector로 사용하지 않고, 컴포넌트가 노출하는 data attribute를 사용합니다.

이 문서의 selector·slot 표는 계약이며, 변경·제거 규칙은 [Stability Policy](./stability.md)를 따릅니다.

## 커스터마이즈 selector 3계층

| 계층          | 형태                                                                                                                  | 용도                               |
| ------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| 컴포넌트 루트 | `data-oc-component="<name>"`                                                                                          | 독립 시각 컴포넌트/프리미티브 식별 |
| 내부 slot     | `data-oc-part="<slot>"`                                                                                               | 컴포넌트 내부의 안정 slot          |
| 시각/상태     | `data-oc-size`, `data-oc-variant`, `data-oc-appearance`, `data-oc-state`, `data-oc-invalid`, `data-oc-orientation` 등 | 컴포넌트가 소유한 시각·상태 분기   |

값은 kebab-case입니다.

```css
/* 예: invalid 상태의 TextInput content 표면 커스터마이즈 */
[data-oc-component='text-input'][data-oc-invalid='true']
  [data-oc-part='content'] {
  --oc-field-control-stroke-color: var(--oc-color-theme-status-negative);
}
```

다음은 selector로 사용하지 마세요.

- `@orioncactuscorp/ui`의 CSS Module 해시 클래스
- 아래 [Legacy 호환 출력](#legacy-호환-출력)에 있는 구형 attribute
- 계약에 없는 incidental wrapper에 의존하는 DOM 깊이 selector

## Override가 동작하는 방식

- 공개 component contract 변수는 property fallback 또는 `--_-{component}-*` internal default를 통해 기본값을 제공합니다. 따라서 per-component CSS chunk가 consumer CSS보다 나중에 로드되어도 consumer host class의 값이 유지됩니다.
- 컴포넌트 소스는 루트 modifier selector를 `:where(...)`로 낮은 specificity로 유지합니다. consumer 클래스 하나로 루트 레벨의 공개 `--oc-*` component contract 변수를 덮을 수 있습니다.
- 스타일 override는 paint property를 직접 재선언하기보다 컴포넌트가 노출하는 contract 변수(`--oc-{component}-*`)를 덮는 방식을 우선하세요. 상태 전이·모션과의 일관성이 유지됩니다.
- 방향성 계약(`--oc-*` 변수, `data-oc-*` 값, prop)은 물리 방향(top/right/bottom/left)이 아니라 논리 방향(`block-start`, `inline-end`, `start`, `end` 등)을 사용합니다. RTL에서 자동으로 뒤집힙니다.

## 채워진 컨트롤 색상 계약

채워진 컨트롤은 배경과 그 위의 전경을 한 쌍의 component contract 변수로 노출합니다. 기본값은 package theme과 같고, consumer는 내부 slot이나 상태 selector를 복제하지 않고 컴포넌트 루트에서 두 색을 함께 조정할 수 있습니다.

| Component              | Background contract                            | Foreground contract                 | 적용 상태                       |
| ---------------------- | ---------------------------------------------- | ----------------------------------- | ------------------------------- |
| `Checkbox`             | `--oc-checkbox-background-color`               | `--oc-checkbox-color`               | checked, indeterminate          |
| `Radio`                | `--oc-radio-background-color`                  | `--oc-radio-color`                  | checked                         |
| `Switch`               | `--oc-switch-background-color`                 | `--oc-switch-color`                 | checked track, thumb            |
| `BackgroundIconButton` | `--oc-background-icon-button-background-color` | `--oc-background-icon-button-color` | 모든 variant의 background, icon |

`BackgroundIconButton`의 disabled 상태는 불투명한 fill 레이어를 컴포넌트 루트 위에 덮어 그립니다. blend mode와 backdrop-filter가 비선형으로 합성되기 때문에, 장식 레이어를 그대로 둔 채 이 레이어의 `opacity`만 올려야 전환이 두 끝 상태의 선형 보간이 되어 중간에 튀지 않습니다. 이 fill 색은 `--oc-background-icon-button-disabled-background-color`로 재정의할 수 있고, 기본값은 `--oc-color-theme-interaction-disable`입니다. disabled에서는 이 레이어가 배경을 덮으므로 `--oc-background-icon-button-background-color` 값은 보이지 않습니다.

```css
.darkPrimaryControl {
  --oc-radio-background-color: var(--oc-color-theme-label-strong);
  --oc-radio-color: var(--oc-color-theme-background-normal-normal);
  --oc-interaction-color: var(--oc-radio-color);
}
```

`BackgroundIconButton`의 키보드 포커스 링은 본체보다 바깥으로 확장된 배경 크기와 `--oc-background-icon-button-radius`를 따릅니다. 공통 `--oc-focus-ring-width`, `--oc-focus-ring-offset`, `--oc-color-theme-focus-ring` 토큰으로 조정할 수 있습니다. 링은 배경의 clipping이나 Interaction의 opacity에 영향을 받지 않는 독립적인 장식 레이어이며, 버튼의 레이아웃과 터치 영역은 바꾸지 않습니다.

## Interaction 프리미티브

`Interaction`은 hover/press/focus 피드백을 그리는 시각 레이어입니다. 시각 pseudo는 passive이며 부모 컴포넌트가 자기 상태 selector로 Interaction의 opacity를 제어합니다. `self`가 아닌 Interaction은 자기 상태를 만들지 않지만, 선택적으로 확장된 투명 hit pseudo의 pointer event가 host로 버블링될 수 있습니다.

```scss
.myControl {
  &:focus-visible > [data-oc-component='interaction'] {
    opacity: var(--oc-interaction-opacity-focus);
  }

  @media (hover: hover) {
    &:hover:not(:disabled) > [data-oc-component='interaction'] {
      opacity: var(--oc-interaction-opacity-hover);
    }
  }
}
```

`Interaction variant="light|normal|strong"`는 primitive 인스턴스의 기본 강도를 정합니다. 색을 커스텀한 host나 합성 영역에서 강도를 함께 바꾸려면 Interaction을 직접 찾지 말고 host에 `data-oc-interaction-variant="light|normal|strong"`를 설정하세요. host selector가 primitive 기본값보다 우선하며 custom property 상속을 통해 모든 하위 Interaction에 적용됩니다.

상태 레이어 색은 host-scoped contract인 `--oc-interaction-color`로 지정합니다. 기본 fallback은 `--oc-color-theme-label-normal`이며 Button처럼 appearance나 variant별 기본값이 fallback과 다른 owner component만 같은 변수를 설정합니다. 별도 매핑이 없는 owner는 primitive fallback과 host 상속을 그대로 사용합니다. consumer는 나중에 로드되는 단일 host 클래스에서 이 변수를 덮을 수 있습니다. 채워진 surface를 커스텀할 때 상태 레이어가 전경색을 따르게 하려면 `--oc-interaction-color: var(--oc-{component}-color)`처럼 명시적으로 연결하세요. foreground와 state-layer를 강제로 결합하지 않으므로 기존 기본 시각과 별도 상태색 customization을 함께 유지할 수 있습니다.

`:root` 또는 바깥 theme scope에서 선언한 값도 상속되지만, owner가 appearance나 variant 기본값을 같은 변수에 직접 선언하면 더 가까운 owner 값이 우선합니다. 따라서 `--oc-interaction-color`는 모든 Interaction을 일괄 교체하는 global theme token이 아니며, 일관된 customization이 필요하면 대상 component host에 설정합니다.

커스텀 React host의 props를 선언할 때는 package root에서 `InteractionHostProps`를 import해 같은 named contract를 재사용할 수 있습니다.

```tsx
<Button
  className={styles.brandButton}
  data-oc-interaction-variant='strong'
>
  확인
</Button>

<ActionArea data-oc-interaction-variant='strong'>
  <ActionAreaButton priority='main'>저장</ActionAreaButton>
  <ActionAreaButton priority='sub'>취소</ActionAreaButton>
</ActionArea>
```

중첩 영역은 `normal`을 명시해 바깥 scope를 재설정할 수 있습니다. `Menu`는 content가 portal에 렌더링되므로 trigger나 `MenuRoot`가 아니라 `MenuContent`에 selector를 설정합니다.

| Contract                                              | 기본값                                           | 용도                                      |
| ----------------------------------------------------- | ------------------------------------------------ | ----------------------------------------- |
| `--oc-interaction-color`                              | `--oc-color-theme-label-normal`                  | host와 하위 Interaction의 상태 레이어 색  |
| `data-oc-interaction-variant="light\|normal\|strong"` | 미설정                                           | host와 하위 Interaction의 named 강도 선택 |
| `--oc-interaction-opacity-factor`                     | primitive `variant`의 `0.75\|1\|1.5`             | host 범위의 직접 강도 조정 escape hatch   |
| `--oc-interaction-opacity-hover`                      | `--oc-color-global-opacity-5 × resolved factor`  | Interaction이 제공하는 hover 출력값       |
| `--oc-interaction-opacity-focus`                      | `--oc-color-global-opacity-8 × resolved factor`  | Interaction이 제공하는 focus 출력값       |
| `--oc-interaction-opacity-active`                     | `--oc-color-global-opacity-12 × resolved factor` | Interaction이 제공하는 active 출력값      |
| `--oc-interaction-touch-target-min`                   | component별 opt-in, foundation은 `44px`          | host 범위의 최소 pointer hit area         |

`--oc-interaction-opacity-*`는 custom host가 상태 selector에서 읽는 resolved output 계약입니다. 임의 강도를 지정할 때는 이 출력값을 자식 selector로 덮지 말고 host에서 `--oc-interaction-opacity-factor`를 설정하세요.

터치 대상 최소 크기는 foundation `--oc-touch-target-min`의 기본값 `44px`을 사용합니다. Button, button/anchor TextButton, IconButton, BackgroundIconButton, interactive Badge, CheckMark, Checkbox, Radio, Switch는 이 값을 internal default로 선택합니다. 실제 component host에서 `--oc-interaction-touch-target-min`을 지정하면 CSS 로딩 순서와 무관하게 해당 값이 우선하며, `0px`은 Interaction의 기존 geometry 바깥으로 hit area를 확장하지 않습니다. 시각 Interaction의 inset, color, radius, opacity, motion geometry는 터치 영역과 분리되어 유지됩니다.

저수준 custom host는 `<Interaction touchTarget />`으로 hit layer를 opt-in합니다. 이때 `data-oc-touch-target="true"`가 렌더링되며, prop을 생략한 기존 `<Interaction />`은 passive visual layer와 기존 pointer geometry를 그대로 유지합니다.

`button`, `a`, `label`, `summary` 또는 interactive ARIA role host(`button`, `link`, `checkbox`, `radio`, `switch`, `tab`)가 `Interaction`을 직계 자식으로 사용하면 native tap highlight가 자동으로 비활성화됩니다. 이 계약은 `touchTarget`, `self`, `variant`와 무관합니다. host는 자체 pressed/selected feedback과 keyboard focus 표시를 담당합니다.

커스텀 host에서 passive Interaction을 사용할 때는 눌림 피드백도 연결합니다. `Interaction`만 렌더링하고 상태 스타일을 연결하지 않으면 native highlight가 사라진 뒤 눌림 피드백이 없을 수 있습니다.

```scss
.customAction:active:not(:disabled, [aria-disabled='true'])
  > [data-oc-component='interaction'] {
  opacity: var(--oc-interaction-opacity-active);
}
```

`touchTarget`은 hit layer를 활성화하며, 해당 host의 accidental text selection도 비활성화합니다. `touchTarget`을 생략한 host에는 텍스트 선택 금지를 추가하지 않습니다. 선택 가능한 control label이 필요한 예외는 host class에서 `user-select: text`를 다시 선언할 수 있습니다. 링크의 long-press context menu와 브라우저 gesture는 유지하므로 `-webkit-touch-callout`과 `touch-action`은 변경하지 않습니다.

Interaction이 더 깊게 중첩되는 custom composition은 실제 interactive host에 `-webkit-tap-highlight-color: transparent`를 직접 선언해야 합니다. 모든 조상이나 컨테이너로 억제 범위를 확장하지 않습니다. Modal handle과 ScrollArea track은 이 규칙에 따라 owner에서 처리합니다. 텍스트 선택과 drag 정책은 각 owner가 별도로 결정합니다.

```scss
.compactAction {
  --oc-interaction-touch-target-min: 32px;
}

.largeTouchAction {
  --oc-interaction-touch-target-min: 48px;
}
```

확장 영역은 layout 공간을 만들지 않습니다. oc-ui가 소유한 복합 layout은 형제 hit area가 겹치지 않게 자체 spacing 계약을 사용하며, consumer가 작은 control을 조밀하게 배치할 때는 layout gap을 확보하거나 host별 최소 크기를 조정해야 합니다. host 바깥 hit testing을 위해 기본 적용 대상은 Interaction ancestor에서 overflow clipping을 사용하지 않으며, 시각 radius는 passive visual layer에서 유지합니다. disabled host는 확장 hit area도 비활성화합니다. Tabs는 기존 `--oc-tabs-tab-interaction-margin-inline`과 list gap 계약을, ScrollArea scrollbar는 데스크톱 drag geometry를 우선하므로 44px 기본 확장을 선택하지 않습니다.

## Badge 계약

| Component | Root selector               | Visual selectors                                                                                                   | Stable parts                                                                 |
| --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `Badge`   | `data-oc-component="badge"` | `data-oc-variant="default\|normal\|accent\|overlay"`, `data-oc-size="xsmall\|small"`, `data-oc-interactive="true"` | `content`, `surface`, `surface-default`, `surface-normal`, `surface-overlay` |

`Badge`의 작은 named size는 `data-oc-size="xsmall"`을 사용합니다. 이전
`data-oc-size="tiny"` 값은 major 변경으로 제거됐습니다.
`default`가 canonical 기본 variant이며, `normal`은 기존 소비자를 위한 호환
선택지입니다. variant를 생략하면 `data-oc-variant="default"`가 출력됩니다.
`default`는 `label-strong` 배경과 `background-normal-normal` 라벨 색을 사용하고,
`normal`은 기존 `label-alternative` 라벨과 `fill-normal` 배경을 유지합니다.

Badge의 `surface` part는 배경 레이어를 담당하며, `accent` 배경이 사라지고
`default` 또는 `overlay` 배경이 나타나는 전환을 별도 opacity 레이어로 처리합니다.
border는 다음 component contract 변수로 조정할 수 있습니다.

| Contract variable         | Default       | 용도              |
| ------------------------- | ------------- | ----------------- |
| `--oc-badge-border-width` | `1px`         | inset border 두께 |
| `--oc-badge-border-color` | `transparent` | inset border 색상 |

border는 Badge의 기존 radius를 따르며 `oc-border` 기반의 고정 all/inset stroke로
그려집니다. 색상만 지정하면 기본 1px border가 표시되고, width를 함께 override할
수 있습니다. 예를 들어 다음처럼 색상만 지정할 수 있습니다.

```scss
.badgeWithBorder {
  --oc-badge-border-color: var(--oc-color-theme-line-normal-normal);
}
```

## Field 레이아웃 계약

`Field`는 label/control/message compound 구조를 유지한 채 vertical/horizontal orientation을 노출합니다.

| Component      | Root selector                       | Orientation selector                         | Part selector            | Stable parts                      |
| -------------- | ----------------------------------- | -------------------------------------------- | ------------------------ | --------------------------------- |
| `Field`        | `data-oc-component="field"`         | `data-oc-orientation="vertical\|horizontal"` | -                        | `field-control`, message 컴포넌트 |
| `FieldControl` | `data-oc-component="field-control"` | -                                            | `data-oc-part="control"` | 렌더링된 control 컴포넌트를 감쌈  |

- horizontal label 폭은 `--oc-field-label-inline-size`로 제어합니다. `labelWidth` prop의 숫자 값은 `16px = 1rem` 기준 rem으로 변환됩니다.
- `form`/`fieldset` 같은 상위 요소에 같은 변수를 지정하면 여러 Field가 label 폭을 공유합니다.
- `--oc-field-label-max-inline-size`는 label 컬럼 상한이며 기본값은 `35%`입니다.
- horizontal의 helper/error message는 control 컬럼에 배치되어 input 시작선에 정렬됩니다.
- `data-oc-label-placement='outside|floating'`은 라벨 배치 요청을 나타냅니다. 가로 Field에서는 `outside`로 정규화합니다. floating 배치는 직접 자식 FieldControl 안의 호환 TextInput과 조합했을 때 적용됩니다.
- floating 배치에서도 FieldLabel은 FieldControl의 형제로 유지하며 같은 Grid 영역에 겹쳐 표시합니다. 별도 라벨 복제 DOM은 없습니다.
- `--oc-field-floating-scale`(기본 `0.75`)과 `--oc-field-floating-reserve`(기본 `body1 line-height × 0.75 + item-gap-micro`)는 축소 비율과 라벨 예약 공간을 조정합니다. 슬롯 측정용 `--oc-field-floating-*-size/offset`은 내부 계산값입니다.
- `data-oc-floating="true"`는 컨트롤의 floating 지원 조건을 나타냅니다. 실제 floating 스타일은 소유 Field에 직접 자식 FieldLabel이 존재할 때만 적용합니다. 라벨이 없으면 placeholder를 그대로 표시하고 라벨 공간을 예약하지 않습니다. 이 판정은 CSS에서 이루어져 hydration 후 라벨 등록 effect를 기다리지 않습니다.
- multi-control row는 `Field` layout 책임이 아닙니다. 내부 control group을 `FieldControl` 안에서 조합하거나, 하나의 label이 여러 native control을 설명해야 하면 시맨틱 grouping 프리미티브를 사용하세요.

## Field Control slot 계약

| Component   | Root selector                    | Native part               | Content surface part     | Slot parts                                                  |
| ----------- | -------------------------------- | ------------------------- | ------------------------ | ----------------------------------------------------------- |
| `TextInput` | `data-oc-component="text-input"` | `data-oc-part="input"`    | `data-oc-part="content"` | `leading-content`, `trailing-content`                       |
| `TextArea`  | `data-oc-component="text-area"`  | `data-oc-part="textarea"` | `data-oc-part="content"` | `supporting-content`, `leading-content`, `trailing-content` |
| `Select`    | `data-oc-component="select"`     | `data-oc-part="select"`   | `data-oc-part="content"` | `leading-content`, `trailing-content`, `indicator`          |

slot content 프리미티브는 독립 컴포넌트 selector를 노출합니다.

`TextInput`의 `data-oc-variant='outlined|underlined'`는 surface 형태이며 기본값은 `outlined`입니다.
호환되는 floating 조합에는 `data-oc-floating='true'`를 표시합니다. 이때 기존 content/input/leading-content/trailing-content DOM은 유지됩니다.
floating input에 비어 있지 않은 placeholder가 있으면 native input에 `data-oc-has-placeholder='true'`를 표시합니다. 이 조합에서는 input 포커스 시 빈 라벨도 위로 이동하고 native placeholder가 fade로 표시됩니다.

FieldLabel 색상은 outside/floating 모두 `--oc-field-label-color`를 사용합니다. 기본은 `label-alternative`이며 포커스나 오류 상태에서도 올라간 floating 라벨과 outside 라벨은 이 색상을 유지합니다. underlined의 floating 라벨이 입력 영역에 내려와 있고 직접 연결된 control이 invalid일 때만 오류 색상을 사용합니다. 빈 값이어도 placeholder 때문에 포커스 시 라벨이 올라가면 기본 색상으로 돌아옵니다. InputGroup의 라벨에도 같은 상태 규칙을 적용합니다. 필수 표시는 라벨 색상을 따르며 색상 전환은 `oc-motion` feedback을 사용합니다.

| Primitive          | Selector                                 | Variants                                       | 접근성 기본값                              |
| ------------------ | ---------------------------------------- | ---------------------------------------------- | ------------------------------------------ |
| `TextInputContent` | `data-oc-component="text-input-content"` | `data-oc-variant="text\|icon\|action\|custom"` | `icon` variant는 `aria-hidden="true"` 기본 |
| `TextAreaContent`  | `data-oc-component="text-area-content"`  | `data-oc-variant="text\|icon\|action\|custom"` | `icon` variant는 `aria-hidden="true"` 기본 |
| `SelectContent`    | `data-oc-component="select-content"`     | `data-oc-variant="text\|icon\|action\|custom"` | `icon` variant는 `aria-hidden="true"` 기본 |

`TextArea`는 `supporting-content`에 `data-oc-align`을 소유합니다.

`TextArea`의 `sizing-probe`는 fluid 글자 치수 변화를 감지하는 비표시 내부 슬롯입니다. `aria-hidden`이며 폼 컨트롤이나 사용자 콘텐츠 슬롯이 아닙니다. 자동 높이는 입력값, 폼 초기화, 가로 폭, 글자 치수, 보조 콘텐츠 크기 변경에 맞춰 갱신됩니다.

세로 underlined outside Field는 라벨과 컨트롤 사이에 `item-gap-micro`를 적용하고 입력 상단 padding을 제거합니다. 헬퍼 간격은 `item-gap-xsmall`로 유지합니다. 내부 `--oc-field-outside-padding-block-start`와 `--oc-field-control-content-padding-block-start`는 이 배치를 전달하며, 음수 margin에 의존하지 않습니다.

서버 렌더링에서는 `minRows` 공간을 CSS로 확보하고 hydration 시 실제 내용 높이를 계산합니다. `minRows`를 초과하는 초기 내용이나 뒤늦게 바뀌는 웹폰트의 줄바꿈까지 서버에서 예측하지는 않습니다. 첫 화면의 공간 확보가 필요하면 예상 내용에 맞는 `minRows`와 애플리케이션의 폰트 로딩 정책을 함께 설정합니다.

`--oc-text-area-min-rows`는 첫 레이아웃에서 minRows만큼의 줄 높이를 확보하는 내부 CSS 값입니다. floating의 상단 예약 공간은 body1 라인 높이의 0.75배와 item-gap-micro로 계산해 fluid typography를 따릅니다.

Field의 labelPlacement 기본값은 floating입니다. underlined TextInput과 TextArea는 data-oc-floating 상태를 사용하며, outlined 및 지원하지 않는 컨트롤과 가로 Field는 외부 라벨 배치를 유지합니다.

`TextArea` root는 `data-oc-variant='outlined|underlined'`를 소유하며 기본값은 `outlined`입니다. 두 형태 모두 native `textarea`와 기존 content/supporting-content slot 구조를 유지합니다.

| `data-oc-align` | 의미                         |
| --------------- | ---------------------------- |
| `start`         | leading content만 존재       |
| `end`           | trailing content만 존재      |
| `space-between` | leading과 trailing 모두 존재 |

## Cell slot 계약

| Component  | Root selector                   | Visual selectors                                                                                                            | Stable parts                                                     |
| ---------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `Cell`     | `data-oc-component="cell"`      | `data-oc-divider="normal\|neutral\|alternative\|none"`, `data-oc-outer-padding="true"`                                      | -                                                                |
| `CellItem` | `data-oc-component="cell-item"` | `data-oc-fill-width="true\|false"`, `data-oc-interactive="true"`, `data-oc-divider`, `data-oc-vertical-align="top\|center"` | `leading`, `content`, `text`, `label`, `description`, `trailing` |

- `fillWidth=true`는 row가 inline padding을 소유하고 interaction 레이어를 row 경계 안에 둡니다. `false`는 content를 flush로 두고 interaction 레이어만 inline margin으로 확장합니다.
- `verticalAlign=top`은 첫 content 줄 기준 정렬, `center`는 전체 content 높이 기준 중앙 정렬입니다.
- `Cell`은 기본적으로 첫 항목의 위쪽, 마지막 항목의 아래쪽 padding을 음수 margin으로 상쇄해 목록 가장자리를 주변 콘텐츠와 정렬합니다. `outerPadding=true`(`data-oc-outer-padding="true"`)는 이 상쇄를 끄고 padding을 유지합니다. `Accordion`도 같은 계약을 따릅니다.
- `interactive=true`일 때 row 활성화와 별개로 동작해야 하는 내부 control은 `data-oc-cell-control`로 opt-in한 경우에만 row 클릭이 활성화합니다.

## PositionSnap 계약

`PositionSnapPositioner`는 위치와 drag 상태를 소유하며, 내부 control의 transform이나 시맨틱을 변경하지 않습니다.

| Component      | Root selector                       | Visual selectors                                                           | Stable parts                         |
| -------------- | ----------------------------------- | -------------------------------------------------------------------------- | ------------------------------------ |
| `PositionSnap` | `data-oc-component="position-snap"` | `data-oc-drag="content\|handle"`, `data-oc-dragging`, `data-oc-positioned` | `positioner`, `position-snap-handle` |

위치 좌표는 PositionSnap runtime의 내부 계약이며 consumer는 `position`, `boundary`, `boundaryPadding` prop으로 제어합니다. 기본 paint order는 docked·raised control 단계인 `--oc-zindex-2`이며, owner 문맥에서 조정해야 할 때만 공개 계약인 `--oc-position-snap-z-index`를 덮습니다.

## Runtime Motion 계약

runtime motion wrapper는 SCSS `oc-motion`과 같은 intent/phase/pace/reduced vocabulary를 사용하며, 시맨틱 상태와 진행 중 애니메이션 상태를 분리해 노출합니다.

| Component      | Root selector                       | Visual selectors                                                                                                 | Stable parts |
| -------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------ |
| `MotionExpand` | `data-oc-component="motion-expand"` | `data-oc-state="open\|closed"`, `data-oc-motion-intent`, `data-oc-motion-state="closed\|opening\|open\|closing"` | `content`    |

`prefers-reduced-motion: reduce`가 활성일 때 `data-oc-reduced-motion="true"`가 방출됩니다. reduced 상태의 `block-size` 같은 non-feedback property expand는 즉시 해소되며 mount/unmount presence 계약은 동일합니다.

## Accordion 계약

summary row는 Cell slot 계약을 시각적으로 따르되 trigger는 native button입니다. details expand는 CSS grid가 아니라 React runtime motion 레이어가 담당합니다.

| Component              | Root selector                               | Visual selectors                                                                                                                                  | Stable parts                                                                                  |
| ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `Accordion`            | `data-oc-component="accordion"`             | `data-oc-divider="normal\|neutral\|alternative\|none"`, `data-oc-disabled="true"`, `data-oc-animation="disabled"`, `data-oc-outer-padding="true"` | -                                                                                             |
| `AccordionItem`        | `data-oc-component="accordion-item"`        | `data-oc-state="open\|closed"`, `data-oc-divider`, `data-oc-disabled="true"`                                                                      | -                                                                                             |
| `AccordionSummary`     | `data-oc-component="accordion-summary"`     | `data-oc-state="open\|closed"`, `data-oc-fill-width="true\|false"`, `data-oc-vertical-align="top\|center"`, `data-oc-disabled="true"`             | `header`, `leading`, `content`, `text`, `label`, `description`, `trailing`, `indicator-frame` |
| `AccordionDetails`     | `data-oc-component="accordion-details"`     | `data-oc-state="open\|closed"`, `data-oc-motion-intent`, `data-oc-motion-state`, `data-oc-reduced-motion="true"`                                  | `panel-inner`                                                                                 |
| `AccordionDescription` | `data-oc-component="accordion-description"` | -                                                                                                                                                 | -                                                                                             |
| `AccordionContent`     | `data-oc-component="accordion-content"`     | -                                                                                                                                                 | -                                                                                             |

`AccordionSummary`가 `aria-expanded`/`aria-controls`/summary id를, `AccordionDetails`가 대응 id와 `aria-labelledby`를 소유합니다. 닫힌 force-mounted details는 `aria-hidden`과 `inert`로 접근성 트리·포커스에서 제외됩니다.

## Tabs 계약

Tabs는 Base UI의 선택·키보드·ARIA 연결을 사용하고, oc-ui가 size, layout, indicator, overflow 표현과 마우스 드래그 스크롤을 소유합니다.

`TabsList dragScroll`은 기본 `true`이며 실제 `scroller`에 `data-oc-drag-scroll="true|false"`, `data-oc-dragging="true|false"`를 제공합니다. 넘치는 목록에서 마우스 드래그는 선택을 유지하고, 일반 클릭은 포커스와 선택을 처리합니다. `dragScroll={false}`는 기존 마우스 동작을 유지합니다. 터치와 휠은 네이티브 스크롤을 사용합니다. 자세한 입력 정책은 [드래그 스크롤](drag-scroll.md)을 참고하세요.

| Component   | Root selector              | Visual selectors                                                                                           | Stable parts                        |
| ----------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `TabsRoot`  | `data-oc-component="tabs"` | -                                                                                                          | -                                   |
| `TabsList`  | `data-oc-part="list"`      | `data-oc-size="small\|medium\|large"`, `data-oc-full-width="true\|false"`, `data-oc-padding="true\|false"` | `scroller`, `indicator`, `trailing` |
| `TabsTab`   | `data-oc-part="tab"`       | `aria-selected="true\|false"`, `aria-disabled="true"`                                                      | `label`                             |
| `TabsPanel` | `data-oc-part="panel"`     | `hidden`                                                                                                   | -                                   |

`TabsTab`의 선택·disabled 스타일은 Base UI 런타임 attribute인 `data-active`/`data-disabled`가 아니라 `aria-selected`/`aria-disabled`를 기준으로 합니다. disabled tab은 `aria-disabled="true"`이지만 native `disabled`가 아니며 키보드 포커스를 받을 수 있습니다. `TabsPanel keepMounted`는 비활성 panel을 DOM에 유지하고 `hidden`으로 접근성 트리에서 제외합니다.

Base UI가 tab 관계와 키보드 탐색을 위해 생성하는 `role`, `tabIndex`, `aria-selected`, `aria-controls`, `aria-labelledby`, `aria-orientation`, panel `id`/`hidden`은 컴포넌트 소유 계약이며 소비자 prop으로 재정의할 수 없습니다.

Tab typography는 `TabsList`의 `size`에 따라 large는 `headline2/bold`, medium은 `body1/bold`, small은 `body2/bold`가 적용되고 각 `TabsTab`이 상속합니다. 소비자는 `TabsList`의 `className` 또는 `[data-oc-part='list']` selector에서 `font-size`, `line-height`, `letter-spacing`, `font-weight`를 재정의해 모든 tab item의 typography를 일괄 조정할 수 있습니다.

Tab content는 inline padding 없이 배치되고 배경색이 transparent인 self `Interaction`이 `--oc-tabs-tab-interaction-margin-inline`만큼 좌우 바깥으로 확장되어 실제 pointer hit area가 됩니다. 인접한 Interaction이 겹치지 않도록 기본 gap은 이 값의 2배이며 `padding` 상태와 무관하게 유지됩니다. `padding=false`이면 list 좌우 여백이 없고 양끝 Interaction의 바깥 확장은 list 경계에서 잘립니다. `padding=true`이면 `--oc-space-section-padding-viewport-x`가 기본 좌우 여백으로 적용됩니다. `ModalHeader bottomContent` 안의 `padding=true` list는 viewport 여백 대신 Modal header의 좌우 여백(`--oc-modal-header-padding-inline`, 기본값은 `--oc-modal-container-padding-inline`)을 사용해 header 콘텐츠 시작선에 정렬합니다. `leadingContent`가 없으면 title 시작선과 같고, 있으면 title이 아니라 leading content가 놓인 header 여백 시작선을 기준으로 합니다. 이 자동 정렬은 specificity 0이므로 `TabsList`의 `className` 또는 list selector에서 `--oc-tabs-list-padding-inline`을 지정하면 그 값이 우선하며, Modal보다 바깥 조상에 지정한 값은 적용되지 않습니다.

다음 `--oc-tabs-*` 변수로 list와 tab의 spacing, 상태 색, indicator, overflow fade를 조정할 수 있습니다.

| Variable                                  | Default                                                                                                                | Scope                        |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| `--oc-tabs-list-padding-inline`           | `0px`; `padding=true`이면 `--oc-space-section-padding-viewport-x`, Modal header-bottom 안에서는 Modal header 좌우 여백 | list 좌우 여백               |
| `--oc-tabs-list-padding-block`            | `0px`                                                                                                                  | list 상하 여백               |
| `--oc-tabs-list-gap`                      | `--oc-tabs-tab-interaction-margin-inline`의 2배                                                                        | tab 사이 간격                |
| `--oc-tabs-tab-interaction-margin-inline` | `--oc-space-item-padding-xsmall`                                                                                       | 각 tab Interaction 좌우 확장 |
| `--oc-tabs-tab-padding-block`             | large/medium은 `--oc-space-item-padding-mini`, small은 `--oc-space-item-padding-tiny`                                  | 각 tab 상하 클릭 영역        |
| `--oc-tabs-edge-fade-size`                | `3rem`                                                                                                                 | overflow edge fade 크기      |
| `--oc-tabs-indicator-block-size`          | `0.125rem`                                                                                                             | indicator 두께               |
| `--oc-tabs-indicator-radius`              | `--oc-atomic-radius-max`                                                                                               | indicator 모서리 반경        |
| `--oc-tabs-indicator-color`               | `--oc-color-theme-label-strong`                                                                                        | 선택 indicator 색            |
| `--oc-tabs-indicator-disabled-color`      | `--oc-color-theme-label-disable`                                                                                       | disabled 선택 indicator 색   |
| `--oc-tabs-label-color`                   | `--oc-color-theme-label-assistive`                                                                                     | 기본 label 색                |
| `--oc-tabs-label-hover-color`             | `--oc-color-theme-label-alternative`                                                                                   | hover label 색               |
| `--oc-tabs-label-selected-color`          | `--oc-color-theme-label-strong`                                                                                        | 선택 label 색                |
| `--oc-tabs-label-disabled-color`          | `--oc-color-theme-label-disable`                                                                                       | disabled label 색            |

## Icon 계약

| Primitive | Selector                       | 접근성 기본값                                                                               |
| --------- | ------------------------------ | ------------------------------------------------------------------------------------------- |
| `SvgIcon` | `data-oc-component="svg-icon"` | 기본 `aria-hidden="true"`. 의미 있는 아이콘은 `role="img"` + `aria-label`/`aria-labelledby` |

## Loading placeholder 계약

상태 안내가 필요하면 `Loading`(assistive technology에 status announce), 부모 영역이 loading 시맨틱을 이미 소유하면 `Skeleton`(장식)을 사용합니다.

| Component  | Root selector                  | Visual selectors                                                                  | Stable parts |
| ---------- | ------------------------------ | --------------------------------------------------------------------------------- | ------------ |
| `Loading`  | `data-oc-component="loading"`  | `data-oc-variant="circular"`, `data-oc-size`                                      | `indicator`  |
| `Skeleton` | `data-oc-component="skeleton"` | `data-oc-variant="text\|rectangle\|circle"`, `data-oc-align`, `data-oc-animation` | `fill`       |

`Skeleton`은 기본 장식이라 `aria-hidden="true"`를 강제하고 consumer의 `role`/`tabIndex` prop을 무시합니다. 폭·높이·radius·색·opacity는 `--oc-skeleton-*` 변수로 노출됩니다.

## Progress indicator 계약

`ProgressIndicator`는 알려진 범위 안의 진행률을 표시하는 읽기 전용 선형 요소입니다. `value`와 `max`는 시각적 길이와 `aria-valuenow`/`aria-valuemax`에 함께 반영되며, consumer는 `aria-label` 또는 `aria-labelledby`로 접근 가능한 이름을 제공합니다.

| Component           | Root selector                            | Visual selectors | Stable parts |
| ------------------- | ---------------------------------------- | ---------------- | ------------ |
| `ProgressIndicator` | `data-oc-component="progress-indicator"` | `data-oc-size`   | `indicator`  |

배경 track과 진행 indicator 색, 선 두께, radius는 `--oc-progress-indicator-background-color`, `--oc-progress-indicator-color`, `--oc-progress-indicator-block-size`, `--oc-progress-indicator-radius` 변수로 조정할 수 있습니다.

## Action Area 계약

`ActionAreaLayout`은 스크롤 가능한 body와 고정 footer slot 사이의 scroll 경계를 소유합니다.

| Component                 | Root selector                                   | Visual selectors                                                       | Stable parts                                                      |
| ------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `ActionArea`              | `data-oc-component="action-area"`               | `data-oc-variant`, `data-oc-background="true"`, `data-oc-empty="true"` | `actions`, `caption`, `extra-slot`, `compact-row`, `compact-slot` |
| `ActionAreaLayout`        | `data-oc-component="action-area-layout"`        | `data-oc-scrollable="true\|false"`                                     | -                                                                 |
| `ActionAreaLayout.Body`   | `data-oc-component="action-area-layout-body"`   | -                                                                      | `body`                                                            |
| `ActionAreaLayout.Footer` | `data-oc-component="action-area-layout-footer"` | -                                                                      | `footer`                                                          |

- `data-oc-scrollable="true"`는 body에 보이는 scrollport 밖 overflow가 있다는 뜻입니다. 이 상태에서 직속 footer `ActionArea`는 block-end padding을 유지하고 `--oc-action-area-padding-block-start: 0`을 받으며, 대응하는 block-start 간격은 body에 예약됩니다.
- `ActionAreaLayout.ActionArea background="auto"`는 scroll 상태를 자식 `ActionArea`의 background 계약에 매핑합니다: body가 스크롤 가능하고 바닥에 닿지 않은 동안만 `data-oc-background="true"`가 방출됩니다. `data-oc-background`는 시각 gradient/background 상태이며 body 예약의 근거로 사용하지 마세요.
- 렌더링할 액션, caption, extraContent가 없는 `ActionArea`는 `data-oc-empty="true"`를 방출하고 자체 padding과 background를 접습니다. `ModalFooter`나 `FloatingWindowFooter`의 직속 빈 `ActionArea`는 footer 예약을 `0`으로 만들어 body는 기본 하단 여백만 유지합니다. depth navigation처럼 root에서는 액션이 없고 다음 depth부터 버튼이 생기는 구성에서 Footer를 계속 마운트해 둘 때 사용합니다. 여백이 필요하면 빈 `ActionArea` 대신 명시적 spacer를 두세요.
- `ModalBody` 또는 같은 presentation frame을 사용하는 `FloatingWindowBody`의 마지막 직속 자식이 `ActionArea`이면 owner surface가 inline padding과 block-end padding을 `0`, desktop action group의 max width를 `none`으로 자동 보정합니다. 본문 padding과 중복되지 않도록 하기 위한 기본값이며, `ActionArea`에 명시한 `--oc-action-area-*` 변수는 이 보정보다 우선합니다. bottom placement의 safe-area 및 footer 예약은 Modal surface/body가 계속 소유합니다.

## FloatingWindow 계약

FloatingWindow는 anchor 기반 geometry와 dialog 시맨틱을 Modal 기본 surface presentation에 결합합니다. 기본 `FloatingWindowRoot`는 modeless이고 `ModalWindowRoot`는 modal focus 정책, 배경 입력을 차단하는 backdrop, Modal popup scale/fade motion을 적용합니다. ModalWindow backdrop은 기본적으로 렌더링하며 필요한 경우 `backdrop="hidden"`으로 시각적 dimmer만 숨길 수 있습니다.

| Layer/slot  | Selector                                                                   | 책임                                         |
| ----------- | -------------------------------------------------------------------------- | -------------------------------------------- |
| Backdrop    | `data-oc-component="floating-window-backdrop"` + `data-oc-part="backdrop"` | ModalWindow 기본 dimmer와 배경 pointer 차단  |
| Positioner  | `data-oc-component="floating-window-positioner"`                           | fixed positioning과 manager 주입 stack level |
| Window      | `data-oc-component="floating-window"` + `data-oc-part="window"`            | dialog 시맨틱과 anchor/free 좌표             |
| Drag handle | `data-oc-part="drag-handle"` + `data-oc-draggable="true\|false"`           | 명시적인 pointer drag 시작 영역              |
| Surface     | `data-oc-part="surface-motion"` + `data-oc-part="surface"`                 | Modal popup presentation과 `data-oc-resize`  |
| Header      | `data-oc-component="modal-header"`                                         | Modal 기본 header slot                       |
| Navigation  | `data-oc-component="modal-navigation-section"`                             | Modal 기본 disclosure navigation             |
| Body        | `data-oc-component="modal-body"`                                           | Modal 기본 scrollable body                   |
| Footer      | `data-oc-component="modal-footer"`                                         | Modal 기본 footer slot                       |
| Close       | `data-oc-component="modal-close"`                                          | dialog close action                          |

Window은 `data-oc-positioned="true\|false"`, `data-oc-window-positioning="anchored\|free"`, `data-oc-window-presentation="floating\|modal"`, `data-oc-window-modality="modeless\|trap-focus\|modal"`, `data-oc-side`, `data-oc-alignment`를 노출합니다. presentation은 사용한 Root 구성을 구분하는 호환 메타데이터이며 모든 presentation은 동일한 Modal popup surface motion을 사용합니다. modality는 focus와 background interaction 계약을 나타냅니다. pointer drag 중에는 `data-oc-window-dragging="true"`가 설정됩니다. `data-oc-side`는 collision flip 뒤 실제 side이고, `data-oc-alignment`는 alignment flip 뒤의 semantic alignment입니다. alignment shift는 같은 alignment 의도를 유지한 채 좌표만 boundary 안으로 보정합니다.

Drag handle 내부의 `button`, `a`, form control, `contenteditable` 요소는 drag를 시작하지 않습니다. 그 밖의 custom interactive target은 `data-oc-window-no-drag`로 같은 동작을 선언할 수 있습니다. 이 attribute는 drag gesture의 opt-out 계약이며 시각 selector가 아닙니다.

Manager는 일반 pointer와 focus intent를 window activation으로 해석하지만 `data-oc-part="close"` intent는 activation에서 제외합니다. 따라서 클릭 가능한 비활성 modeless window의 Close는 stack 순서를 바꾸지 않고 해당 위치에서 종료 모션을 실행합니다. modal backdrop과 inert 영역 뒤의 window는 이 예외와 관계없이 상호작용할 수 없습니다.

`FloatingWindowHeader`와 `FloatingWindowNavigationSection`은 surface 최상단을 소유하는 대체 관계의 chrome입니다. 기본 조합에서는 둘을 함께 렌더링하지 않습니다. `FloatingWindowNavigationSection`은 기본적으로 title을 dialog title로 등록하며, 별도의 custom title owner를 제공할 때만 `titleAsDialogTitle={false}`를 사용합니다.

modeless `FloatingWindowRoot`는 열릴 때 현재 focus를 유지합니다. `ModalWindowRoot`는 Modal과 같은 초기 focus resolver를 사용해 등록된 visible title을 우선하고, title이 없으면 첫 tabbable 요소로 fallback합니다. 이 정책은 DOM상 close button이 먼저 있다는 이유만으로 닫기 동작을 자동 강조하지 않기 위한 dialog interaction 계약입니다.

FloatingWindow surface 크기·radius·padding은 Modal의 `--oc-modal-*` popup 계약을 사용합니다. `resize="fixed"`는 Modal fixed surface와 같은 내부 body scroll contract를 활성화하며, surface의 `data-oc-scrollable`, Header의 `data-oc-sticky`, Footer의 `data-oc-background`를 실제 scroll state에 맞춰 갱신합니다. Footer의 ActionArea 조합은 Modal과 같은 direct-child selector 계약을 사용합니다.

`FloatingWindowManager` 안의 window는 고유한 `windowId`로 등록되며 다음 managed state를 노출합니다.

| Selector                            | 의미                                         |
| ----------------------------------- | -------------------------------------------- |
| `[data-oc-window-active='true']`    | 현재 manager stack의 활성 window             |
| `[data-oc-window-active='false']`   | 열려 있지만 비활성인 managed window          |
| `--oc-floating-window-stack-offset` | manager가 정규화한 backdrop/window 쌍 offset |

Manager는 modeless window보다 modal과 `trap-focus` window를 위에 유지합니다. 동일 modality tier에서는 open, pointer, focus 활성화 순서가 stack을 결정하며 Escape와 outside dismissal은 활성 window 하나에만 허용됩니다. Manager 밖에서는 `data-oc-window-active`를 출력하지 않습니다.

여러 focus-owning window가 열려도 Base UI modal/inert ownership은 활성 window 하나에만 부여됩니다. 비활성 modal과 `trap-focus` window는 요청 modality와 stack tier를 보존하지만 아래에 있는 동안 document inert를 중복 적용하지 않습니다.

| Variable                            | Default                                                                                                                    | Scope                         |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `--oc-floating-window-stack-offset` | shared overlay stack depth × 2; unmanaged `stackLevel` prop은 additive offset, Manager 안에서는 manager-owned depth가 우선 | backdrop/window stacking pair |

`--oc-floating-window-x`, `--oc-floating-window-y`는 geometry runtime이 소유하는 live 좌표이므로 consumer style override 대상으로 사용하지 않습니다.

## Modal 레이어 계약

다중 스냅 bottom sheet의 `data-oc-part="handle-area"`는 `data-oc-snap-action="cycle"`인 native button이며 내부 `handle`은 span입니다. 단일 스냅·일반 드래그 핸들은 기존 장식 요소를 유지합니다. 태그명 대신 part selector를 사용하세요. 버튼 이름은 다음 동작에 따라 `모달 펼치기` 또는 `모달 축소하기`이며 키보드 회피 중에는 `aria-disabled="true"`와 `tabIndex="-1"`로 스냅 순환을 막습니다. 드래그가 허용된 핸들은 포인터 입력을 유지해 실제 드래그 시 키보드를 닫을 수 있고, 드래그도 허용되지 않으면 native `disabled`를 함께 사용합니다.

`ModalContent position`은 스냅이 비활성인 Popup의 viewport에 `data-oc-modal-position="center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right"`를 노출합니다. 기본값은 `center`이고 `left`/`right`는 RTL에서도 물리 방향입니다. viewport 여백 안에서 정렬하며 `bottom`/`full` 배치와 활성 드래그 스냅에는 이 attribute를 내보내지 않습니다. 활성 스냅의 배치는 기존 `snapPositions`/`defaultSnapPosition`이 소유합니다. 이 attribute는 viewport 소유이므로 content rest props로 override할 수 없습니다.

Modal은 transition motion과 시각 clipping을 분리합니다.

| Layer          | Selector                                                                                                    | 책임                                                                                                       |
| -------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Backdrop       | `data-oc-component="modal-backdrop"` + `data-oc-part="backdrop"`                                            | dimmer 표시와 backdrop press dismissal                                                                     |
| Motion layer   | `data-oc-component="modal-content"` + `data-oc-part="surface-motion"`                                       | dialog 시맨틱, Base UI transition 상태, opacity/transform/drag translate                                   |
| Visual surface | `data-oc-part="surface"`                                                                                    | background, radius, overflow clipping, layout, header/body/footer slot, `data-oc-scrollable="true\|false"` |
| Bottom fill    | `data-oc-component="modal-bottom-fill"` + `data-oc-part="bottom-fill"`                                      | bottom sheet overshoot fill                                                                                |
| Header         | `data-oc-component="modal-header"` + `data-oc-part="header"`                                                | 타이틀 영역, optional drag handle, `header-bottom` 보조 chrome slot                                        |
| Navigation     | `data-oc-component="modal-navigation-section"`                                                              | 고정 chrome row + 확장 panel                                                                               |
| Body           | `data-oc-component="modal-body"` + `data-oc-part="body"`                                                    | scroll area wrapper                                                                                        |
| Body content   | `data-oc-component="modal-body-content"` + `data-oc-part="body-content"` + `data-oc-layout="content\|fill"` | padded content 영역                                                                                        |
| Footer         | `data-oc-component="modal-footer"` + `data-oc-part="footer"`                                                | 고정 overlay action/footer slot                                                                            |

시각 커스터마이즈는 `data-oc-part="surface"` 또는 component contract 변수를 대상으로 하세요. `surface-motion`은 transition/placement/drag 동작을 의도적으로 바꿀 때만 대상입니다.

Modal의 기본 `initialFocus="auto"`는 `ModalHeader`의 visible title을 `tabIndex={-1}`인 정적 문맥 시작점으로 사용합니다. title이 없으면 첫 tabbable 요소로 fallback합니다. `first-interactive`, `title`, ref/function으로 업무 의미에 맞는 대상을 명시할 수 있으며, 되돌리기 어려운 확인 dialog는 가장 덜 파괴적인 action을 지정하는 것이 권장됩니다. title focus는 일반 Tab 순서에 추가되지 않으며 첫 Tab부터 기존 control 순환이 시작됩니다.

- `ModalContent`의 `className`은 motion layer, visual surface, bottom fill 세 레이어에 적용됩니다 — 공개 `--oc-modal-*` 변수 override가 layout 계산까지 도달하게 하기 위함입니다. `style`은 visual surface에 적용되며, `style`의 CSS custom property는 motion/bottom fill 레이어로도 전달됩니다.
- `ModalHeader bottomContent`는 기본 header row 아래 `data-oc-part="header-bottom"`에 보조 chrome을 렌더링합니다. Modal은 이 slot을 header 높이에 포함하고 `--oc-modal-container-padding-block-start`를 현재 size의 `--oc-modal-container-padding-block`으로 자동 설정해 body와 scroll 시작 간격을 예약합니다. 명시적인 `ModalContent style` 변수는 이 기본값을 override할 수 있습니다.
- `ModalHeader bottomContent`의 직계 `ProgressIndicator`는 edge-attached navigation chrome으로 취급해 `--oc-progress-indicator-radius: 0`을 자동 적용합니다. 별도 wrapper 없이 slot에 직접 배치하는 구성이 canonical usage입니다.
- `ModalHeader bottomContent`의 `TabsList padding`은 Modal header 좌우 여백을 list 좌우 여백으로 사용해 탭 label 시작선을 header 콘텐츠 시작선에 맞춥니다(`leadingContent`가 있어도 title이 아닌 header 여백 기준). `padding=false` list는 slot 가장자리까지 붙는 full-bleed 배치를 유지합니다.
- `ModalHeader handle`은 `data-oc-part="handle-area"` 안에 32×4px bar `data-oc-part="handle"`과 Interaction을 렌더링합니다. drag가 활성화되면(`drag='handle'` 또는 `drag='content'`) handle-area는 `data-oc-draggable="true"`가 되어 두 모드에서 공통 pointer hit 영역이 됩니다. fine pointer에서는 surface 좌우 끝까지 닿는 16px 상단 strip, coarse pointer에서는 중앙 72×44px pad입니다. fine pointer의 full-width strip은 header control과 겹치지 않도록 header content의 상단 padding을 strip 높이(16px) 이상으로 보정하며, coarse pointer의 중앙 pad에는 이 보정을 적용하지 않습니다. hover 가능 기기에서 strip에 hover하면 bar가 제자리에서 1.1배(35.2×4.4px)로 커지고 Interaction hover가 켜지며, 누르고 있는 동안에는 drag hook이 handle-area에 `data-oc-state="pressed"`를 내보내 확대, Interaction active, `grabbing` cursor를 release까지 유지합니다. release 시 pointer가 area 위에 남아 있으면 hook이 `data-oc-state="hover"`를 내보내 브라우저의 `:hover` 재평가가 늦어지는 동안(WebKit) 확대 상태를 이어 주고, pointer가 area를 실제로 벗어나면 지웁니다. 확대는 `expand` intent의 smooth spring이며 reduced-motion에서는 scale 없이 tint만 남습니다. `drag='content'`는 handle-area와 header 전체를 같은 chrome drag arbitration으로 처리하고, `drag='handle'`는 handle-area에서만 drag를 시작합니다. 시각 커스터마이즈는 bar 요소(`background-color`, 크기)를 대상으로 하세요. `ModalNavigationSection`의 handle은 navigation row 전체가 drag 영역이므로 handle-area 없이 bar만 렌더링합니다.
- Modal의 커스텀 본문 스크롤바는 Footer/ActionArea 배경 위에 표시됩니다. 스크롤바 조작 영역은 키보드로 올라온 Footer를 포함해 버튼 영역 위에서 끝납니다. 본문 콘텐츠는 Footer 아래에 유지되며, 일반 ScrollArea의 독립 레이어 계약은 유지됩니다.
- bottom 모달의 마우스·펜 드래그는 핸들이 렌더링된 Header/Navigation chrome에서만 시작합니다. `handle={false}`는 해당 chrome의 마우스·펜 드래그도 막습니다. Body/Footer의 텍스트 선택은 시트 드래그로 전환하지 않으며 터치의 본문 스크롤·드래그 판정은 유지합니다. 자세한 입력 정책과 닫힘 기준은 [gesture 문서](./gesture.md)를 참고하세요.
- `ModalHeader`의 자체 영역(title, 빈 공간, handle-area)을 탭하면 body가 sheet 모션 spring으로 맨 위로 스크롤됩니다. 단, 키보드 세션이 열린 동안에는 본문 스크롤 위치를 유지한 채 키보드만 닫습니다. 키보드가 닫힌 뒤 다시 탭하면 기존처럼 맨 위로 이동합니다. iOS status bar 탭이 페이지를 맨 위로 보내는 것과 같은 보조 제스처로, 모달이 페이지 스크롤을 잠근 동안 그 역할을 이어받습니다. 제외 기준은 drag 시작을 막는 기준과 같습니다. 닫기·뒤로 가기 버튼, `trailingContent`/`bottomContent` 안의 컨트롤(button, link, input, `role="button"`·`option`·`radio` 등)과 `data-oc-modal-no-drag`로 표시한 콘텐츠(하위 요소 포함)에서 온 클릭은 제외되고, 소비자 `onClick`이 `preventDefault`하면 건너뜁니다. drag 뒤에 따라오는 click은 drag hook이 취소하므로 동작하지 않으며, body가 이미 맨 위면 아무 일도 하지 않습니다. reduced-motion에서는 즉시 이동합니다. 접근성 트리에는 아무 것도 추가하지 않습니다(header는 여전히 role 없는 chrome이며 스크롤 자체가 기본 수단입니다).
- 키보드 세션 중 drag 시작 제외 기준(`data-oc-modal-no-drag`, 링크·버튼·form control·`contenteditable`·ARIA role control)에서 시작한 세로 경계 touch는 시트 drag를 시작하지 않지만, 임계값 이후 visual viewport pan을 막기 위해 `preventDefault`될 수 있습니다. 이 제외 대상에서는 가로 우세 제스처를 네이티브 동작으로 유지하고, 일반 본문은 실제 가로 스크롤 조상이 있을 때 가로 우세 제스처를 네이티브 동작으로 유지합니다.
- `data-starting-style`/`data-ending-style`은 Base UI가 motion layer에 방출합니다.
- `data-oc-modal-backdrop="hidden"`으로 정착하면 backdrop 레이어는 렌더링되지 않습니다. 전환 중 잠시 남는 exiting backdrop 요소에 layout/hit-testing/커스터마이즈를 의존하지 마세요.
- 다른 Modal 안에 React 트리로 중첩해 렌더링한 Modal·Alert도 형제로 배치한 경우와 같이 자기 backdrop을 렌더링합니다. 부모의 backdrop 설정과 무관하게 중첩 dialog의 backdrop이 부모 surface 위를 dim하며, 부모 backdrop이 보이는 경우 dim이 겹칩니다. backdrop press와 Escape는 가장 위 dialog만 닫습니다. 닫히는 dialog는 퇴장 전환이 끝나 unmount될 때까지 쌓임 순서를 유지하므로 backdrop이 아래 Modal 뒤로 내려가지 않습니다.
- `backdropFrom` snap 임계 아래에 정착한 bottom sheet는 backdrop 레이어에 `data-oc-modal-backdrop-passthrough="true"`를 방출합니다. 이 상태는 non-modal이며 배경이 상호작용 가능합니다. attribute는 드래그 프레임 단위가 아니라 정착(rest) 시점에 바뀝니다.
- modal elevation shadow와 backdrop dim은 하나의 깊이 신호의 보완 관계입니다(매 프레임 `shadow = 1 − dim`). dim 상태 정착은 shadow 없음, undimmed/hidden-backdrop 정착은 full shadow이며, `backdropFrom` 경계를 지나는 이동은 둘을 crossfade합니다.
- visual surface의 `data-oc-scrollable`은 footer 예약 스타일링용입니다. `ActionArea` footer가 있을 때 `true`면 footer action area의 block-start padding이 body content에 예약되고, `false`면 standalone action area padding 모델이 유지되고 footer 높이만 예약됩니다.
- bottom placement에서 motion layer가 sheet geometry를 소유합니다: `--oc-modal-sheet-height`가 높이를 제어할 때 `data-oc-modal-sheet-sizing="managed"`, 높이 변화가 sheet motion 계약으로 전환되어야 할 때만 `data-oc-modal-sheet-transition="auto"`, snap-point 동작 중 `data-oc-modal-snapping="true"`.
- `resize="fixed"`는 `--oc-modal-fixed-block-size`를 요청 높이로 사용하고 `--oc-modal-fixed-max-block-size`(기본 `--oc-modal-max-height`)로 clamp합니다. popup placement는 `--oc-modal-popup-fixed-max-block-size`로 별도 clamp됩니다.
- `ModalBody layout="fill"`은 header/footer/content padding 예약을 유지한 채 body content slot을 iframe·map·viewer 같은 embedded 표면용 fill container로 바꿉니다.
- size별 공개 변수: `--oc-modal-small-width`/`--oc-modal-medium-width`/`--oc-modal-large-width`와 `--oc-modal-{size}-padding-*`이 내부 `--oc-modal-container-padding-*` 계약으로 매핑됩니다. header/body/footer padding은 기본적으로 container padding을 상속하며, `--oc-modal-header-padding-*`/`--oc-modal-footer-padding-*`로 slot별 예외를 둘 수 있습니다. edge 정렬 chrome control용으로 `--oc-modal-header-edge-offset-inline`이 있습니다.
- 닫기 시도가 차단되어 피드백 중일 때 motion layer에 `data-oc-modal-feedback="reject"`가 방출됩니다. transform을 직접 덮지 말고 `--oc-modal-reject-distance` 같은 contract 변수를 사용하세요.
- bottom placement에서 소프트 키보드 회피 세션이 활성일 때 viewport 레이어(`data-oc-part="viewport"`)에 `data-oc-modal-keyboard-avoiding="true"`가, 포커스가 떠나 키보드가 닫히는 동안에는 `data-oc-modal-keyboard-closing="true"`가 함께 방출됩니다. `keyboardDismiss="none"`으로 키보드를 유지한 채 배경 문서를 스크롤할 수 있는 non-modal 표시에서 세션 동안 viewport 레이어를 문서에 앵커해 root scroll timeline으로 위치를 유지할 수 있는 환경(scroll-driven animation 지원, 변형되지 않은 portal 조상)에서는 `data-oc-modal-scroll-anchor="true"`도 방출됩니다. 기본 정책(`background-scroll`)은 세션 동안 페이지를 움직이지 않으므로 앵커를 쓰지 않습니다. iOS Safari는 앵커된 레이어가 스크롤 위치만큼 이동한 동안 그 안의 `backdrop-filter` 요소(예: `BackgroundIconButton`의 `normal` appearance)에 backdrop을 공급하지 않아 단색으로 보이므로, `none`을 쓰는 화면은 이 제약을 감안하세요. 셋 모두 키보드 geometry 추격의 게이트로 쓰이는 내부 지향 상태이며, 소비자는 이 attribute의 방출 타이밍(세션 시작/해제 시점의 지연 포함)과 그 동안의 `position` 값에 의존하지 않는 것이 좋습니다.
- 콘텐츠 높이를 따르는 bottom Modal은 키보드 예약 높이만큼 확장하되 상단 여백을 반영한 최대 높이와 snap 상한을 넘지 않습니다. `drag="content"` 또는 `drag="handle"`에서 실제 시트 드래그가 시작되면 입력 포커스를 해제해 키보드를 닫으며, 드래그가 끝날 때까지 예약 여백을 유지합니다. 작은 터치와 모달 본문 스크롤만으로는 입력 포커스를 해제하지 않습니다. 배경 문서가 스크롤 가능한 non-modal 표시(`modal={false}` 또는 `'trap-focus'`)에서는 surface 밖에서 시작한 터치가 드래그로 이어지면 입력 포커스를 해제해 키보드를 닫고, 그 제스처는 페이지를 스크롤하지 않습니다(`keyboardDismiss="background-scroll"` 기본값). 페이지 스크롤은 키보드가 완전히 닫혀 복원된 viewport가 보고된 뒤 다음 제스처부터 동작합니다. iOS는 페이지 스크롤 중 키보드를 유지하고 fixed surface를 layout viewport 기준으로 함께 밀어내며 키보드가 내려가는 중의 스크롤에서는 fixed 레이어 배치를 멈추므로, 키보드를 유지해야 하면 `keyboardDismiss="none"`을 지정하고 소비자 앱 viewport meta에 `interactive-widget=resizes-content`를 추가하세요.
- 키보드 확장은 스냅의 표시 높이에만 적용됩니다. `snapPoint`와 `onSnapPointChange`는 기존 스냅 값을 유지하며, 확장·복원만으로 변경 이벤트를 발생시키지 않습니다.
- 입력 포커스를 해제해 키보드를 닫으면 모달 높이 복원을 함께 시작합니다. 본문의 스크롤 보호용 여백은 뷰포트 복구와 모달 및 스크롤 애니메이션이 완료될 때까지 유지합니다. 키보드를 닫기 위해 시작한 드래그에서는 높이 복원을 드래그 종료까지 미룹니다.
- `ModalNavigationSection`은 body content가 아니라 Modal chrome입니다. 루트는 `data-oc-state="open|closed"`, toggle row는 `data-oc-component="modal-navigation"` + `data-oc-part="navigation"` + `aria-expanded`/`aria-controls`, indicator는 `data-oc-part="indicator"`/`indicator-frame`, panel은 `data-oc-component="modal-navigation-panel"`(collapsed 동안 `aria-hidden`/`inert`), panel content는 `data-oc-part="panel-inner"`를 노출합니다.

## ScrollArea scrollbar 계약

`ScrollArea`는 현재 scrollbar 렌더링 경로를 `data-oc-scrollbar-mode="custom | native"`, 공개 표시 정책을 `data-oc-scrollbar-visibility="auto | hidden | hidden-on-touch"`로 제공합니다. `native` 모드에서는 운영체제 scrollbar와 네이티브 스크롤 물리를 유지합니다. `hidden`은 모든 환경에서, `hidden-on-touch`는 터치 환경에서만 스크롤 가능성을 유지한 채 시각적 indicator를 숨깁니다.

## Provider 소유 예외

다음 non-`data-oc-*` attribute는 컴포넌트 소유 계약이 아니라 외부 provider/foundation 소유입니다.

| Attribute             | Owner                          | 정책                                   |
| --------------------- | ------------------------------ | -------------------------------------- |
| `data-side`           | Base UI / positioning provider | provider 상태 스타일링에 사용 가능     |
| `data-starting-style` | Base UI / transition provider  | transition 상태 스타일링에 사용 가능   |
| `data-ending-style`   | Base UI / transition provider  | transition 상태 스타일링에 사용 가능   |
| `data-theme`          | Foundation theme 계약          | 테마 선택용. 컴포넌트 selector 계약 밖 |

## Legacy 호환 출력

아래 attribute는 구형 consumer 호환용으로만 방출됩니다. 새 스타일링에 사용하지 마세요. 제거는 major 릴리스에서만 진행됩니다([Stability Policy](./stability.md) 참조).

| Component                            | Legacy attribute        | Replacement                |
| ------------------------------------ | ----------------------- | -------------------------- |
| `SectionHeader`                      | `data-size`             | `data-oc-size`             |
| `SectionHeader`                      | `data-viewport`         | `data-oc-viewport`         |
| `TextInput`                          | `data-invalid`          | `data-oc-invalid`          |
| `TextInput`                          | `data-interactive`      | `data-oc-interactive`      |
| `Select`                             | `data-invalid`          | `data-oc-invalid`          |
| `Checkbox`                           | `data-indeterminate`    | `data-oc-indeterminate`    |
| `ScrollArea`                         | `data-root`             | `data-oc-scroll-root`      |
| `ScrollArea`                         | `data-scroll`           | `data-oc-scroll`           |
| `ScrollArea`                         | `data-scroll-label`     | `data-oc-scroll-label`     |
| `ScrollAreaContainer`                | `data-scroll-container` | `data-oc-scroll-container` |
| `ScrollArea` scrollbar               | `data-orientation`      | `data-oc-orientation`      |
| `ScrollArea` scrollbar, track, thumb | `data-state`            | `data-oc-state`            |

## InputGroup

- 루트: 네이티브 fieldset에 `data-oc-component="input-group"`.
- 상태: `data-oc-variant="outlined|underlined"`, `data-oc-label-placement="outside|floating"`, 오류 시 `data-oc-invalid="true"`.
- 직접 슬롯: `data-oc-part="label"` (legend), `surface` (입력 행), `description`, `error`.
- 입력: `data-oc-component="input-group-input"`, `data-oc-part="input"`. 실제 placeholder가 있으면 `data-oc-has-placeholder="true"`.
- 장식 구분자: `data-oc-part="separator"`, 고정 `aria-hidden="true"`.
- 각 입력은 독립적인 접근 가능한 이름, 네이티브 ref/name/value와 병합된 설명 ID를 갖습니다. 그룹 상태 상속은 네이티브 Tab 순서를 바꾸지 않습니다.
- 하나의 surface 안에서 InputGroup 중첩은 지원하지 않습니다.
