# API Naming Conventions

`@orioncactuscorp/ui`의 공개 API는 이름에서 역할과 사용 형태를 예측할 수
있도록 설계합니다. 유틸과 React API는 실행·소유권·수명에 따라 이름을
선택하고, 컴포넌트의 시각적 선택지는 `variant`, `appearance`, `size`로
일관되게 표현합니다.

## 유틸과 React API 네이밍

이름은 API의 역할과 사용 형태를 드러내도록 선택합니다. 먼저 React와의 결합,
객체의 소유권과 수명, 실행하는 작업을 판단하고 가장 가까운 기존 공개 API를
참고합니다. `oc`는 oc-ui 소속인 모든 함수에 붙이는 필수 접두사가 아닙니다.

| API 형태            | 이름 기준                                                                           | 기존 예시                                                        |
| ------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| React 훅            | React hook 규칙과 state·ref·effect 수명에 연결되는 기능은 `use*`                    | `useDragScroll`, `useScrollSnapIndex`, `usePrefersReducedMotion` |
| 실행·제어 객체 생성 | 소비자가 보관하고 제어하는 객체를 생성하면 `create*`                                | `createSpringAnimator`, `createFlingAnimator`                    |
| 단일 작업·조합·변환 | 실행하는 작업을 구체적인 동작명으로 표현                                            | `mergeRefs`                                                      |
| 도메인 진입점       | 하나의 모델·정책·용어 체계를 공유하는 연산을 함께 제공할 실익이 있으면 `oc<Domain>` | `ocSpring`, `ocMotion`, `ocGesture`, `ocFocus`                   |

React 앱에서 호출한다는 이유만으로 일반 함수를 훅으로 만들지 않습니다.
`useDragScroll`은 실제 스크롤 요소에 ref를 연결하고 입력 리스너와 진행 중인
동작을 React 수명에 맞게 관리하므로 훅입니다. `useOcDragScroll`처럼 소속을
중복 표시하는 접두사는 기존 훅 컨벤션에서 사용하지 않습니다.

### `oc` 도메인 진입점의 판단 기준

- 관련 연산이 공통 모델·정책·용어 체계를 공유하고, 함께 탐색하고 사용하는 것이 소비자에게 도움이 되는지 확인합니다.
- 같은 책임을 가진 기존 진입점이 있으면 새 namespace보다 기존 API 확장이 적합한지 먼저 확인합니다.
- 이름이 실제 제공 기능보다 넓은 책임을 암시하지 않아야 합니다. oc-ui 소속, 내부 토큰·spring 사용, 함수 개수, 가상의 미래 확장만으로 namespace를 만들지 않습니다.
- `oc`는 순수 계산이나 무상태를 보장하지 않습니다. DOM 작업이나 상태를 가진 객체 생성도 해당 도메인의 역할에 포함될 수 있습니다.

| 기존 진입점 | 현재 역할                                                |
| ----------- | -------------------------------------------------------- |
| `ocSpring`  | spring 모델·프리셋·평가와 관련 animator 생성             |
| `ocMotion`  | intent·phase·pace·reduced-motion을 포함한 모션 정책 해석 |
| `ocGesture` | 저항·감속·투사 계산과 속도 추적 도구                     |
| `ocFocus`   | 포커스 가능 여부 등을 고려한 관련 DOM 포커스 작업        |

이 기준은 현재 공개 API를 설명하고 신규 API를 판단하기 위한 기준입니다.
모든 기존 이름이 처음부터 이 기준으로 설계되었다고 가정하거나, 분류를 맞추기
위해 공개 이름을 일괄 변경하지 않습니다. `ocSpring.createAnimator`와
`createSpringAnimator`처럼 이미 제공하는 진입점은 호환성을 유지합니다.
신규 API에 같은 기능의 namespace와 named export를 함께 제공할 때는 구체적인
소비자 필요가 있어야 합니다.

이름을 제안할 때는 API 형태, 가장 가까운 기존 선례, 이름이 표현하는 책임을
함께 설명합니다. 형태가 모호하면 먼저 객체의 소유권과 수명을 정합니다.
`ocGesture`의 계산 도구와 `ocMotion`의 정책 해석을 사용하더라도 입력 이벤트와
컴포넌트 owner 정책은 호출부의 책임으로 유지합니다.

## 역할은 계약, 시각 값은 기본값

prop 이름과 TypeScript에 선언된 선택지는 버전 관리되는 계약입니다. 반면 각
선택지가 사용하는 색, 간격, 크기 같은 구체적인 시각 값은 디자인 시스템의
기본값입니다. 지원되는 token, component contract 변수, selector 계약을 통해
consumer가 조정할 수 있으며, 디자인 시스템 정책에 따라 변경될 수 있습니다.

지원되는 선택지의 정확한 목록은 각 컴포넌트의 TypeScript 선언을 기준으로
확인하세요. 모든 컴포넌트가 아래 축을 전부 제공하지는 않습니다.

## `variant`: 하나의 시각·표현 축

컴포넌트에 열거형 시각 또는 표현 축이 하나라면 `variant`를 사용합니다.
`variant`가 나타내는 구체적인 역할은 컴포넌트에 따라 의미·색, 강조 강도,
형태, surface 표현, layout 표현, slot content 종류 등이 될 수 있습니다.

상태, 간격, 동작처럼 독립된 개념은 `variant`에 합치지 않고 별도 prop으로
표현합니다.

`AnimatedNumber`의 `variant`는 바뀐 자리를 바꾸는 표현(`none | slide | blur | roll`,
기본 `slide`) 하나입니다. 방향(`trend`), 속도(`pace`), 값 공개 회전 수(`spins`)는
동작 계약이라 별도 prop으로 둡니다. 글자 크기와 굵기는 감싸는 요소의 typography를
상속하므로 `size` prop은 없습니다.

## `appearance` + `variant`: 독립된 두 축

형태 또는 surface와 의미 또는 색을 각각 선택해야 하는 컴포넌트는 두 축을
분리합니다.

- `appearance`: 형태 또는 surface 처리
- `variant`: 의미 또는 색 의도

`Button`이 대표적인 예입니다. `appearance`는 `solid | outlined`, `variant`는
`default | primary | secondary | assistive`를 제공합니다.

```tsx
<Button appearance='solid' variant='primary'>
  저장
</Button>

<Button appearance='outlined' variant='assistive'>
  도움말
</Button>
```

이 분리는 형태와 의미를 하나의 조합 이름으로 합치지 않고 각 역할을 독립적으로
읽고 선택할 수 있게 합니다.

`ScrollAreaEdge`도 같은 구조입니다. `variant`는 가장자리 처리(`fade |
progressive-blur`), `appearance`는 그 처리가 콘텐츠와 만나는 형태(`soft | hard`,
Apple의 scroll edge effect style)입니다.

## 다른 컴포넌트의 축을 넘기는 prop

owner가 내부에 렌더링하는 컴포넌트의 축을 소비자에게 열 때는 새 축을 만들지
않고, 원래 컴포넌트의 타입을 그대로 받아 `{컴포넌트}Variant` 이름으로 넘깁니다.
렌더링하지 않는 선택지가 필요하면 원래 타입에 `'none'`을 더합니다.

```tsx
<ModalHeader
  backgroundVariant='light' // BackgroundVariant | 'none'
  scrollEdgeVariant='progressive-blur' // ScrollAreaEdgeVariant | 'none'
  scrollEdgeAppearance='hard' // ScrollAreaEdgeAppearance
  divider='normal' // DividerVariant | 'none'
/>
```

`ActionAreaLayout.Footer`의 `scrollEdgeVariant`(`ScrollAreaEdgeVariant | 'none' | 'auto'`, `auto`는 owner의 스크롤 방식에 따라 해석)와
`scrollEdgeAppearance`, `divider`(`DividerVariant | 'none'`), `interactionVariant`(Accordion, Cell)와 Cell `divider`도 같은 규칙입니다. 이
prop은 원래 컴포넌트의 값 순서와 의미를 따르므로, 원래 컴포넌트에 값이 추가되면
owner에서도 같은 값을 받습니다.

## TextInput 형태와 Field 라벨 배치

`TextInput`과 `TextArea`는 `variant='outlined | underlined'`로 입력 영역의 형태를 선택합니다.
기본값은 기존 사방 테두리형인 `outlined`입니다. 라벨 배치는 독립된 계약이므로
`Field`의 `labelPlacement='outside | floating'`으로 선택하며 기본값은 `floating`이며, 지원되는 underlined 입력에만 적용됩니다. `labelPlacement="outside"`를 명시하면 외부 라벨을 유지합니다.

여러 줄 입력에는 `TextArea variant='underlined' minRows={2} maxRows={5}`를 사용합니다.
밑줄형에서도 줄바꿈, 자동 높이 조절, supporting content를 유지합니다. `TextArea`도 밑줄형이면 floating 라벨이 기본입니다. 빈 라벨은 첫 줄에 정렬되며, 여러 줄 입력으로 높이가 늘어나도 올라간 라벨은 상단에 고정됩니다.

```tsx
<Field labelPlacement='floating'>
  <FieldLabel>생년월일 6자리</FieldLabel>
  <FieldControl>
    <TextInput variant='underlined' inputMode='numeric' maxLength={6} />
  </FieldControl>
  <FieldMessage>생년월일을 6자리로 입력하세요.</FieldMessage>
</Field>
```

floating은 세로 Field의 직접 자식 FieldLabel과 FieldControl, 그 안의 직접 자식
underlined TextInput 또는 TextArea 조합에서 지원합니다. 지원 타입은 text/email/password/search/tel/url입니다.
가로 Field, outlined 입력, 날짜·숫자 입력, 다른 컨트롤은 기존 외부 라벨 배치를 유지합니다.
숫자 문자열은 `type='text'`와 `inputMode='numeric'`를 조합하세요.

라벨은 비어 있을 때 입력 위치에 머물고, 값이 있으면 위로 이동하며 0.75배로 축소됩니다.
placeholder가 없으면 포커스만으로 이동하지 않습니다. placeholder가 있으면 빈 입력도 포커스 시 라벨이 올라가고 안내가 fade로 나타납니다. 빈 상태에서 blur하면 안내를 숨기고 라벨을 내립니다. 값이 있으면 blur 후에도 작은 라벨을 유지합니다.
floating 라벨은 medium 굵기입니다. 모든 FieldLabel은 기본 `label-alternative`, 입력 영역에 포커스가 있을 때 primary, invalid일 때 오류 색상을 공통으로 사용합니다. invalid 색상은 포커스보다 우선하며, 값이 있어도 포커스가 없으면 기본 색상입니다. 밑줄형의 stroke 끝은 max radius로 둥글게 표시합니다.
placeholder는 실제 input에 전달하며, 생략한 경우에만 내부 빈 값 판별용 공백을 사용합니다. 추가 설명은 FieldMessage에 작성하세요.
라벨은 한 줄로 표시하고 긴 내용은 말줄임 처리하며, 전체 내용은 접근성 이름으로 유지합니다.
reduced-motion에서는 이동·축소 전환을 제거하고 색상 피드백을 유지합니다.

## `size`: 표준 크기 순서의 부분집합

named size의 표준 순서는 다음과 같습니다.

```text
xsmall < small < medium < large
```

각 컴포넌트는 실제로 지원하는 크기만 부분집합으로 제공합니다. 예를 들어 어떤
컴포넌트가 `small | medium`만 제공하더라도 사용하지 않는 `xsmall`이나 `large`를
대칭성을 위해 추가하지 않습니다. TypeScript union의 선언 순서는 별도 계약이
아니며, 크기 비교에는 위 순서를 사용합니다.

### Named size 밖의 예외

named preset이 아니라 외부 문맥이나 직접 지정한 치수로 크기를 정해야 할 때만
다음과 같은 값을 허용합니다.

- `inherit`: owner의 typography·크기를 상속
- `custom`: component CSS가 치수를 결정
- 숫자 또는 자유 형식 값: primitive나 icon-sized control에 직접 치수를 전달

`TextButton`은 owner 안에서 자연스럽게 조합할 수 있도록 `size='inherit'`을
제공합니다. 색을 상속하는 `variant='inherit'`은 size가 아닌 variant 축의
선택지입니다.

```tsx
<TextButton variant='inherit' size='inherit'>
  자세히 보기
</TextButton>
```

새 named size가 필요할 때 `tiny`, `mini` 같은 별도 척도를 만들지 않습니다.
직접·custom·상속 크기가 필요한 경우에는 해당 예외의 목적을 컴포넌트 API에
문서화합니다.

`Badge`의 작은 preset도 `size='xsmall'`을 사용하므로 모든 named size가 이
표준 순서를 따릅니다.

`Badge`의 `variant='default'`는 소비자 커스터마이즈의 기준이 되는 canonical
기본 표현이며 `label-strong` 배경과 `background-normal-normal` 라벨 색을 사용합니다.
기존 `variant='normal'`은 `label-alternative` 라벨과 `fill-normal` 배경을 사용하는
레거시 표현으로 유지됩니다. 신규 코드는 `default`를 사용하세요.

`ProgressIndicator`는 선의 block size를 `small | medium | large`로 제공합니다.
기본 `small`은 모바일과 navigation chrome에 사용하고, `medium`과 `large`는
태블릿·데스크톱의 독립적인 작업 진행 표시에 사용합니다.

`PageIndicator`는 `variant`·`size`를 두지 않습니다. 점 색은
`--oc-page-indicator-color`(기준색)와 `--oc-page-indicator-active-color`로 조정하며,
사진 위 배치처럼 비활성 비율과 대비 처리가 함께 달라지는 표현이 필요해지면
`variant`로 추가합니다.

`SegmentedControlRoot`는 `small | medium | large`를 제공하며 기본값은 `medium`입니다.
컨트롤 전체 높이는 같은 size의 `Button`과 같아서 한 줄에 함께 둘 수 있습니다.
iOS형 solid 표현 하나만 제공하므로 `appearance`·`variant` prop은 없습니다.

`Slider`는 `small | medium`을 제공하며 기본값은 `medium`입니다.
`Switch`와 같은 size subset이고, size는 thumb 지름과 포인터 영역 높이를 바꿉니다.
흰 손잡이 표현 하나만 제공하므로 `appearance`·`variant` prop은 없습니다.

## Carousel

`transition`은 `slide | fade`이며 기본값은 `slide`입니다. 같은 레이아웃, indicator, 컨트롤을 공유하고 슬라이드 교체 방식만 다르므로 시각 축인 `variant`가 아니라 동작 축으로 둡니다. 크기는 슬라이드 콘텐츠가 정하므로 `size`는 없습니다.

선택은 값이 아니라 위치라서 `value` 대신 `index`(제어형), `defaultIndex`(비제어형), `onIndexChange`를 씁니다. `index`가 `undefined`가 아니면 제어형이고, 이후 제어형과 비제어형을 오가면 개발 모드에서 경고합니다. 범위를 벗어난 값은 양 끝으로 맞추고 유한한 숫자가 아니면 0으로 보며, 이렇게 맞춘 값은 `onIndexChange`로 알리지 않습니다.

- 버튼, indicator, 방향키, 자동 넘김은 `onIndexChange`로 제안만 하고 `index`가 바뀌어야 움직입니다. 부모가 받아들이지 않으면 아무것도 움직이지 않습니다.
- slide 끌기·스와이프·휠 스크롤과 fade 끌기는 화면이 손을 따라가므로, 놓는 순간 표시와 진행 막대가 먼저 바뀌고 착지할 슬라이드를 제안합니다. 스크롤이 정착하거나 디졸브가 끝났을 때 `index`와 다르면 `move` 모션(fade는 디졸브)으로 `index`에 돌아갑니다. loop는 멈춘 자리에서 가까운 쪽으로 돌아갑니다.
- 누르고 있는 동안 바뀐 `index`는 손을 뗀 뒤 반영합니다.
- 거절은 다음 렌더에 응답이 없는지로 추측하지 않습니다. `onIndexChange` 안에서 상태를 바로 갱신하세요. 정착보다 늦게 응답하는 부모에서는 거절된 것처럼 돌아갔다가 응답이 오면 다시 이동합니다.
- `onIndexChange`는 현재 `index`와 같은 값으로 부르지 않습니다. 끌기 한 번에 놓는 순간 한 번, 관성으로 다른 슬라이드에 정착했을 때 한 번 더 불러 최대 두 번이며, 같은 끌기 안에서 같은 값을 두 번 보내지 않습니다. 휠·키보드 스크롤은 놓는 순간이 없으므로 정착할 때 한 번 부릅니다. 놓는 순간의 예측이 빗나가 현재 `index`로 정착하면 정정 호출이 없으므로, 늦게 응답하는 부모는 받은 예측값을 적용할 수 있습니다.
- 보고, 복귀 예약, 방향키 포커스 요청은 입력 단위로 관리합니다. 새로 누르거나 스크롤을 시작하면 이전 끌기의 중복 판정과 예약된 복귀가 사라지고, 버튼·방향키 제안은 받아들여지거나 다른 제안·`index`로 바뀔 때까지 의도와 포커스 요청을 유지합니다. 늦게 받아들여진 방향키 제안은 포커스가 그사이 indicator를 떠나지 않았을 때만 그 탭으로 옮깁니다.
- 바깥에서 바꾼 `index`의 진행 막대 방향과 loop 이동 방향은, 그 값이 직전에 제안한 값이면 제안할 때의 의도(loop의 마지막→처음 다음은 앞으로)를 따릅니다. 아니면 loop는 가까운 쪽(같으면 앞으로), loop가 아니면 인덱스 순서입니다.
- 제어형 자동 넘김이 제안한 다음 장을 부모가 받아들이지 않으면 진행 막대가 꽉 찬 채로 기다리고(`data-oc-autoplay="paused"`), `index`가 바뀌면 그 슬라이드부터 다시 셉니다.
- 자동 넘김 상태는 아직 제어형으로 열지 않습니다.

## InputGroup

`variant`는 `outlined | underlined` 외형 축입니다. `labelPlacement`는 독립적인 `outside | floating` 배치 축입니다. 기본 요청값은 floating이며, Field와 동일하게 outlined는 outside로 표시하고 underlined에만 floating을 적용합니다. `required`, `readOnly`, `disabled`, `invalid`는 그룹 소유 상태입니다. 네이티브 입력 속성은 `InputGroupInput`에 전달합니다.

## Reveal

`Reveal`의 `variant`는 등장 표현(`fade | slide | blur`) 하나를 나타냅니다. 기본값은 `fade`입니다. `trigger`(`mount | in-view`)는 표현과 독립된 실행 조건입니다. `pace`는 공통 motion scale을 따르고, 별도 `appearance`나 `size`는 없습니다. `RevealProvider`는 같은 기본값을 영역별로 제공합니다.

Reveal은 `visible` 제어 모드와 `trigger`·`threshold` 자동 모드를 union으로 구분합니다. `delay`는 등장 시차(ms)이며 퇴장·반전·reduced-motion에는 적용하지 않습니다. `visible`은 시각 variant가 아닌 표시 상태이고, 높이 변화는 별도 layout owner의 책임입니다.

RevealGroup은 Content·Items가 모션 정책을 공유하고 각자의 진입 시점을 갖는 compound component입니다. variant는 Reveal과 같은 fade | slide | blur이며 기본 fade, pace는 공통 quick | normal | slow를 사용합니다. trigger는 기본 in-view이고 threshold·visible·delay는 받지 않습니다. Content는 본문, Items의 각 직접 자식은 순차 등장 항목입니다. 독서 완료를 감지하거나 임의의 하위 DOM을 분석하지 않습니다.

RevealGroup의 timing은 duration·contentGap·stagger만 선택적으로 받는 밀리초 계약입니다. duration은 pace·전역 배율 적용 전 체감 시간이고, contentGap·stagger도 normal pace 기준 시차이며 pace·전역 배율을 동일하게 적용합니다. 유한한 0 이상 값만 허용하며 접근성·즉시 표시 정책을 우회하지 않습니다.
