---
name: ui-motion-principles
description: |
  UI コンポーネントに「動き」を付けるときに使う Skill。Disney の 12 原則を実装した TypeScript ライブラリ
  twelve-principles で、ボタンの押し心地・ホバー・入場/退場・トースト/モーダルの
  出し入れ・リストの順次表示・エラーの shake・お祝い演出・派手なアニメーションを実装する。
  「アニメーションを付けて」「動きをつけて」「モーション」「気持ちいい押し心地」「ぬるっと出して」「フェードイン」
  「バウンス」「派手に」「お祝い演出」「12原則」「twelve-principles を使って」などで発動。
  React / 素の DOM / Astro アイランドのどれでも使える。
---

# ui-motion-principles — 12 原則ライブラリで UI に動きを付ける

## いつ使うか

- Web UI（React / Vanilla TS / Astro island）に、トランジション・マイクロインタラクション・演出を付けるとき。
- 既存の CSS transition / framer-motion 的な実装を「原則に沿った」動きに置き換えるとき。
- 派手な演出（オンボーディング、成功時のお祝い、ゲーム的 UI、LP）を作るとき。

動きの品質レビュー（監査）が目的なら監査用のスキルを、実装は本スキルを使う。

## 前提: ライブラリの導入

- 解説サイト: https://twelve-principles.iru-yo.com （全 API・ライブデモ・原則ごとの解説）。API 全量は `/reference/api/`。
- 導入は npm から:

```sh
npm i twelve-principles
```

- パッケージには本スキルも同梱される: `cp -r node_modules/twelve-principles/skills/ui-motion-principles .claude/skills/`。
- ローカルのリポジトリを試すときは `npm pack` した tarball を入れる（ディレクトリ直指定の symlink だとライブラリ側の `node_modules/react` を掴み React が二重化する）。
- import: コア `twelve-principles`、React `twelve-principles/react`（`react >= 18` が peer）。ランタイム依存ゼロ、WAAPI + `commitStyles`（Chrome 84+ / Firefox 75+ / Safari 13.1+）。

## 手順

1. **状況を分類し、API を選ぶ**（下の決定表）。自前でキーフレームを書く前に、レシピ → ビヘイビア → 原則関数の順で既存部品を探す。
2. **personality を 1 つ決める**（原則 12: Appeal）。アプリ全体を `MotionProvider` で包む。
   - 通常 UI: `natural`（既定）/ `snappy`（業務系）/ `calm`（落ち着き）/ `playful`（楽しい）— `guardrails: true`。
   - 派手な面: `bouncy` / `cartoon` — `guardrails: false`（押下の縮み最大 20%、大きな弾み）。
   - ブランド独自: `definePersonality({ ...overrides }, base)`。**モジュールスコープの定数**にする。
3. **実装する**（パターンは下記、雛形は `templates/`）。
4. **合成で質を上げる**: `anticipate`（溜め）→ `followThrough`（行き過ぎて戻る）→ `exaggerate`（振れ幅）を必要な分だけ重ねる。
5. **実機で検証する**（必須）。ブラウザでボタンを押し、再生中の `getComputedStyle(el).transform` を rAF でサンプリングして
   「溜め → 本動作 → オーバーシュート → 静止」の数値を確認する。スクリプトは `scripts/sample-motion.js`。
   reduced motion（`reducedMotion="fade"`）と、静止後にインラインスタイルが消えることも確認する。

## 決定表

| 状況 | 使う API | 主な原則 |
| --- | --- | --- |
| ボタン押下の手応え | `<Motion press>` / `usePress` / `pressable` | 1, 5 |
| ホバーで浮く | `<Motion hover>` / `useHover` / `hoverable` | 11, 6 |
| カードが視線に傾く | `<Motion tilt>` / `useTilt` / `tiltable` | 11 |
| マウント時の入場 | `<Motion enter="rise">` / `useEnter` / `play(el, enter(kind))` | 6, 9 |
| トースト・ポップオーバー・モーダルの出し入れ | `<Presence show enter exit>` / `usePresence`（退場完了後にアンマウント） | 6, 2 |
| リスト/グリッドの順次表示 | `useCascade(input, { each, cap, drag, trigger })` / `overlap` | 5, 9 |
| 主役に注目させる（モーダル、選択カード） | `useStage(active)` / `stage(el)` | 3 |
| 入力エラー | `shake()` | 5 |
| 通知・注意喚起 | `jump()` / `heartbeat()` / `tada()` / `accompany(primary, "wiggle")` | 2, 8 |
| 成功・お祝い・オンボーディング | `bounceIn` / `fallIn` / `tada` / `rubberBand` / `flip` + `bouncy`/`cartoon` | 1, 5, 10 |
| A 点から B 点へ移動 | `arc(from, to, { bend, orient })` + `travelDuration(distance)` | 7, 9 |
| トグル状態の往復（開閉など） | `useMotion().animateTo(pose)` / `setLayer`（割り込みでも飛ばない） | 6 |
| 物理シミュレーション（落下・揺れ） | `straightAhead((t, ms) => pose, { duration })` | 4a |
| 独自のキーポーズ | `poseToPose([...frames], { duration })` | 4b |

コンテキストメニューは入場アニメーションを付けない（退場のみ）。進捗バー以外で `linear` を使わない。

## 実装パターン

```tsx
// アプリ全体
<MotionProvider personality="playful">{children}</MotionProvider>

// 宣言的に
<Motion as="button" press hover enter="pop" onClick={save}>保存</Motion>
<Presence show={open} enter="pop" exit="pop" role="status">保存しました</Presence>

// イベント駆動（personality はコールバックで受け取る）
const { ref, play } = useMotion<HTMLDivElement>();
<div ref={ref} onClick={() => play((p) => jump({ personality: p }))} />

// 複数フックの ref を束ねるときは必ず useMemo
const merged = useMemo(() => mergeRefs(ref, pressRef, hoverRef), [ref, pressRef, hoverRef]);
```

```ts
// Vanilla
const off = pressable(button, { personality: "snappy" }); // 解除関数を返す
play(card, followThrough(anticipate(enter("rise")), { bounce: 0.35 }));
```

## 数値の目安（既定値に組み込み済み）

- ユーザー起点の動きは 300ms 以内（`USER_INITIATED_MAX_MS`）。押下 100ms、ホバー 200ms、入場 200ms、退場 150ms。
- スタッガーは 1 項目 50ms 以内（`MAX_STAGGER_MS`、`cap` で変更可）。派手なカスケードは `cap: 120〜150`。
- 押下の縮みは 2〜5%（guardrails 時）。squash/stretch は UI で ±5%、キャラクター的演出で 20% 以上。
- 入場 `easings.out`、退場 `easings.in`、画面内移動 `easings.inOut`、オーバーシュートは bezier ではなく `spring`。

## 落とし穴 (Pitfalls)

- **ライブラリが所有するプロパティ**: アニメーション対象要素の `transform` `transform-origin` `opacity` `filter` `box-shadow`。
  同じ要素に CSS transition やレイアウト用 transform を併用しない。必要なら外側ラッパーでレイアウト、内側で動きを分ける。
  `elevation` を含むモーション中は CSS の `box-shadow` が上書きされる。
- **1 要素 1 モーション**: 同じ要素で `play` すると前のモーションは置き換わる。同時に別の動き（例: 本体の jump と
  アイコンの wiggle）は別要素に付ける。ホバー + 押下 + 傾きの共存は `setLayer`（ビヘイビアは自動で使う）。
- **`mergeRefs` を毎レンダー作らない**（`useMemo`）。作るとアタッチ/デタッチが毎回走る。
- **`Presence` はラッパー要素（`as`、既定 `div`）を描画する**。レイアウトに影響する場合は `as` を合わせる。
- **`useCascade` は直下の子要素だけ**を動かす。再生し直すには `trigger` を変える。
- **`exit(kind)` は `enter(kind)` の開始姿勢へ戻る**（`exit("slideLeft")` は右へ去る）。去る方向を指定したいときは
  `play(el, { duration, frames: [{ x: 0, easing: easings.in }, { x: -24, opacity: 0 }] })` のように直接書く。
- **短い区間の `followThrough` は行き過ぎが小さい**。長距離移動で大きく弾ませたいなら `arc(..., { easing: spring({ bounce }) })` のように spring を easing に渡す。
- **SSR/Astro**: `useEnter` や `Presence` の初回入場はハイドレーション後に再生される。Astro では `client:visible` 島にする。
- **Expressive レシピは 300ms を超える**。ホバーや押下など日常のフィードバックには使わない。無限ループは画面外で止める
  （`el.getAnimations({ subtree: true }).forEach(a => a.pause())` を IntersectionObserver で）。
- **reduced motion**: 既定 `"auto"` は OS 設定を尊重し空間移動をフェードに置換する。`"full"` は本質的な動きに限る。
- **personality オブジェクトを JSX 内で生成しない**（毎レンダー別物になり、ビヘイビアが付け直される）。

## 運用方針

- 動きは「控えめな UI 既定値」を基本に、演出面（お祝い・オンボーディング等）だけ意図的に派手にする。
- 実装後は必ずブラウザで動作を数値確認してから報告する（ユニットテストだけで済ませない）。

## サポートファイル

- `references/api-cheatsheet.md` — 主要 API のシグネチャと既定値の早見表
- `templates/MotionButton.tsx` — press + hover + クリックで jump するボタン
- `templates/Toast.tsx` — Presence で出し入れするトースト（自動クローズ付き）
- `templates/CascadeList.tsx` — 順次表示するリスト（cap/drag 付き）
- `templates/Celebrate.tsx` — 成功時に cartoon personality で派手に祝うコンポーネント
- `scripts/sample-motion.js` — ブラウザで再生中の transform/opacity をサンプリングする検証スニペット
