# Spring Motion

`oc-spring(...)`은 iOS spring의 perceptual duration 기준으로 CSS transition을 작성하기 위한 SCSS helper입니다. 입력한 duration은 사용자가 의도한 감각적 시간이고, 출력 CSS에는 감쇠가 안정되는 settling duration과 `linear(...)` timing function이 들어갑니다.

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

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

Sass는 위 코드를 대략 다음 형태의 CSS로 컴파일합니다.

```css
.sheet {
  transition: transform 0.815s linear(...);
}
```

## 기본 사용법

단일 property는 CSS transition shorthand와 비슷하게 씁니다.

```scss
.smooth {
  transition: oc-spring(transform, 0.5s, smooth);
}

.snappy {
  transition: oc-spring(transform, 0.5s, snappy);
}

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

네 번째 positional 값은 preset bounce에 더하는 `extra-bounce`입니다.

```scss
.tuned {
  transition: oc-spring(transform, 0.5s, snappy, 0.1);
}
```

delay가 필요하면 named argument를 사용합니다.

```scss
.delayed {
  transition: oc-spring(transform, 0.5s, snappy, 0.1, $delay: 80ms);
}
```

## 여러 Property

같은 spring 설정을 여러 property에 적용할 수 있습니다.

```scss
.popover {
  transition: oc-spring((opacity, transform), 0.3s, snappy);
}
```

property별로 다른 설정이 필요하면 map을 사용합니다.

```scss
.menu {
  transition: oc-spring(
    (
      opacity: (
        0.3s,
        snappy,
      ),
      transform: (
        0.3s,
        snappy,
        0.1,
      ),
    )
  );
}
```

property별 tuple 순서는 다음과 같습니다.

```scss
(duration, preset, extra-bounce, delay, samples)
```

예를 들어 opacity에는 짧은 snappy spring을, transform에는 extra bounce와 delay를 줄 수 있습니다.

```scss
.panel {
  transition: oc-spring(
    (
      opacity: (
        0.2s,
        snappy,
      ),
      transform: (
        0.3s,
        snappy,
        0.1,
        40ms,
      ),
    )
  );
}
```

## 고급 옵션

`bounce`, physical spring, `initial-velocity`, `epsilon`, `stability-window`, `max-duration`처럼 의미가 더 명시적이어야 하는 옵션은 named map을 사용합니다.

```scss
.custom-bounce {
  transition: oc-spring(transform, 0.6s, $bounce: 0.2);
}

.physical {
  transition: oc-spring(transform, $mass: 1, $stiffness: 100, $damping: 10);
}

.mixed {
  transition: oc-spring(
    (
      opacity: (
        duration: 0.2s,
        preset: snappy,
        delay: 50ms,
      ),
      transform: (
        mass: 1,
        stiffness: 100,
        damping: 10,
      ),
    )
  );
}
```

property별 named map에서 사용할 수 있는 key는 다음과 같습니다.

```scss
duration
preset
extra-bounce
bounce
mass
stiffness
damping
initial-velocity
delay
epsilon
stability-window
max-duration
samples
```

알 수 없는 key나 5개를 초과하는 tuple은 컴파일 에러로 처리합니다.

## Sass 문법 주의점

직접 함수 호출에서는 Sass named argument를 사용할 수 있습니다.

```scss
.direct {
  transition: oc-spring(transform, 0.5s, snappy, $extra-bounce: 0.1);
}
```

하지만 property별 tuple 안에서는 `$extra-bounce:` 같은 named argument 문법을 사용할 수 없습니다. tuple 안에서는 positional 값을 쓰거나, named map key를 사용해야 합니다.

```scss
.valid-tuple {
  transition: oc-spring(
    (
      transform: (
        0.5s,
        snappy,
        0.1,
      ),
    )
  );
}

.valid-map {
  transition: oc-spring(
    (
      transform: (
        duration: 0.5s,
        preset: snappy,
        extra-bounce: 0.1,
      ),
    )
  );
}
```

## Prebuilt CSS에서 사용

SCSS를 컴파일하지 않고 prebuilt CSS만 쓰는 consumer는 foundation token을 사용할 수 있습니다.

```scss
.target {
  transition: transform calc(0.5s * var(--oc-spring-bouncy-duration-scale))
    var(--oc-spring-bouncy-timing);
}
```

현재 제공되는 preset token은 다음과 같습니다.

```scss
--oc-spring-smooth-timing
--oc-spring-smooth-duration-scale
--oc-spring-snappy-timing
--oc-spring-snappy-duration-scale
--oc-spring-bouncy-timing
--oc-spring-bouncy-duration-scale
```

## Escape Hatches

transition shorthand를 직접 조립해야 하면 timing과 duration helper를 따로 사용할 수 있습니다.

```scss
.target {
  transition-property: opacity, transform;
  transition-duration: oc-spring-settle-duration(0.5s, snappy);
  transition-timing-function: oc-spring-timing(0.5s, snappy);
}
```

기존 CSS 변수 방식과 조합해야 하면 scale helper를 사용할 수 있습니다.

```scss
.target {
  transition-duration: calc(0.5s * #{oc-spring-duration-scale(0.5s, snappy)});
}
```

## 어떤 API를 써야 하나요?

- CSS transition만 필요하면 `oc-spring(...)`을 사용합니다.
- JS에서 transition 문자열이나 spring sample을 계산해야 하면 `ocSpring.*`를 사용합니다.
- gesture, drag, interrupt처럼 target이 계속 바뀌는 DOM interaction은 `createSpringAnimator`를 사용합니다.
- React state의 `target`을 따라가는 UI는 `useSpringTarget`으로 시작합니다.
- React에서 `setTarget()`, `setSpring()`, `jumpTo()`를 직접 제어해야 하면 `useSpringValue`를 사용합니다.

## Why This Exists

CSS transition은 선언적이고 가볍지만, gesture 중 target이 바뀌는 순간 현재 velocity를 이어받기 어렵습니다. 일반 easing curve는 시간에 대한 값만 정의하므로 interrupt 시점의 물리 상태를 자연스럽게 보존하지 못합니다.

orioncactus spring은 Apple Spring처럼 duration/bounce로 조율할 수 있는 물리 모델을 사용합니다. 같은 model/solver를 SCSS sampling, TypeScript runtime, React hook이 공유하기 때문에 정적인 transition과 interactive gesture가 같은 motion language를 갖습니다.

```mermaid
flowchart LR
  A["duration + bounce"] --> B["spring model"]
  C["mass / stiffness / damping"] --> B
  B --> D["closed-form solver"]
  D --> E["SCSS linear(...) sampling"]
  D --> F["runtime animator"]
  F --> G["React hooks"]
```

## TypeScript Runtime

런타임에서 spring 값을 계산하거나 CSS transition 문자열을 조립해야 하면 `@orioncactuscorp/ui/utils/spring`을 사용합니다. `ocSpring.*` 결과는 Apple `Spring`과 같은 물리 모델을 공유하므로 static CSS sampling과 runtime animator에 같은 spring 값을 전달할 수 있습니다.

```ts
import { ocSpring } from '@orioncactuscorp/ui/utils/spring';

const spring = ocSpring.snappy({ duration: 0.5, extraBounce: 0.1 });

spring.transition('transform');
spring.settlingDuration;
spring.timingFunction;
spring.evaluate({ time: 0.2 }); // { value, velocity }
```

진행 중인 spring을 자연스럽게 interrupt해야 하는 gesture나 drag UI는 `createSpringAnimator`를 사용합니다. animator는 frame마다 현재 value와 velocity를 추적하고, `setTarget()`이 호출되면 그 순간의 velocity를 다음 spring으로 이어받습니다.

```ts
import {
  createSpringAnimator,
  ocSpring,
} from '@orioncactuscorp/ui/utils/spring';

const spring = ocSpring.snappy({ duration: 0.5, extraBounce: 0.1 });

const animator = createSpringAnimator({
  value: { x: 0, y: 0 },
  target: { x: 160, y: 80 },
  spring,
  onUpdate: value => {
    element.style.transform = `translate(${value.x}px, ${value.y}px)`;
  },
});

animator.setTarget({ x: 240, y: 140 });
```

`value`, `target`, `velocity`는 number, number array, numeric object를 지원합니다. object를 사용할 때는 초기 value와 같은 key shape를 유지해야 합니다.

spring 자체를 바꿔야 하면 `setSpring()` 또는 `setTarget(target, { spring })`을 사용합니다. `blendDuration`을 전달하면 duration/bounce 기반 spring끼리는 duration과 bounce 값을, physical spring은 mass/stiffness/damping 값을 일정 시간 동안 섞습니다.

```ts
animator.setTarget(
  { x: 320, y: 120 },
  {
    spring: ocSpring.bouncy({ duration: 0.45 }),
    blendDuration: 0.2,
  },
);
```

## React Hook Runtime

React component에서 spring 값을 state처럼 구독하려면 `@orioncactuscorp/ui/react/spring`을 사용합니다. `useSpringTarget`은 React state의 target을 따라가는 controlled hook이고, `useSpringValue`는 현재 `value`, `velocity`, `target`과 imperative setter를 함께 반환하는 low-level hook입니다.

```tsx
'use client';

import { useState } from 'react';
import { useSpringTarget } from '@orioncactuscorp/ui/react/spring';
import { ocSpring } from '@orioncactuscorp/ui/utils/spring';

const spring = ocSpring.snappy({ duration: 0.5, extraBounce: 0.1 });

export function DragFollower() {
  const [target, setTarget] = useState({ x: 160, y: 80 });
  const position = useSpringTarget(target, {
    value: { x: 0, y: 0 },
    spring,
  });

  return (
    <button
      type='button'
      onPointerMove={event => {
        setTarget({ x: event.clientX, y: event.clientY });
      }}
    >
      <span
        style={{
          transform: `translate3d(${position.value.x}px, ${position.value.y}px, 0)`,
        }}
      />
    </button>
  );
}
```

object/array target은 reference 변경 기준으로 retarget합니다. 매 render마다 새 object를 만들면 매번 새 target으로 간주하므로, 계산된 target은 `useMemo`나 state로 안정화하는 것을 권장합니다.

`useSpringAnimator`는 React lifecycle에 맞춰 animator를 생성/정리해야 하지만 DOM write는 직접 제어하고 싶은 경우에 사용합니다. `useSpringTarget`과 `useSpringValue` 모두 초기 생성 옵션은 `createSpringAnimator`와 같은 shape를 쓰고, 세밀한 retarget과 spring 변경은 반환된 `setTarget()`, `setSpring()`, `jumpTo()`로 제어합니다.

## Architecture: Apple Spring Model

orioncactus spring은 `duration`을 CSS transition duration이 아니라 Apple Spring의 perceptual duration 관점으로 다룹니다. 사용자가 지정한 duration은 움직임이 지각되는 기본 response이고, 실제 CSS에는 값이 충분히 안정되는 settling duration을 계산해 출력합니다. 그래서 `spring.settlingDuration`은 입력 duration보다 길 수 있습니다.

duration/bounce 기반 spring은 먼저 `bounce`를 damping ratio로 바꾸고, duration을 mass/stiffness/damping으로 변환합니다. duration spring은 mass를 1로 고정해 response가 입력 duration과 맞도록 stiffness를 계산하고, damping은 damping ratio에서 파생합니다. physical spring은 이 변환을 거치지 않고 사용자가 전달한 mass/stiffness/damping을 그대로 사용합니다.

SCSS `oc-spring(...)`, TypeScript `ocSpring.*`, `createSpringAnimator`, React `useSpringTarget`, `useSpringValue`는 모두 같은 spring model과 closed-form solver를 공유합니다. CSS helper는 solver 결과를 `linear(...)` stop으로 샘플링하고, runtime animator는 frame마다 같은 solver로 현재 value와 velocity를 평가합니다. 새 API를 추가할 때도 별도 easing engine을 만들지 않고 이 model/solver를 통과해야 합니다.

runtime animator의 핵심 contract는 velocity continuity입니다. gesture 도중 `setTarget()`이 호출되면 현재 position과 velocity를 보존한 채 새 target으로 이어가므로, drag나 interrupt가 CSS transition 재시작처럼 끊기지 않습니다. `setSpring()`과 `blendDuration`도 같은 원칙을 따르며, React hook은 이 animator를 React lifecycle에 맞춰 감싼 얇은 wrapper입니다.

## 비용과 기준

`oc-spring(...)`은 spring curve를 CSS `linear(...)` stop으로 샘플링하므로 출력 CSS가 길어집니다. 컴포넌트 내부 transition처럼 재사용되는 스타일에는 적합하지만, 수십 개의 서로 다른 spring을 한 화면에서 동적으로 생성하는 용도로는 적합하지 않습니다.

기본 기준은 다음과 같습니다.

- 단순한 opacity, transform motion은 `oc-spring(property, duration, preset)` 사용
- preset보다 조금 더 탄성이 필요하면 네 번째 positional `extra-bounce` 사용
- 여러 property가 같은 rhythm이면 property list 사용
- property별 설정이 다르면 property map 사용
- physical spring과 고급 옵션은 named map 사용
- gesture, drag, interrupt가 중요한 런타임 상호작용은 `createSpringAnimator` 사용
- React state target을 따라가야 하면 `useSpringTarget` 사용
- React component에서 spring 값을 직접 제어해야 하면 `useSpringValue` 사용
