# Semantic Motion

`oc-motion`은 duration/easing preset wrapper가 아니라 UI motion을 semantic contract로 선언하기 위한 helper입니다. 컴포넌트는 "0.24s ease"를 고르는 대신 무엇이 움직이고, 전환의 어느 순간이며, 같은 motion을 얼마나 빠르게 보정할지만 선언합니다.

```scss
oc-motion($properties, $intent, $phase, $pace)
```

- `$intent`: 무엇이 움직이는가. UI의 역할과 공간 모델을 고릅니다.
- `$phase`: 언제 움직이는가. 전환의 시점과 방향을 고릅니다.
- `$pace`: 같은 motion을 빠르게, 기본값으로, 느리게 보정합니다.
- escape hatch: semantic contract로 부족할 때만 국소 조정합니다.

실제 duration token, spring profile, reduced-motion 정책은 디자인 시스템이 중앙에서 관리합니다.

일반 컴포넌트와 consumer SCSS에서는 `oc-motion`을 우선 사용합니다. `oc-spring`은 직접 물리 spring을 설계해야 하는 고급 escape hatch로 남깁니다.

React runtime에서 height/expand처럼 CSS transition만으로 안정적으로 다루기 어려운 motion은 `@orioncactuscorp/ui/utils/motion`과 `@orioncactuscorp/ui/react/motion` subpath를 사용합니다. 이 runtime layer도 같은 intent, phase, pace, reduced-motion policy, `--oc-motion-duration-factor*` token vocabulary를 공유하고, 실제 프레임 계산은 low-level `ocSpring` engine에 위임합니다.

Runtime motion도 CSS helper와 동일하게 최종 계산 duration이 `0`이면 spring을 시작하지 않고 최종 상태로 즉시 완료합니다. instance factor, global factor token, intent duration, phase duration 중 어느 값으로 `0`이 만들어져도 같은 계약을 적용합니다. `resolveOcMotion`을 직접 사용하는 consumer는 `animate`가 `true`일 때만 반환된 `spring`을 scheduler에 전달해야 합니다.

Runtime duration, duration factor, pace factor는 모두 `0` 이상의 유한한 값이어야 합니다. CSS의 factor/pace token은 표준 CSS `<number>` 형태의 단위 없는 숫자만 허용하므로 `+1.2`, `5e-1` 같은 표기는 유효하지만, `1s`, `0px`, 숫자 뒤 임의 문자열처럼 CSS 계산과 runtime 해석이 달라질 수 있는 값은 설정 오류로 처리합니다. 여러 multiplier를 곱한 최종 duration이 overflow로 비유한 값이 되는 경우(예: 매우 큰 programmatic factor 조합)도 오류입니다.

## 빠른 시작

```scss
@use '@orioncactuscorp/ui/scss/mixins/motion' as *;

.button {
  @include oc-motion((background-color, color), feedback);
}

.notice {
  @include oc-motion(opacity, fade);
}

.popover {
  @include oc-motion((opacity, transform), disclosure, $phase: enter);
}

.popover[data-ending-style] {
  @include oc-motion((opacity, transform), disclosure, $phase: exit);
}

.bottom-sheet {
  @include oc-motion(transform, sheet, $phase: enter);
}

.bottom-sheet[data-drag-release] {
  @include oc-motion(transform, sheet, $phase: release);
}
```

`disclosure`, `surface`, `sheet`, `expand`처럼 lifecycle이 있는 motion은 들어올 때 `enter`, 나갈 때 `exit`를 명시하는 것을 기본으로 합니다. lifecycle selector가 없거나 양방향으로 같은 transition을 써도 되는 단순 케이스에서만 기본값인 `change`를 fallback으로 사용합니다.

`@include oc-motion(...)`은 기본 transition과 `prefers-reduced-motion: reduce` 대응을 함께 출력합니다. reduced-motion 대응이 필요 없는 순수 함수 값만 필요할 때는 `transition: oc-motion(...)`을 직접 사용할 수 있습니다.

```scss
.simple {
  transition: oc-motion(opacity, feedback);
}
```

React runtime에서는 같은 vocabulary를 TS로 해석할 수 있습니다.

```tsx
import { MotionExpand } from '@orioncactuscorp/ui/react/motion';
import { ocMotion } from '@orioncactuscorp/ui/utils/motion';

const config = ocMotion.resolve({
  intent: 'expand',
  phase: 'enter',
  pace: 'normal',
  reduced: 'auto',
});

export function Panel({ open, children }) {
  return (
    <MotionExpand open={open} intent='expand' forceMount>
      {children}
    </MotionExpand>
  );
}
```

## 왜 semantic contract로 작성하는가

motion을 컴포넌트마다 `0.2s ease`, `0.3s linear(...)`처럼 직접 쓰면 다음 문제가 생깁니다.

- 같은 종류의 움직임이 컴포넌트마다 조금씩 달라집니다.
- reduced-motion 정책을 컴포넌트마다 따로 관리합니다.
- fade, disclosure, surface, sheet, expand처럼 비슷해 보이지만 체감 기준이 다른 motion의 경계가 흐려집니다.
- enter, exit, release가 같은 duration/easing으로 묶여 닫힘이나 drag release가 어색해집니다.
- 장기적으로 duration/easing을 조정할 때 전체 surface를 다시 찾아야 합니다.

`oc-motion`은 motion을 `intent + phase + pace`로 표현합니다. 컴포넌트는 motion의 의미를 선언하고, 실제 값은 foundation token과 helper policy가 결정합니다.

## Mental model

| 축           | 답하는 질문                                      | 예시                                    |
| ------------ | ------------------------------------------------ | --------------------------------------- |
| `intent`     | 이 UI는 어떤 역할과 공간 모델로 움직이는가?      | `disclosure`, `surface`, `sheet`        |
| `phase`      | 전환의 어느 순간인가?                            | `enter`, `exit`, `release`              |
| `pace`       | 이 instance는 기본보다 빠른가, 느린가?           | `quick`, `normal`, `slow`               |
| escape hatch | semantic API로 부족한 한 컴포넌트 보정이 있는가? | `$duration-factor`, `$extra-bounce`     |
| amplitude    | motion의 거리나 scale 강도를 token으로 줄일까?   | `oc-motion-scale`, `oc-motion-distance` |

`phase`는 standalone animation이 아닙니다. `enter`만으로는 motion이 정해지지 않고, `disclosure enter`, `surface enter`, `sheet enter`, `expand enter`처럼 intent와 결합해야 체감 기준이 생깁니다.

## oc-motion contract

가장 일반적인 사용은 mixin입니다. base transition과 reduced-motion override를 함께 출력합니다.

### `@include oc-motion(...)`

```scss
@include oc-motion(
  $properties,
  $intent,
  $phase: change,
  $pace: normal,
  $duration-factor: 1,
  $reduced: auto,
  $duration-token: null,
  $extra-bounce: null
);
```

컴포넌트 스타일에서는 이 mixin을 기본으로 사용합니다.

### `oc-motion(...)`

```scss
transition: oc-motion(
  $properties,
  $intent,
  $phase: change,
  $pace: normal,
  $duration-factor: 1,
  $duration-token: null,
  $extra-bounce: null
);
```

transition item list만 반환합니다. `@media (prefers-reduced-motion: reduce)`는 출력하지 않습니다.

### `oc-motion-duration(...)`

```scss
transition-duration: oc-motion-duration(
  $intent,
  $phase: change,
  $pace: normal,
  $duration-factor: 1,
  $duration-factor-token: var(--oc-motion-duration-factor),
  $duration-token: null
);
```

duration token, 전역 duration factor token, pace factor, 숫자 duration factor를 합친 `calc(...)` 값을 반환합니다.

### `oc-motion-timing(...)`

```scss
transition-timing-function: oc-motion-timing($intent, $phase: change);
```

intent와 phase에 맞는 token timing을 반환합니다.

### `oc-motion-scale(...)`

```scss
transform: scale(
  oc-motion-scale($scale, $intent: feedback, $scale-factor-token: null)
);
```

scale amplitude를 runtime token에 연결합니다. 기본 token 값 `1`에서는 입력한 scale을 그대로 유지하고, token을 `0`으로 두면 `scale(1)`이 되어 scale 효과가 사라집니다. `0.5`처럼 0과 1 사이의 값은 더 얕은 scale, `1`보다 큰 값은 더 강한 scale을 만듭니다. 전체 scale은 `--oc-motion-scale-factor`로 조정하고, feedback 계열은 `--oc-motion-scale-factor-feedback`, spatial 계열 intent는 `--oc-motion-scale-factor-spatial`로 override할 수 있습니다. `fade`도 intent vocabulary completeness를 위해 amplitude helper에서 사용할 수 있지만, 권장 contract는 opacity 중심입니다. scale/translate가 motion 의미를 설명하기 시작하면 `disclosure`, `surface`, `sheet`, `move` 같은 역할 intent를 먼저 검토합니다.

### `oc-motion-distance(...)`

```scss
transform: translateY(
  oc-motion-distance($distance, $intent, $distance-factor-token: null)
);
```

translate 또는 keyframe offset처럼 motion 거리 자체가 피드백 강도에 해당하는 값을 runtime token에 연결합니다. 기본 token 값 `1`에서는 입력한 거리를 그대로 유지하고, token을 `0`으로 두면 translate/reject shake 거리가 사라집니다. 전체 distance는 `--oc-motion-distance-factor`로 조정하고, `disclosure`/`surface`/`sheet`/`expand`/`move` 계열은 `--oc-motion-distance-factor-spatial`, `feedback`/`reject`/`gesture` 계열은 `--oc-motion-distance-factor-reject`로 override할 수 있습니다.

## 인자 역할

| 구분             | 인자                     | 값                                                                                                                   | 역할과 사용 기준                                                                                                         |
| ---------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Required         | `$properties`            | `opacity`, `transform`, `(opacity, transform)`                                                                       | transition 대상 property입니다. reduced-motion에서 property별 보존 여부를 판단하는 기준이 됩니다.                        |
| Required         | `$intent`                | `feedback`, `fade`, `reject`, `disclosure`, `surface`, `sheet`, `expand`, `move`, `gesture`, `ambient`, `continuous` | UI motion의 역할과 공간 모델입니다. duration, spring profile, 기본 reduced policy를 결정합니다.                          |
| Timing axis      | `$phase`                 | `change`, `enter`, `exit`, `release`, `loop`                                                                         | 같은 intent 안에서 전환의 시점과 방향을 구분합니다. 생략하면 `change`입니다.                                             |
| Timing axis      | `$pace`                  | `quick`, `normal`, `slow`                                                                                            | 공유 vocabulary로 duration을 상대 보정합니다. 먼저 `$pace`를 고르고, 그래도 부족할 때만 `$duration-factor`를 사용합니다. |
| Reduced axis     | `$reduced`               | `auto`, `preserve`, `fade`, `static`, `feedback`                                                                     | reduced-motion에서 무엇을 남길지 정합니다. 기본은 intent의 policy를 따르는 `auto`입니다.                                 |
| Escape hatch     | `$duration-factor`       | unitless number                                                                                                      | 한 컴포넌트만 미세 조정하는 multiplier입니다. 음수는 금지하고, `0`은 instant completion 용도로만 사용합니다.             |
| Escape hatch     | `$duration-token`        | CSS duration token                                                                                                   | 기존 component-local duration CSS variable과 연결해야 할 때만 사용합니다. spring-backed motion에서는 사용할 수 없습니다. |
| Escape hatch     | `$extra-bounce`          | unitless number                                                                                                      | spring-backed motion의 bounce만 국소 override합니다. transform entrance가 너무 건조할 때처럼 제한적으로 사용합니다.      |
| Advanced token   | `$duration-factor-token` | CSS number token                                                                                                     | `oc-motion-duration(...)` 전용입니다. reduced-motion 내부 정책처럼 multiplier token 자체를 바꿔야 할 때만 사용합니다.    |
| Amplitude helper | `$scale-factor-token`    | CSS number token                                                                                                     | `oc-motion-scale(...)` 전용입니다. scale amplitude를 어떤 token으로 줄일지 바꿉니다.                                     |
| Amplitude helper | `$distance-factor-token` | CSS number token                                                                                                     | `oc-motion-distance(...)` 전용입니다. translate/keyframe distance amplitude를 어떤 token으로 줄일지 바꿉니다.            |

## Intent 선택

선택 기준은 컴포넌트 이름이 아니라 motion의 역할과 공간 모델입니다. 일반 content show/hide는 `fade`, Tooltip과 Menu는 `disclosure`, Popup Modal은 `surface`, Bottom Modal은 `sheet`, Accordion은 `expand`를 사용합니다.

| Intent       | 역할                                      | 대표 사용처                         | 자주 쓰는 phase            | 기본 reduced-motion 정책 |
| ------------ | ----------------------------------------- | ----------------------------------- | -------------------------- | ------------------------ |
| `feedback`   | 즉각적인 시각 반응                        | hover, focus, active                | `change`                   | `feedback`               |
| `fade`       | 일반 content의 단순 표시/숨김             | Notice, helper text, loaded content | `change`                   | `fade`                   |
| `reject`     | 허용되지 않은 시도를 알려주는 문제 피드백 | blocked dismiss, invalid            | `change`, `enter`          | `feedback`               |
| `disclosure` | trigger에 붙은 작은 transient surface     | Menu, Tooltip, Popover              | `enter`, `exit`            | `fade`                   |
| `surface`    | 화면 맥락 위의 작업 focus surface         | Popup Modal                         | `enter`, `exit`            | `fade`                   |
| `sheet`      | 화면 edge에 고정된 spatial panel          | Bottom Modal, drawer                | `enter`, `exit`, `release` | `static`                 |
| `expand`     | layout 흐름 안에서 펼쳐지는 content       | Accordion                           | `enter`, `exit`            | `static`                 |
| `move`       | 같은 control 안에서 위치가 바뀌는 요소    | Switch thumb, indicator             | `change`                   | `static`                 |
| `gesture`    | 사용자 조작 후 정착하는 자유 surface      | Popup drag, drag snap               | `release`                  | `feedback`               |
| `ambient`    | 은은한 대기 상태 반복 표현                | Skeleton, status pulse              | `loop`                     | `static`                 |
| `continuous` | 일정 속도 반복 motion                     | Loading rotate, progress sweep      | `loop`                     | `static`                 |

### Reject feedback

`reject`는 사용자의 시도가 처리되지 않았음을 즉시 알려야 할 때 사용합니다. 예를 들어 닫을 수 없는 Alert의 background를 눌렀거나, 잘못된 입력으로 다음 단계 이동이 막혔거나, 현재 상태에서 허용되지 않는 전환을 시도한 경우가 여기에 해당합니다.

단순 hover, press, selected 같은 반응은 `feedback`을 사용합니다. 시도가 실패했거나 의도적으로 차단되었음을 알려야 할 때만 `reject`를 사용합니다.

`reject` motion은 보통 surface 전체가 짧게 흔들리는 형태입니다. 좌우 이동 거리처럼 motion 강도 자체가 거리로 표현되는 값은 `oc-motion-distance(..., reject)`로 감싸고, component-local 기본 거리와 global distance scale이 함께 작동하게 둡니다.

```scss
.dialog {
  --dialog-reject-distance: #{oc-motion-distance(0.75rem, reject)};

  animation: oc-dialog-reject-shake oc-motion-duration(reject)
    oc-motion-timing(reject) both;
}
```

현재 Alert는 background dismissal이 차단된 상태에서 backdrop을 누르면 Modal surface에 transient `data-oc-modal-feedback='reject'` state를 부여합니다. 기본 흔들림 거리는 `--oc-modal-reject-distance`로 조정할 수 있고, 전체 reject 거리 강도는 `--oc-motion-distance-factor-reject`가 담당합니다.

## Phase 선택

`phase`는 standalone animation 이름이 아니라 intent에 붙는 시점입니다. 같은 `enter`라도 `disclosure enter`는 작은 surface가 민첩하게 열리는 motion이고, `sheet enter`는 edge-anchored panel이 공간을 설명하며 들어오는 motion입니다.

| Phase     | 답하는 질문                         | 대표 조합                          |
| --------- | ----------------------------------- | ---------------------------------- |
| `change`  | 값이나 상태가 바뀌었는가?           | `feedback change`, `move change`   |
| `enter`   | 화면에 나타나거나 열리는가?         | `disclosure enter`, `sheet enter`  |
| `exit`    | 화면에서 사라지거나 닫히는가?       | `disclosure exit`, `surface exit`  |
| `release` | 사용자가 drag를 놓은 뒤 정착하는가? | `sheet release`, `gesture release` |
| `loop`    | 자동으로 반복되는가?                | `ambient loop`, `continuous loop`  |

`settle`은 public phase로 쓰지 않습니다. 사용자가 일으킨 단계는 `release`이고, 최종 상태에 수렴하는 물리 결과는 runtime이나 spring 내부 구현에서만 `settle`이라는 이름을 사용할 수 있습니다.

`fade`는 일반 transition처럼 `change` 기본값을 사용합니다. 같은 opacity transition이 `0 -> 1`, `1 -> 0` 양방향에 적용되므로, exit timing을 별도로 조정해야 할 때만 `$phase: exit` 같은 override를 추가합니다.

`change`는 lifecycle phase를 대체하는 이름이 아니라 fallback입니다. Base UI의 `data-starting-style`/`data-ending-style`, `data-state`, class 같은 lifecycle selector가 있으면 해당 selector에서 `enter`/`exit`를 명시하고, selector를 구분하기 어려운 단순 state transition에서만 `change`를 사용합니다. `oc-motion`은 lifecycle selector를 생성하지 않고, 어떤 selector가 어느 phase를 의미하는지는 컴포넌트나 primitive가 소유합니다.

`disclosure`, `surface`, `sheet`, `expand`는 역할별 profile이 다릅니다. `disclosure enter/change`의 transform은 작은 surface가 민첩하게 열리도록 낮은 snappy bounce를 허용하고, `disclosure exit`은 spring 없이 `0.14s`와 `cubic-bezier(0.8, 0, 1, 1)`로 초반 관성을 낮춘 뒤 빠르게 정리합니다. `surface`, `sheet`, `expand`는 기본적으로 smooth/no-bounce입니다. Popup Modal은 1보다 커지는 scale overshoot를 피하고, `surface exit`은 smooth `0.24`를 기준으로 닫힘을 정리합니다. Bottom Sheet는 iOS bottom sheet reference에 맞춰 `sheet enter` smooth `0.26`, `sheet exit/release` smooth `0.33`을 기준으로 둡니다. Accordion은 뜨는 surface가 아니라 문서 흐름이 펼쳐지는 motion이므로 `expand`를 사용합니다.

`reject`는 사용자의 시도가 거부되었음을 알려야 하는 surface shake motion에 사용하며, keyframes 자체가 좌우 반동을 만들기 때문에 timing은 smooth spring을 사용합니다. 좌우로 방향을 여러 번 바꿔야 하는 shake는 keyframes로 변위를 정의하고 `oc-motion-duration(reject)`와 `oc-motion-timing(reject)`를 animation duration/timing에 연결합니다. 단일 transform 복귀 transition만 필요한 경우에는 `oc-motion(transform, reject)`를 사용할 수 있습니다.

`move`는 Switch thumb, indicator, Accordion chevron처럼 같은 control 안에서 위치나 방향이 바뀌는 motion에 사용합니다. hover, press, selected 같은 즉각 반응은 `feedback change`를 유지합니다. `ambient loop`는 Skeleton pulse처럼 은은하게 반복되는 상태 표현에 사용하고, spinner 회전이나 progress sweep처럼 일정 속도 자체가 의미인 반복은 `continuous loop`를 사용합니다. 두 반복 intent는 spring-backed motion이 아니며 reduced-motion에서는 기본 static으로 둡니다. `gesture release`는 Popup drag snap처럼 작은 자유 surface가 손을 놓은 뒤 빠르게 정착해야 하는 경우에 사용하며, SCSS transition helper와 TS runtime animator 모두 snappy spring profile을 사용합니다.

```scss
.menu {
  @include oc-motion((opacity, transform), disclosure, $phase: enter);
}

.menu[data-ending-style] {
  @include oc-motion((opacity, transform), disclosure, $phase: exit);
}

.bottom-sheet {
  @include oc-motion(transform, sheet, $phase: enter);
}

.accordion-details {
  @include oc-motion(block-size, expand, $phase: enter);
}

.dialog[data-oc-feedback='reject'] {
  animation: oc-dialog-reject-shake oc-motion-duration(reject)
    oc-motion-timing(reject) both;
}

.chevron {
  @include oc-motion(transform, move, $pace: quick);
}
```

## Extra bounce override

`$extra-bounce`는 spring-backed transition의 반동 강도를 컴포넌트 한 곳에서만 조정해야 할 때 쓰는 escape hatch입니다. 기본 profile을 우선 사용하고, 같은 intent 안에서도 transform entrance가 너무 건조하거나 과하게 느껴지는 경우에만 사용합니다.

```scss
.small-menu {
  @include oc-motion(transform, disclosure, $phase: enter, $extra-bounce: 0.08);
}

.tooltip {
  @include oc-motion(transform, disclosure, $phase: enter, $extra-bounce: 0);
}
```

`$extra-bounce`는 `oc-spring`의 preset bounce에 더해지는 unitless 값입니다. `disclosure enter/change`의 `transform`은 기본적으로 약한 extra bounce를 갖고, override를 넘기면 property 기본값보다 명시 값이 우선합니다. 여러 property를 함께 넘기면 같은 override가 모든 spring-backed property에 적용되므로, opacity처럼 overshoot가 의미 없는 property와 섞기보다 transform transition에 국소적으로 쓰는 편이 좋습니다.

non-spring motion에서는 `$extra-bounce`가 의미 없으므로 helper가 에러를 냅니다. `feedback change`처럼 token timing path를 쓰는 motion은 `$pace`, `$duration-factor`, `oc-motion-scale`, `oc-motion-distance`로 조정합니다. Reduced-motion에서는 transform spring이 제거될 수 있으므로 extra bounce는 일반 motion profile의 국소 조정으로만 봅니다.

## Pace와 duration factor

`$pace`는 반복적으로 쓰는 semantic 선택지입니다.

```scss
.fast-feedback {
  @include oc-motion(opacity, feedback, $pace: quick);
}

.calm-surface {
  @include oc-motion((opacity, transform), surface, $pace: slow);
}
```

foundation의 `--oc-motion-pace-*` token은 unitless duration multiplier입니다.

```scss
--oc-motion-pace-quick: 0.75;
--oc-motion-pace-normal: 1;
--oc-motion-pace-slow: 1.35;
```

`$duration-factor`은 개별 컴포넌트에서 token 체계를 벗어나지 않고 미세 조정해야 할 때만 사용합니다.

```scss
.compact {
  @include oc-motion(opacity, feedback, $duration-factor: 0.85);
}
```

`$pace`와 `$duration-factor`은 같은 개념이 아닙니다. `$pace`는 공유 vocabulary이고, `$duration-factor`은 국소 조정을 위한 숫자 multiplier입니다. 장기 유지보수 관점에서는 `$pace`를 먼저 선택하고, 정말 필요한 경우에만 `$duration-factor`을 추가합니다.

spring-backed Sass transition에서는 두 종류의 시간이 보입니다. `sheet enter 0.26s`, `sheet release 0.33s` 같은 값은 spring curve를 샘플링하는 semantic 기준 duration이고, CSS에 출력되는 transition-duration은 그 spring이 안정적으로 settle되는 시간입니다. 그래서 compiled CSS나 Storybook preview에서는 `0.26s`가 더 긴 `calc(...)` 값으로 보일 수 있습니다. `$pace`와 `$duration-factor`는 이 출력 duration에 곱해지는 instance 보정입니다.

## Reduced Motion

`@include oc-motion(...)`은 항상 reduced-motion block을 함께 출력합니다.

```scss
.tooltip {
  @include oc-motion((opacity, transform), disclosure);
}
```

reduced-motion에서 `disclosure`와 `surface`는 opacity fade만 남기고 transform은 제거합니다. base transition이 spring-backed transition이어도 reduced block에서는 `transition-timing-function`을 `oc-motion-timing(...)` 기반 token timing으로 명시해 spring/overshoot timing이 남지 않게 합니다. `sheet`와 `expand`는 기본적으로 static입니다. Bottom Sheet처럼 공간 구조 이해에 꼭 필요한 translate는 컴포넌트가 별도 invariant로 보존할 수 있습니다.

```css
@media (prefers-reduced-motion: reduce) {
  .tooltip {
    transition-property: opacity;
    transition-duration: calc(
      var(--oc-motion-duration-disclosure) *
        var(--oc-motion-duration-factor-reduced) *
        var(--oc-motion-pace-normal) * 1
    );
    transition-timing-function: var(--oc-motion-timing-disclosure);
    transform: none;
  }
}
```

### Reduced policy

| Policy     | 동작                                                                                                                                    |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `auto`     | intent의 기본 reduced-motion 정책을 사용합니다.                                                                                         |
| `fade`     | feedback-safe property만 transition하고 transform은 제거합니다.                                                                         |
| `feedback` | `fade`와 같은 방식으로 paint/opacity feedback만 남깁니다.                                                                               |
| `static`   | transition을 제거합니다. transform이 있으면 `transform: none`도 출력합니다.                                                             |
| `preserve` | motion을 유지하되 `--oc-motion-duration-factor-preserve`와 token timing을 사용합니다. 공간 구조 이해에 꼭 필요한 motion에만 사용합니다. |

feedback-safe property는 다음으로 제한합니다.

```scss
opacity
color
background-color
border-color
box-shadow
outline-color
fill
stroke
```

예를 들어 `@include oc-motion(transform, feedback)`처럼 feedback-safe property가 하나도 없으면 reduced-motion에서는 transition을 제거합니다.

`transform`을 `:active`, checked selector, `[data-state]` 같은 더 구체적인 state selector에서 바꾸는 경우에는 reduced-motion override도 같은 state selector에서 보완해야 합니다. `oc-motion`이 출력하는 `transform: none`은 helper가 include된 selector에만 적용되므로, 더 높은 specificity의 state transform까지 자동으로 이기지는 않습니다.

## Component-local duration token

기존 component contract를 유지해야 할 때 `$duration-token`을 사용할 수 있습니다.

```scss
.interaction {
  --oc-interaction-time: 0.2s;

  @include oc-motion(
    (opacity, transform),
    feedback,
    $duration-token: var(
        --oc-interaction-time,
        var(--oc-motion-duration-feedback)
      )
  );
}
```

이 옵션은 non-spring motion에서만 허용합니다. spring-backed motion은 Sass가 compile-time duration으로 spring curve를 샘플링해야 하므로 CSS custom property duration을 받을 수 없습니다.

```scss
// Invalid: surface enter/change is spring-backed.
.invalid {
  @include oc-motion(opacity, surface, $duration-token: var(--local-time));
}
```

## Runtime API

CSS transition만으로 안정적으로 다루기 어려운 height, measured layout, gesture release motion은 runtime API를 사용할 수 있습니다. runtime도 같은 `intent + phase + pace` vocabulary를 사용합니다.

```tsx
import { ocMotion } from '@orioncactuscorp/ui/utils/motion';

const config = ocMotion.resolve({
  intent: 'sheet',
  phase: 'release',
  pace: 'normal',
  reduced: 'auto',
  reducedMotion: prefersReducedMotion,
  property: 'transform',
  element: surfaceElement,
});
```

| Option           | 역할                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `intent`         | UI motion의 역할과 공간 모델입니다. Sass `$intent`와 같은 vocabulary를 사용합니다.                           |
| `phase`          | 전환 시점입니다. 생략하면 `change`입니다.                                                                    |
| `pace`           | semantic speed modifier입니다. 생략하면 `normal`입니다.                                                      |
| `durationFactor` | 한 instance만 duration을 미세 조정하는 숫자 multiplier입니다.                                                |
| `extraBounce`    | spring-backed motion의 bounce를 국소 override합니다. non-spring motion에서는 에러를 냅니다.                  |
| `reduced`        | 어떤 reduced-motion policy를 적용할지 고릅니다. `auto`는 intent 기본 policy를 사용합니다.                    |
| `reducedMotion`  | 현재 환경이나 호출 맥락이 reduced-motion인지 전달합니다. policy 이름이 아니라 runtime 상태입니다.            |
| `property`       | reduced-motion에서 animate 여부와 property별 spring preset을 판단하는 기준입니다. 가능하면 명시합니다.       |
| `element`        | computed CSS custom property를 읽을 대상 element입니다. 전달하지 않으면 default token snapshot을 사용합니다. |
| `tokens`         | 테스트나 특수 runtime에서 element 대신 직접 넘기는 token snapshot override입니다.                            |

`reduced`와 `reducedMotion`은 다른 값입니다. `reduced`는 정책이고, `reducedMotion`은 현재 motion을 줄여야 하는 상태입니다. `property`도 중요합니다. 예를 들어 `feedback` reduced policy는 opacity, color, background-color 같은 feedback-safe property만 유지하고 `transform`은 제거할 수 있습니다.

`$duration-token`은 Sass-only escape hatch입니다. runtime에서는 `element`의 computed CSS custom property나 `tokens` override를 통해 duration token을 읽습니다.

## Token customization

consumer는 foundation CSS custom property를 override해 전체 motion 톤을 조정할 수 있습니다.

```scss
:root {
  --oc-motion-duration-factor: 0.9;
  --oc-motion-scale-factor: 0.8;
  --oc-motion-scale-factor-feedback: 0.6;
  --oc-motion-scale-factor-spatial: 0;
  --oc-motion-distance-factor: 0.75;
  --oc-motion-distance-factor-reject: 1;
  --oc-motion-distance-factor-spatial: 0.5;
  --oc-motion-duration-feedback: 0.16s;
  --oc-motion-pace-slow: 1.25;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --oc-motion-duration-factor-reduced: 0.65;
  }
}
```

amplitude token은 parent-child 구조를 따릅니다. 전체 톤을 조정할 때는 parent token을 먼저 바꾸고, 특정 intent만 다르게 가져가야 할 때 child token을 override합니다.

```scss
:root {
  --oc-motion-scale-factor: 0.75;
  --oc-motion-distance-factor: 0.75;
}

.dense-tool {
  --oc-motion-scale-factor-feedback: 0.5;
}

.calm-surface {
  --oc-motion-distance-factor-spatial: 0.25;
}
```

`--oc-motion-scale-factor-feedback`, `--oc-motion-scale-factor-spatial`, `--oc-motion-distance-factor-reject`, `--oc-motion-distance-factor-spatial`은 기본적으로 각 parent token을 참조합니다. 따라서 child token을 지정하지 않으면 전체 scale/distance factor 정책을 그대로 따릅니다.

주요 token은 다음과 같습니다.

```scss
--oc-motion-duration-factor
--oc-motion-duration-factor-reduced
--oc-motion-duration-factor-preserve
--oc-motion-scale-factor
--oc-motion-scale-factor-feedback
--oc-motion-scale-factor-spatial
--oc-motion-distance-factor
--oc-motion-distance-factor-reject
--oc-motion-distance-factor-spatial
--oc-motion-pace-quick
--oc-motion-pace-normal
--oc-motion-pace-slow
--oc-motion-duration-feedback
--oc-motion-duration-fade
--oc-motion-duration-reject
--oc-motion-duration-disclosure
--oc-motion-duration-disclosure-exit
--oc-motion-duration-surface
--oc-motion-duration-surface-exit
--oc-motion-duration-sheet
--oc-motion-duration-sheet-exit
--oc-motion-duration-sheet-release
--oc-motion-duration-expand
--oc-motion-duration-expand-exit
--oc-motion-duration-move
--oc-motion-duration-gesture
--oc-motion-duration-gesture-release
--oc-motion-duration-ambient
--oc-motion-duration-continuous
--oc-motion-timing-feedback
--oc-motion-timing-fade
--oc-motion-timing-reject
--oc-motion-timing-disclosure
--oc-motion-timing-disclosure-exit
--oc-motion-timing-surface
--oc-motion-timing-surface-exit
--oc-motion-timing-sheet
--oc-motion-timing-sheet-exit
--oc-motion-timing-sheet-release
--oc-motion-timing-expand
--oc-motion-timing-expand-exit
--oc-motion-timing-move
--oc-motion-timing-gesture
--oc-motion-timing-gesture-release
--oc-motion-timing-ambient
--oc-motion-timing-continuous
```

spring-backed Sass `@include oc-motion(...)`은 `--oc-motion-duration-sheet` 같은 intent duration token을 직접 읽어 curve를 다시 만들지 않습니다. spring timing curve는 Sass compile-time의 semantic duration으로 샘플링하고, 출력 transition-duration에는 settle duration, `--oc-motion-duration-factor`, `--oc-motion-pace-*`, `$duration-factor`만 남깁니다. 개별 `--oc-motion-duration-*` token은 non-spring path, reduced-motion fallback, `oc-motion-duration(...)`, TS runtime token snapshot에서 사용합니다.

TS runtime `ocMotion.resolve(...)`도 동일하게 element의 computed CSS custom property를 읽어 duration을 계산합니다. `extraBounce` option은 Sass `$extra-bounce`와 같은 정책을 따르며 spring-backed motion에서만 사용할 수 있습니다. reduced-motion 환경에서는 `preserve` 정책만 `--oc-motion-duration-factor-preserve`를 사용하고, `fade`와 `feedback` 정책은 `--oc-motion-duration-factor-reduced`를 사용합니다. 일반 content show/hide는 `fade`라 reduced-motion에서도 opacity fade만 남깁니다. Accordion처럼 `block-size` 자체를 움직이는 `expand` motion은 reduced-motion에서 즉시 끝냅니다. Menu/Tooltip은 `disclosure`, Popup Modal은 `surface`라 reduced-motion에서도 opacity fade만 남깁니다. Bottom Modal의 open/drag release처럼 sheet 위치 자체가 인터랙션의 의미인 경우에는 별도 product invariant로 사용자 설정과 무관하게 동일한 공간 이동을 유지합니다.

## `oc-motion`과 `oc-spring`

대부분의 컴포넌트 transition은 `oc-motion`을 사용합니다.

```scss
.content {
  @include oc-motion((opacity, transform), surface);
}
```

직접 spring의 preset, bounce, physical parameter를 설계해야 하는 낮은 수준의 motion은 `oc-spring`을 사용합니다.

```scss
@use '@orioncactuscorp/ui/scss/mixins/spring' as *;

.custom-sheet {
  transition: oc-spring(transform, 0.5s, bouncy, 0.1);
}
```

`oc-spring`을 사용할 때도 reduced-motion 정책은 직접 작성해야 합니다. public 컴포넌트에서는 특별한 이유가 없다면 `oc-motion`에 새 intent/profile을 추가하는 쪽이 장기적으로 더 유지보수하기 쉽습니다.

자세한 low-level spring 사용법은 [Spring Motion](./spring.md)을 참고하세요.

## 권장 패턴

```scss
// Good: intent와 reduced-motion 정책을 중앙에서 관리합니다.
.notice {
  @include oc-motion(opacity, fade);
}

// Good: 작은 floating surface는 disclosure intent를 사용합니다.
.menu {
  @include oc-motion((opacity, transform), disclosure);
}

// Good: disclosure translate 거리는 distance factor token으로 전역 조정할 수 있습니다.
.menu[data-starting-style] {
  transform: scale(oc-motion-scale(0.82, disclosure))
    translateY(oc-motion-distance(-1rem, disclosure));
}

// Good: exit은 phase로 표현합니다.
.menu[data-ending-style] {
  @include oc-motion((opacity, transform), disclosure, $phase: exit);
}

// Good: modal 형태별 motion intent를 나눕니다.
.popup-modal {
  @include oc-motion((opacity, transform), surface, $phase: enter);
}

.bottom-sheet {
  @include oc-motion(transform, sheet, $phase: enter);
}

.accordion-panel {
  @include oc-motion(block-size, expand, $phase: enter);
}

// Good: 단순 feedback은 feedback intent를 사용합니다.
.button {
  @include oc-motion((background-color, color, box-shadow), feedback);
}

// Good: 거부된 시도 후 surface shake는 reject duration/timing을 사용합니다.
.dialog {
  --dialog-reject-distance: #{oc-motion-distance(0.75rem, reject)};

  animation: oc-dialog-reject-shake oc-motion-duration(reject)
    oc-motion-timing(reject) both;
}
```

## 피해야 할 패턴

```scss
// Avoid: 같은 disclosure motion이 컴포넌트마다 다른 raw easing으로 흩어집니다.
.tooltip {
  transition:
    opacity 0.2s ease,
    transform 0.2s cubic-bezier(0.2, 0, 0, 1);
}

// Avoid: public 컴포넌트가 raw spring과 reduced-motion 정책을 따로 관리합니다.
.menu {
  transition: oc-spring((opacity, transform), 0.3s, snappy);
}

// Avoid: pace 대신 숫자 scale만 공유 vocabulary처럼 사용합니다.
.surface {
  @include oc-motion(opacity, surface, $duration-factor: 1.35);
}
```

## 테스트 팁

SCSS helper나 public component transition을 바꿀 때는 Sass compilation test로 다음을 확인합니다.

- 기본 transition이 의도한 intent/phase/pace token 또는 spring profile을 사용하는지
- function-only `oc-motion(...)` 호출이 reduced-motion media를 출력하지 않는지
- mixin `@include oc-motion(...)` 호출이 reduced-motion media를 출력하는지
- spring-backed disclosure/surface가 reduced-motion에서 token timing으로 바뀌는지
- spring-backed motion의 `$extra-bounce` override와 non-spring error가 의도대로 동작하는지
- transform이 reduced-motion에서 제거되어야 하는 policy인지
- invalid intent, phase, pace, reduced policy, negative duration factor이 compile error로 실패하는지

컴포넌트 테스트에서는 selector와 상태별 CSS 결과를 함께 확인합니다. Menu, Tooltip처럼 Base UI의 `data-starting-style`, `data-ending-style`, `data-side`에 의존하는 컴포넌트는 helper 출력과 handwritten override의 cascade 관계도 테스트 또는 주석으로 남깁니다.
