# 스크롤 애니메이션

`animateScrollTo`는 버튼 등 명시적인 명령으로 네이티브 스크롤 위치를 이동합니다.
휠·트랙패드·터치의 기본 스크롤은 브라우저에 유지하고, 사용자 입력이 시작되면
프로그램 이동을 중단합니다. React Hook이 아니므로 이벤트 핸들러에서 직접 호출합니다.

```tsx
import { useEffect, useRef } from 'react';
import {
  animateScrollTo,
  type ScrollAnimation,
} from '@orioncactuscorp/ui/utils/scroll';

export function ScrollExample() {
  const container = useRef<HTMLDivElement>(null);
  const animation = useRef<ScrollAnimation | null>(null);
  useEffect(() => () => animation.current?.stop(), []);

  return (
    <>
      <button
        onClick={() => {
          if (container.current) {
            animation.current = animateScrollTo(container.current, {
              top: 600,
            });
          }
        }}
      >
        아래로 이동
      </button>
      <div ref={container} style={{ overflow: 'auto', height: 300 }}>
        <div style={{ height: 1200 }}>스크롤 콘텐츠</div>
      </div>
    </>
  );
}
```

## API

```ts
animateScrollTo(target: Window | HTMLElement, options: ScrollAnimationOptions): ScrollAnimation;
```

| 옵션   | 기본값     | 의미                                                         |
| ------ | ---------- | ------------------------------------------------------------ |
| `top`  | 생략       | CSS px 단위의 절대 세로 좌표                                 |
| `left` | 생략       | CSS px 단위의 네이티브 가로 좌표. RTL에서는 음수 사용        |
| `pace` | `'normal'` | 기존 `move` 모션의 `'quick' \| 'normal' \| 'slow'` 속도 정책 |

생략한 축은 현재 위치에 유지하며, 진행 중인 해당 축의 이전 이동도 중단합니다.
두 축을 모두 생략하면 아무 작업도 하지 않습니다. 유한하지 않은 좌표나 지원하지
않는 pace는 예외입니다. 범위를 벗어난 유한 좌표는 실제 스크롤 범위로 제한합니다.

반환값의 `stop()`은 해당 호출의 이동을 현재 위치에서 중단하며 반복 호출해도
안전합니다. 같은 대상에 다시 호출하면 위치·속도를 이어 새 목표로 이동하고,
이전 handle의 `stop()`은 새 이동에 영향을 주지 않습니다. `window`와 문서의
실제 scrolling element는 같은 실행 대상으로 취급합니다.

```ts
animateScrollTo(window, { top: 0 });
animateScrollTo(container, { left: -400, pace: 'quick' });
```

## 모션과 사용자 입력

- 대상 요소의 [모션 토큰](./motion.md)을 읽어 `move` intent와 전역 duration 설정을 적용합니다. 독립 easing·spring·bounce·duration 옵션은 제공하지 않습니다.
- reduced-motion 또는 유효 duration 0에서는 즉시 이동합니다. 이동 중 reduced-motion이 켜지면 유효 목적지로 즉시 이동하고 종료합니다.
- 휠·터치·포인터·스크롤 키와 외부 코드의 위치 변경은 프로그램 이동을 중단합니다. 편집 가능한 요소의 키 입력과 중첩 스크롤 영역의 입력은 해당 영역에 둡니다.
- 의도적인 즉시 이동에는 네이티브 `scrollTo({ behavior: 'instant' })`를 사용합니다.
- 요소 제거, 페이지 이탈, 숨김 전환에는 실행을 정리합니다. 호출부는 React 언마운트 등 자체 수명이 끝날 때도 `stop()`을 호출하세요.
- 이 함수는 포커스, URL hash, 브라우저 기록, 라우팅을 변경하지 않습니다. 문서 내 탐색이라면 의미에 맞는 링크와 포커스 정책을 호출부에서 구성하세요.

## 드래그·스냅과 조합

같은 요소에 [useDragScroll](./drag-scroll.md)을 연결하면 프로그램 이동이 기존
드래그·release 관성을 중단하고, 새 드래그는 프로그램 이동을 중단합니다.
이 조합에는 버튼 핸들러의 별도 `cancel()` 호출이 필요하지 않습니다. `ScrollArea`와
함께 쓰면 바깥 host 대신 실제 `ScrollAreaContainer`를 대상으로 지정합니다.

CSS snap은 유지합니다. 지원되는 단일 축 mandatory snap에서는 scroll-padding,
scroll-margin, oversized snap 영역을 고려해 요청 좌표에 가장 가까운 허용 위치로
이동합니다. 그 밖의 snap 모드·writing mode에서는 이동 후 브라우저가 최종 위치를
보정할 수 있습니다. `stop()` 이후에도 native snap 정착은 일어날 수 있습니다.
페이지 번호를 지정하려면 [useScrollSnapIndex](./page-indicator.md)를 사용하세요.

함수가 실행되는 동안 snap은 잠시 중지하고 종료 시 복원합니다. 소비자가 실행 중
바꾼 snap 스타일은 덮어쓰지 않습니다. 네이티브 smooth와 중복 실행하지 않도록
프레임별 좌표 쓰기는 `instant`로 처리합니다.
