# 动效开发

> 动效工程实施层。在 React 项目中按上下文嗅探选 React Bits OSS 组件并通过 `npx shadcn add` 安装；在非 React 项目（Vue / Svelte / Astro / vanilla JS）中直接写 CSS/JS 或推荐合适的动效库。三层设计哲学：上下文优先（读代码比追问便宜）/ 视觉动效描述（用日常语言描述动效像什么，不说技术参数）/ 嗅探主驱（追问是逃生通道，不是主路径）。所有产物强制场景合规自检（DASHBOARD 场景禁止无限循环、禁止持续背景动效、禁止超大位移）；安装后按 Personality + scene 调 props（不裸用组件默认值）。 触发关键词：动效开发、动效实现、装动效组件、React Bits、reactbits、动效落地、给按钮加效果、给标题加动效、给背景加动效、hover 效果、点击效果、入场效果、滚动效果、loading 动画。 排除（反向）：不定动效调性 / Personality（用 /动效规划 先定）、不生成 Lottie JSON、不写 GLSL/WebGL shader、不覆盖非 Web 平台（Flutter / SwiftUI / Compose）、不操作 React Bits Pro 付费组件（pro.reactbits.dev 是独立 skill 范畴）。

- Skill: `ahang1598/skill-85` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add ahang1598/skill-85`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ahang1598/skill-85/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: ahang1598 (https://skillmd.com/u/ahang1598)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/ahang1598/skill-85

---


## 能力矩阵

本 Skill 的三种运行模式，可单独运行也可叠加。最常见路径：链式模式（从 /动效规划 来，按 Personality + duration palette 直接 prop overrides）→ 在 React 项目装 ReactBits 组件 → 写场景合规 CSS。

| 模式 | 触发条件 | 产出特征 |
| --- | --- | --- |
| 🟢 **独立模式** | 无 motion-plan 上下文，直接调用 | 进入 Phase 0（框架探测）→ Phase 1（参数抽取）→ Phase 2（上下文嗅探）→ 选组件 + 默认 prop（按 scene 调） |
| 🔵 **链式模式** | 检测到 `spark-output/context/motion-plan.json` | 跳过 Personality 推断；直接用 motion-plan.element_specs 作为 prop overrides；跳过 Phase 2 调性嗅探 |
| 🟣 **增强模式** | 项目同目录有 SparkDesign 组件库 / 已存在 `spark-output/profile/motion-apply.md` | Component 选型优先匹配 SparkDesign 已有组件；profile 累积 reject/accept 历史，下次选型更准 |

> 三种模式都遵守同一套场景合规红线（LOCKED #7）：DASHBOARD 场景禁 `animation: ... infinite` / 禁持续背景动效 / 禁超 2s 入场 / 禁 translate >20px / scale >1.05 / rotate >5deg。

## 输入要求

| 输入项 | 必填？ | 来源优先级 | 缺失时行为 |
| --- | --- | --- | --- |
| `project_name` | ✅ | 链式 motion-plan / brief > 用户输入 | Phase 0 追问 |
| `framework` | ✅ | 读 `package.json` 自动探测（react/next/remix/gatsby/vue/svelte/astro/vanilla） | 无 package.json + 无 web 标志 → 进入「非 Web 项目」拒绝分支 |
| `scene` | ✅ | 链式 motion-plan.scope.target_pages 推断 > brief 推断 > Phase 2 L3 嗅探（路由 / 依赖 / README 关键词） | 嗅探结果 NEUTRAL，应用 LANDING 默认但 cap 在 mid-range |
| `personality_applied` | ✅ | 链式 motion-plan.personality > Phase 2 嗅探 fallback | 链式不存在时 fallback 到 Corporate（DASHBOARD）/ Premium（LANDING） |
| `target` 元素 | ✅ | 链式 motion-plan.element_specs > 用户输入 | 用户没指明 + 页面 3+ 个候选 → AskUserQuestion 用「UI 元素 + 视觉动效描述」格式追问 |
| `path` 路径决策 | ✅ | Phase 0a/0b/4 决策树自动选 | 必出 PATH DECISION NOTIFICATION（📋 路径 / 原因 / 场景） |
| `prop_overrides` | ✅ | 链式 motion-plan.element_specs[].duration_ms + easing > scene-aware tuning 表默认值 | DASHBOARD 强制覆盖（不用组件默认值） |

**信息完整度判断**：必填项任一缺失 → 优先嗅探项目文件解决（"读代码比问问题便宜"）；嗅探无果且页面 3+ 候选 → AskUserQuestion 追问，且每个选项必须含「UI 元素 + 视觉动效描述」（禁止裸列元素名）。

## Chain Context

### 上游读取（Phase 0 之前执行）

按以下顺序尝试读取上下文，找到即提取并告知用户已沿用：

1. 扫描会话中的 `<!-- spark-context:motion-plan -->` / `<!-- spark-context:flow-web -->` / `<!-- spark-context:brief -->` marker
2. 读取项目目录 `spark-output/context/motion-plan.json` / `flow-web.json` / `flow-mobile.json` / `brief.json`
3. 读取 `spark-output/profile/motion-apply.md`（本 Skill 自己的运行时记忆，含历史选型 / 拒绝偏好 / Tailwind 版本陷阱等）
4. 都没有则跳过，按独立模式 Phase 0→4 全流程执行

可复用字段映射：

- `motion-plan.personality.archetype` → **`personality_applied.archetype`**（直接继承，不再嗅探）
- `motion-plan.brand_motion_identity.signature_easing` + `duration_palette` → `prop_overrides.easing` + `duration` 基础值
- `motion-plan.element_specs[]` → 每个 `target` 的 `prop_overrides`（duration_ms / easing / stagger_ms）
- `motion-plan.scope.target_pages` + `target_elements` → 直接消费，省去 Phase 1 追问
- `motion-plan.choreography.stagger_budget_ms` → 多组件编排总时长上限校验
- `motion-plan.reduced_motion_fallback.strategy` → 写 `@media (prefers-reduced-motion: reduce)` 时的具体策略
- `flow-web/flow-mobile.pages[]` → `scene` 辅助判定
- `brief.style` + `project_type` → `scene` 辅助判定（"产品设计" + 含 dashboard / admin → scene=dashboard）

读到 motion-plan 后告知用户："检测到动效规划上下文（Personality=[...]，[N] 个 element_specs），将直接作为 prop overrides 应用，跳过调性嗅探。"

### 下游输出（最终交付时执行，严格按 chain-protocol §2.1 Step 1→6 顺序）

1. **先写盘**：`Write` 工具把完整 JSON 写到 `spark-output/context/motion-apply.json`
2. **输出自检行**：`✅ motion-apply.json 已写盘到 spark-output/context/motion-apply.json`
3. **渲染交付摘要**（PATH DECISION NOTIFICATION + 安装组件列表 + prop overrides 表 + 写入文件列表）
4. **输出紧凑 marker**：
   ```
   <!-- spark-context:motion-apply ref="spark-output/context/motion-apply.json" -->
   动效开发已完成：project=<name>，framework=<...>，scene=<...>，installed=[<ComponentA>, <ComponentB>]，<N> 段 CSS
   <!-- /spark-context:motion-apply -->
   ```
5. **Handoff 引导**：推荐下游 `/设计走查`（check）做动效合规走查
6. **更新链路面板**（按 Avatar SKILL.md 第 140-158 行的标准流程，独立段落告知用户）

**Profile 同步**：除了 chain context，本 Skill 同时维护 `spark-output/profile/motion-apply.md`（项目级长期记忆，含 stack/scene/tonality/history/preferences/Tailwind 版本陷阱）。该文件在 Phase 5 写入，与 chain context 互补——chain 给下游 Skill 看，profile 给自己下次跑看。

### 字段流向下游

- `motion-apply.installed_components[].name` + `prop_overrides` → **/设计走查**（check）的「动效合规」走查依据（实施是否守住 motion-plan.personality）
- `motion-apply.path_decisions[]` → **/PRD** 的「工程交付清单」（哪些走组件、哪些走原生 CSS、peer deps 增加情况）
- `motion-apply.css_snippets[]` → **/设计走查**（check）的资产清点（每段 CSS 是否过 scene compliance）
- `motion-apply.profile_path` → 项目层长期记忆位置，**/项目复盘**（retro）可读取做技术决策回顾

---

# React Bits Advisor

Helps users add animations and interactive effects to any web project. For React projects, picks from the React Bits OSS catalog (130 components). For non-React projects, writes simple effects directly or recommends suitable libraries. Asks the user only when context inference is insufficient.

## When this skill triggers

- User asks for an animation, animated heading/title, animated background, scroll effect, hover/interaction polish in **any** web project
- User mentions "React Bits", "reactbits", "reactbits.dev"
- User pastes a reactbits.dev URL or a `@react-bits/...` install command

Do NOT trigger for:
- React Bits **Pro** (paid product at `pro.reactbits.dev`, separate component pool, has its own official skill)
- Generic GSAP / Framer Motion API-level authoring without component selection (use those skills instead)

## Hard constraints (LOCKED — never override)

1. **Never copy component source code — not into this skill, not into `references/`, and not manually into the user's project.**
   - Reason: React Bits is licensed MIT + Commons Clause, which prohibits redistributing components. Always fetch via the official CLI: `npx shadcn@latest add @react-bits/<name>-<variant>`.
   - If user asks "直接复制源码到我项目里" or similar: politely refuse and explain — "React Bits 使用 MIT + Commons Clause 许可，组件分发只能通过官方 CLI。我来用 `npx shadcn add` 帮你装，效果完全一样且确保合规。" If user insists after explanation, still refuse — LOCKED constraints are non-negotiable regardless of user acknowledgment.
2. **Never invent component names or URLs.** If a component name is uncertain, read `references/visual-index.md` or fall back to the official site `https://reactbits.dev/`. Encountered an unknown name? Stop and tell the user.
3. **OSS-only scope.** Only operate on `reactbits.dev` (free, MIT+CC). If a request looks like Pro (mentions blocks/templates, `pro.reactbits.dev`, or `@react-bits-pro/`), refuse and redirect: "This is React Bits Pro territory — separate paid product with its own skill. See pro.reactbits.dev."
4. **At most 3 candidates per visual clarification round.** More than that — go back and narrow via context sniffing.
5. **Verify peer-deps before install.** Heavy deps (`three`, `ogl`, `@react-three/fiber`, > 500 KB bundle impact) require explicit user confirmation; do not silently install. **Engine-coexistence check**: if the project already has one animation engine (e.g. `motion`/`framer-motion`) and the candidate component pulls a *different* engine (e.g. `gsap` + `@gsap/react` + ScrollTrigger/SplitText plugins, ~70 KB gzipped), warn the user that two engines will coexist and offer either to (a) proceed knowingly, (b) pick a same-engine alternative, or (c) consolidate by replacing the existing engine. Verified 2026-05-25: SplitText pulls full GSAP stack on a motion-only project.
6. **Never destructively edit the user's existing components.** New components are placed where the React Bits registry decides (typically `src/components/<Name>.tsx` per `components` alias in `components.json`, **not** `src/components/ui/` — that's shadcn-ui's own `ui` alias). Adapt to whatever path the install actually lands at.
7. **Scene compliance applies to ALL output — not just component selection (CRITICAL).**
   The DASHBOARD scene rules (no infinite loops, no continuous background animations, no attention-stealing effects) apply to EVERY line of code this skill outputs, regardless of which path produced it:
   - Phase 0a (direct CSS/JS) — e.g. `animation: X infinite` is FORBIDDEN in dashboard
   - Phase 0b (library recommendation + code snippet) — snippet must obey scene rules
   - Phase 4 (ReactBits component install + wiring) — props must be scene-tuned
   - Any custom code written alongside a component install
   
   **Pre-delivery self-check (LOCKED — run before showing code to user)**:
   If detected scene = DASHBOARD, scan your own output for these violations:
   - `animation: ... infinite` or `animation-iteration-count: infinite` → VIOLATION (continuous loop)
   - `@keyframes` that run forever without user trigger (no `:hover`/`:focus`/`:active`/JS event gate) → VIOLATION
   - Durations > 2s on entrance animations → likely too theatrical for dashboard
   - `transform` with large values (translate > 20px, scale > 1.05, rotate > 5deg) → disproportionate
   - Any effect that draws the eye continuously while idle → VIOLATION
   
   If a violation is found: **fix it before outputting**, don't output then apologize. The user should never see code that violates scene rules.
   
   Dashboard-safe patterns: `transition` (fires only on state change), `:hover`/`:focus` gated animations, `animation` with `forwards` fill (plays once), JS-triggered animations that fire once per interaction.

## Layered configuration model

Three categories of decisions, in priority order:

| Layer | Examples | When fixed |
|---|---|---|
| **LOCKED** (above) | CLI-only install, OSS-only, no source copy | Skill permanent |
| **USER-CONFIRMED** | Stack variant (JS/TS × CSS/TW), animation intensity preference | Per-project, sticky after first confirmation, written to `spark-output/profile/motion-apply.md` |
| **VARIES** | Specific component pick, props, where it's wired in | Per-task |

USER-CONFIRMED values are **inferred via context sniffing first**. Only ask if inference fails or signals conflict.

### Three design pillars (LOCKED — all behavior derives from these)

1. **Context-first (上下文理解)**:  The skill has full access to the user's project files. Use them. Reading the user's page code, dependencies, styles, and existing components is ALWAYS cheaper than asking a question. The skill should feel like it "understands" the project without being told. Asking is a signal that context-reading failed — minimize it.

2. **Visual animation descriptions (动效描述)**:  Animation is visual. Every time the skill mentions a component, proposes a candidate, or asks a question, it MUST describe what the animation LOOKS LIKE in everyday language — not what it IS technically. "逐词从模糊到清晰，像相机对焦" beats "BlurText with animationFrom blur 10px opacity 0". The user picks based on what they can imagine, not what API props say.

3. **Sniffing is the main driver, asking is the escape hatch (嗅探主驱)**:  The default behavior is SILENT: sniff → infer → install → explain afterwards. Asking the user is the LAST resort after both context-reading AND semantic reflection have failed. When asking IS necessary, it must be (a) grounded in their actual page, (b) include visual descriptions, (c) ask about ONE specific gap. A well-designed run touches the user exactly 0–1 times before delivery.

These three pillars are not guidelines — they are the skill's identity. Any phase logic that violates these (e.g. asking generic questions, omitting visual descriptions, ignoring available project context) is a BUG to be patched.

---

## Workflow

```
[Phase 0] Framework detection (gate)
    └─ Read package.json → detect framework
    └─ React / Next.js / Remix / Gatsby? → proceed to Phase 1 (React Bits path)
    └─ Non-React (Vue / Svelte / Astro / vanilla / etc)?
         └─ Simple effect? → write CSS/JS directly (Phase 0a)
         └─ Complex effect? → recommend libraries from references/non-react-alternatives.md (Phase 0b)
[Phase 1] Smart parameter extraction
    └─ Component name explicit in user prompt? → skip to Phase 4
    └─ Animation term ("typewriter", "glitch", "blur") explicit? → narrow Phase 2 candidates
    └─ Neither? → REFLECT on semantic intent (see Phase 1 miss rules)
    └─ Reflection fails? → ask user about functional intent BEFORE entering Phase 2
[Phase 2] Context sniffing (the main path)
    └─ High confidence → silent execution + post-hoc transparency
    └─ Medium → silent + 1-line explanation + escape hatch
    └─ Low / conflict / red-line → REFLECT on why scores are flat (see Phase 2 miss rules)
    └─ Reflection resolves? → re-score with narrowed pool
    └─ Reflection fails? → ask user the ONE differentiating question, then re-score
[Phase 3] Visual clarification (escape hatch only — entered ONLY after reflection exhausted)
    └─ ≤ 3 candidates with: name + 1-line visual + analogy + live demo URL
    └─ Always include "you pick — give me your best guess" option
    └─ > 3 candidates at entry? → reflect on what question cuts the pool, ask it
[Phase 4] Install + wire
    └─ Verify deps → run shadcn CLI → import + minimal usage example
[Phase 5] Sink to profile
    └─ Write project tonality + chosen component to spark-output/profile/motion-apply.md
```

### Global principle: REFLECT before any phase transition (LOCKED)

When ANY phase produces a "miss" (no match, low confidence, too many candidates, ambiguous signal), the skill must:
1. **Pause and reflect**: "Why did this phase not produce a clear answer? What information am I missing?"
2. **Try to self-resolve**: Can I infer the missing info from what the user already said + what I already read?
3. **If reflection resolves**: continue with the insight (no user interaction needed)
4. **If reflection fails**: ask the user ONE targeted question about the specific gap — not a generic preference question
5. **NEVER mechanically jump to the next phase** hoping it will fix itself — that's how loops form

### When asking is NECESSARY vs when context should resolve silently (LOCKED)

**Core stance: sniffing is the main driver. Asking is the ESCAPE HATCH, not the main path.**

But "escape hatch" does NOT mean "avoid asking at all costs." It means: don't ask when context already gives the answer. When context genuinely doesn't contain the answer, asking is the RIGHT thing to do — not a failure.

**Two dimensions of ambiguity** (BOTH must be resolved before proceeding):

1. **WHERE** — which UI element gets the animation? (target ambiguity)
2. **WHAT** — what visual effect happens? (outcome ambiguity)

A user input like "添加点击动效" is ambiguous on BOTH dimensions:
- WHERE: which element? (cards? buttons? nav items? list rows?)
- WHAT: what happens visually on click? (particles fly out? ripple expands? border glows? card presses down? color flashes?)

**Resolution rules:**

| WHERE clear? | WHAT clear? | Action |
|---|---|---|
| Yes (e.g. "给卡片加") | Yes (e.g. "加个涟漪") | Silent execution |
| Yes | No | Ask about WHAT — describe 2–3 visual outcomes for that element |
| No | Yes (e.g. "加个粒子点击效果") | Read page → if 1 obvious target, proceed; if multiple, ask WHERE |
| No | No | Read page → ask BOTH (combine into one question with options) |

**DO NOT ask when:**
- BOTH dimensions are clear from user words + context (e.g. "给按钮加个 Material 涟漪" → WHERE=buttons, WHAT=ripple → just do it)
- Profile records a previous choice for this exact scenario (e.g. "last time user chose ClickSpark for card clicks" → re-use unless they say otherwise)
- The user explicitly said "你帮我选" or "你看着办" → silent execution with post-hoc explanation

**DO ask when:**
- The WHAT dimension is ambiguous: user only named a trigger ("点击"/"hover"/"入场") but not the visual outcome. A trigger + no visual = must ask what it should LOOK LIKE.
- The WHERE dimension is ambiguous: page has 3+ distinct regions the effect could apply to
- Two candidate effects would produce drastically different visual outcomes and you can't tell which the user prefers
- Context and profile have no prior signal about user's style preference for this type of effect

**Critical principle: naming a TRIGGER is not the same as specifying an EFFECT.**
"点击动效" specifies the trigger (click) but NOT the visual result. The skill must not map "click" → "ClickSpark" by default. "Click animation" could mean:
- 粒子从点击点向外飞散 (ClickSpark)
- 涟漪从点击点扩散 (CSS ripple)
- 边框闪一圈星光 (StarBorder)
- 卡片整体轻微下压回弹 (CSS scale transform)
- 点击位置出现一个短暂亮点 (CSS radial-gradient flash)
- 颜色从点击点向外脉冲 (CSS color pulse)

These are COMPLETELY different experiences. Choosing one for the user without asking is presumptuous.

**How to ask — question format (LOCKED, inherits from original design)**

Every question to the user must include visual animation descriptions. Never ask bare "侧导航？标题？卡片？" — that tells the user nothing about what will happen. Instead:

```
看了你的 dashboard 页面，有几个适合加动效的位置：
- 侧导航 → 菜单项切换时向右轻滑入场，选中态柔光高亮 (AnimatedContent)
- 页面标题 → "早上好" 逐词淡入，模糊聚焦感 (BlurText, blur 3px → 0)
- 数据卡片 → hover 时表面流光扫过，点击微粒子飞溅 (GlareHover + ClickSpark)
你想先给哪个加？
```

Rules:
- Each option = **UI 元素 + 动效会是什么样子 + 对应组件名**
- Animation description must be VISUAL (用户能想象画面), not technical (不说 "opacity 0→1 transition 300ms")
- Use everyday language: "逐词淡入" "表面流光扫过" "微粒子飞溅" "轻滑入场"
- At most 3–4 options, each option is one sentence
- Options are derived from READING THE USER'S ACTUAL PAGE (not from skill's category taxonomy)
- After the user picks a target, the component selection can often be inferred silently (target → component is usually 1:1 or 1:2 at most)

---

## Phase 0: Framework detection (gate)

Before anything else, determine if this is a React project.

### Detection

Read `package.json` dependencies. React project = any of these present:
- `react` / `react-dom`
- `next` (Next.js implies React)
- `gatsby` / `remix` / `@remix-run/*`

If `package.json` doesn't exist or none of the above are found → **non-React project**.

**Non-web project gate**: If `package.json` doesn't exist AND none of these web indicators are present (no `index.html`, no `vite.config.*`, no `webpack.config.*`, no `angular.json`, no `svelte.config.*`, no `astro.config.*`, no `nuxt.config.*`), the project is likely **not a web project** (could be Flutter/Dart, iOS/Swift, Android/Kotlin, etc.). In this case:
- Do NOT proceed to Phase 0a/0b
- Output: "This skill handles web project animations (React, Vue, Svelte, Angular, vanilla HTML/JS). Your project doesn't appear to be a web project. The motion-plan skill's timing/easing principles are conceptually applicable but all concrete output is web-specific."
- Stop here — do not attempt to write CSS/JS for non-web frameworks

### Phase 0a: Simple effects — write directly

If the requested effect is **simple** (can be done in ≤ 30 lines of CSS/JS with no library), just write it. Don't recommend a library for something CSS can do natively.

**⚠️ PATH DECISION NOTIFICATION (LOCKED — must output this before writing code)**:

Whenever you choose the "write directly" path (Phase 0a) instead of installing a ReactBits component, you MUST output a clear decision notification to the user BEFORE writing code:

```
📋 路径: 直接写 CSS/JS（非 ReactBits 组件）
原因: <one sentence — e.g. "这个效果用 CSS transition 就能实现，不需要装库">
场景: <detected scene — LANDING/DASHBOARD/NEUTRAL>
```

This also applies when a REACT project's request is better served by vanilla CSS than a component (e.g. simple hover transition). The notification prevents the user from wondering "why didn't it use a ReactBits component?"

**Simple effect examples** (write directly):
- Fade-in on page load → CSS `@keyframes fadeIn` + `animation` property
- Hover scale / color transition → CSS `transition` + `:hover`
- Smooth scroll → CSS `scroll-behavior: smooth`
- Button ripple on click → small JS + CSS `::after` pseudo-element
- Staggered list entrance → CSS `@keyframes` + `animation-delay: calc(var(--i) * 0.1s)`
- Typewriter (single line, no cursor blinking) → CSS `steps()` + `overflow: hidden` + `white-space: nowrap`
- Gradient text → CSS `background-clip: text` + `linear-gradient`
- Parallax scroll (basic) → `transform: translateY(calc(var(--scroll) * 0.3))` + tiny scroll listener
- Skeleton loading shimmer → CSS `@keyframes` + `linear-gradient` moving background

**How to deliver**:
1. Output the PATH DECISION NOTIFICATION (above)
2. Write the CSS/JS code directly into the user's project files
3. Keep it minimal — no build tools, no npm install, just working code
4. Add a comment `/* animation: <what it does> */` at the top for discoverability

### Phase 0b: Complex effects — recommend libraries

If the effect is **complex** (needs timeline orchestration, scroll-driven sequences, physics simulation, 3D, or is too involved for vanilla CSS/JS), recommend a library from `references/non-react-alternatives.md`.

**Complexity signals** (any one → complex):
- User says "timeline" / "sequence" / "orchestrate" / "stagger with easing control"
- Effect involves scroll position → element animation binding with precise offsets
- 3D transforms beyond basic `perspective` + `rotateY`
- Particle systems, fluid dynamics, WebGL shaders
- SVG path morphing / path animation / shape interpolation
- Physics-based (spring, inertia, bounce with realistic damping)
- Multi-element choreography (10+ elements coordinated)

**How to deliver**:
1. Output the PATH DECISION NOTIFICATION (same format as Phase 0a: 📋 路径 + 原因 + 场景)
2. Name the recommended library and explain WHY it fits (one sentence)
3. Give a minimal install command + code snippet (≤ 20 lines) showing the exact effect
4. If the user's framework has a native solution (Vue `<Transition>`, Svelte `transition:`), prefer that over a third-party library
5. At most 2 library recommendations — one lightweight, one full-featured. Don't dump a list of 10 options.

### Framework-native solutions (always prefer over third-party)

- **Vue**: `<Transition>` / `<TransitionGroup>` for enter/leave; `@vueuse/motion` for declarative animations
- **Svelte**: `transition:fade` / `transition:fly` / `animate:flip` (built-in, zero-dep)
- **Angular**: `@angular/animations` (built-in BrowserAnimationsModule)
- **Astro**: `<ViewTransitions />` for page transitions; otherwise vanilla CSS/JS (Astro outputs static HTML)

---

## Phase 1: Smart parameter extraction

Before any sniffing, scan the user prompt for:

- **Explicit component name**: `BlurText`, `SplitText`, `ShinyText`, etc → skip directly to Phase 4 (read `references/visual-index.md` for that component's spec).
- **Explicit animation term WITH visual specificity**: see `references/decision-tree.md` for keyword → candidate mapping (e.g. "打字机效果/typewriter" → DecryptedText/TypewriterText). Use this to pre-narrow Phase 2 candidate pool from 30 to ~5. **But note**: this only works when the user described the VISUAL OUTCOME, not just the TRIGGER. "打字机效果" describes a visual (characters appear one by one); "点击动效" does NOT describe a visual (it only names the trigger).
- **Explicit reactbits.dev URL** in the prompt: extract slug → resolve component name via `references/visual-index.md` or just hand to Phase 4.
- **Explicit framing target**: "标题/heading/hero" vs "background" vs "loader" — narrows the category.

- **All parameters already specified** (fast path): If the user gave concrete values for ALL of: property, distance/amplitude, duration, easing, and target elements — this is an implementation request, not a selection request. Skip Phase 2 entirely. Route decision:
  - If the effect is achievable with simple CSS/JS (fade, slide, stagger via `animation-delay`) → write it directly (Phase 0a style output), even in a React project
  - If the effect specifically needs a ReactBits component (user named one, or the visual matches only one candidate) → go to Phase 4 with the user's explicit params as prop overrides
  - **Do NOT run sniffing to second-guess explicitly provided values.** The user is the designer; respect their numbers.

**Critical distinction — TRIGGER vs VISUAL OUTCOME:**
- "点击动效" / "hover 效果" / "入场动画" → these specify a TRIGGER only. The visual outcome (what it looks like) is unknown. **Do NOT map directly to components.** Instead, note the trigger type and proceed to ask about visual outcome.
- "打字机效果" / "涟漪扩散" / "模糊渐显" / "粒子飞溅" → these specify a VISUAL OUTCOME. Map to components via decision-tree.
- "给按钮加个 Material 涟漪" → BOTH trigger (click on button) and visual (ripple) are clear → proceed directly.

If user input is trigger-only: this is NOT a miss — it's partial information. Read the page to resolve WHERE, then ask about WHAT with grounded visual options (see question format rules above). Do not map trigger keywords to default components.

If any explicit signal hits that specifies a visual outcome, use it. **Do not ignore explicit user signals just because Phase 2 would have inferred something else.**

### Phase 1 miss — REFLECT before proceeding (LOCKED)

If none of the above extraction rules match directly, **do NOT silently skip to Phase 2 with the full unfiltered pool**. Instead, pause and reflect:

1. **Semantic inference**: What is the user actually describing functionally? Map their words to a behavior, not a keyword.
   - "点击动效" → trigger=click, visual=UNKNOWN → this is trigger-only input. Narrow category to interaction/component, but DO NOT select a specific component. Instead ask about visual outcome: "粒子飞散？涟漪扩散？边框闪光？卡片下压？"
   - "打字机效果" → trigger=entrance, visual=characters appearing sequentially → CLEAR. Map to DecryptedText/TypewriterText directly.
   - "页面切换" → trigger=route change, visual=transition between views → narrow to AnimatedContent, FadeContent, PixelTransition
   - "加载状态" → trigger=data fetching, visual=skeleton shimmer or spinner → look for loaders/skeletons
   - Rule: if user described WHAT IT LOOKS LIKE, you can narrow. If they only described WHEN it fires, you need to ask WHAT.

2. **Scene + target cross-reference**: The user often gives context that narrows category even when animation term is vague.
   - "dashboard的点击动效" → scene=DASHBOARD + target=interaction → Components category only → ClickSpark, StarBorder, GlareHover, SpotlightCard
   - "首页的背景" → scene=LANDING + target=background → Backgrounds category

3. **If reflection narrows to ≤ 10 candidates**: proceed to Phase 2 with that narrowed pool.

4. **If reflection still cannot narrow** (truly ambiguous intent — user said something like "加点动效" with zero functional specificity): **read the user's target page first, then ask a grounded question**.

   **Step A — Read the page**: Before asking, read the representative page the user is likely working on. Detection order:
   - User mentioned a specific route/page? → read that file
   - Scene = DASHBOARD? → read `app/(dashboard)/page.tsx`, `src/pages/Dashboard.tsx`, or the layout file
   - Scene = LANDING? → read `app/page.tsx`, `src/App.tsx`, `pages/index.tsx`
   - Fallback: read whatever the main entry component is

   **Step B — Identify concrete UI elements** in that page: sidebar/nav, header/title, card grid, table, form, buttons, hero section, footer, etc. You now have the user's actual UI inventory.

   **Step C — Ask with grounded options + visual animation descriptions** derived from what you actually saw in their code. Each option MUST describe the animation effect, not just the UI element:
   ```
   看了你的 dashboard 页面，有几个适合加动效的位置：
   - 侧导航 → 菜单项切换时向右轻滑入场，选中态柔光高亮
   - 页面标题 → "Good morning" 逐词从模糊到清晰聚焦，像相机对焦
   - 数据卡片 → hover 时表面流光扫过，数字从 0 滚动到真实值
   - 活动列表 → 新条目从下方轻微浮入，透明度渐显
   你想先给哪个加？
   ```

   **Rules for grounded questions (LOCKED)**:
   - Options MUST come from the user's real page structure, not generic categories
   - Each option = **UI 元素 + 动效视觉描述（用户能想象画面）+ 不超过一句话**
   - 动效描述用日常语言："逐词从模糊到清晰" "表面流光扫过" "轻微浮入" "柔光高亮"，不用技术参数
   - 3–4 options max (pick the most visually impactful candidates from the page)
   - If the page has only 1–2 obvious animation targets: skip the question, just proceed with that target
   - Never ask bare "侧导航？标题？卡片？" without describing the animation — that tells the user nothing about what will HAPPEN
   - Never ask "文字/背景/交互/过渡" in the abstract when you can read the actual page and say "侧导航/标题/卡片/表格"

   **Asking necessity check (before asking)**:
   - Re-verify: is context truly insufficient? If user said "点击动效" and the page has only one interactive region (e.g. a card grid), just animate that — don't ask.
   - The threshold: ask ONLY when the page has 3+ distinct regions that could all reasonably be the target AND the user's words don't favor one over others.

   This is the key difference from a generic skill: **we have access to the user's code, so our questions should reflect their reality, not our category taxonomy.**

**Never enter Phase 2 with the full unfiltered pool (45+ candidates).** Phase 2's scoring only works well when the pool is ≤ 10–15. Larger pools produce flat score distributions that inevitably escalate to Phase 3 and loop.

---

## Phase 2: Context sniffing (main driver)

### Sniffing order (progressive — stop at first sufficient signal)

1. `spark-output/profile/motion-apply.md` (project profile from previous runs)
2. `package.json` (deps, description, framework)
3. `tailwind.config.{js,ts,mjs,cjs}` or `tailwind.config` in CSS (theme.colors, fontFamily)
4. Representative page: `app/page.tsx` / `src/App.tsx` / `pages/index.tsx`
5. `tsconfig.json` (TS yes/no; strict mode for tone hint)
6. `globals.css` / root style file (border-radius defaults, font imports)
7. `README.md` first 30 lines (project semantic — marketing vs dashboard vs portfolio)
8. Existing `src/components/ui/` (already-installed React Bits components → consistency signal)

### Four-layer signal matrix

```
L1 Tech stack (hardest, binary)
    - tsconfig.json present → variant TS, else JS
    - tailwind.config.* present → variant TW, else CSS
    - existing animation engine in deps:
        framer-motion / motion → prefer "motion" engine components
        gsap → prefer GSAP engine components
        three, @react-three/fiber → 3D-capable already
        ogl → low-level WebGL already
        none → recommend lightest-weight engine first

L2 Visual tonality (medium hard, requires aggregation)
    - Tailwind theme.colors:
        mostly grayscale + 1 accent → restrained / professional
        saturated multi-color palette → playful / vibrant
        neon (cyan/magenta) → cyber / tech
    - Font family:
        Inter / Geist / Söhne / system → modern restrained
        Playfair / Cormorant / serif → editorial elegant
        JetBrains Mono / Fira Code → developer / technical
        Pacifico / Caveat / handwritten → casual playful
    - Border radius default in globals.css:
        0–4 px → sharp / minimal
        8–12 px → mainstream modern
        16+ px → friendly / rounded

L3 Project semantics + scene type (soft, high value, careful)
    - **Scene detection** (NEW — determines animation intensity ceiling):
        Landing signals: route `/(marketing)/`, `/landing`, `/hero`, `/(public)/`;
            package.json keywords "landing"/"showcase"/"agency"/"portfolio";
            README mentions "landing page"/"showcase"/"marketing site";
            large hero sections, full-viewport backgrounds, few interactive forms.
            → scene = LANDING (high-impact tolerance)
        Dashboard signals: route `/(dashboard)/`, `/admin`, `/settings`, `/console`, `/(app)/`;
            deps include data-table / chart / form libs (react-hook-form, zod, @tanstack/react-table, recharts);
            README mentions "dashboard"/"admin"/"CRM"/"ERP"/"internal"/"management"/"backoffice";
            heavy use of table/form/sidebar layout components.
            → scene = DASHBOARD (low-impact ceiling)
        Both present: route groups like `app/(marketing)/` AND `app/(dashboard)/`
            → ask user which part they're working on RIGHT NOW (this is a valid question, not low-value)
        Neither detected: → scene = NEUTRAL (no filtering, fall through to tone-only)

    - **Scene → intensity ceiling mapping**:
        LANDING:   allow all intensities (dramatic, intense, showcase, edgy — all ok)
        DASHBOARD: ceiling = subtle/functional. Hard-filter components tagged scene=landing.
                   Prefer: short-duration, non-looping, user-triggered-only animations.
                   Tolerate: micro-interactions (ClickSpark), functional transitions (FadeContent, AnimatedContent),
                             data visualisation polish (CountUp, Counter), subtle hover feedback (GlareHover, SpotlightCard).
                   Reject: continuous background animations (Hyperspeed, Plasma), dramatic entrances (SplitText with GSAP),
                           heavy 3D (DomeGallery, ModelViewer), attention-stealing loops (Ribbons, Lightning).
        NEUTRAL:   no scene filter, rely on tone matching only.

    - package.json description / README headline (non-scene semantics, still used for tone):
        "portfolio" / "creative" → bold ok
        "docs" / "blog" → informational / restrained

L4 Existing convention (softest, highest authority)
    - Already-used React Bits components → strong consistency pull
    - spark-output/profile/motion-apply.md preferences
    - Project styleguide.md / CONTRIBUTING.md (if present)
```

### Scoring formula

```
candidate_score = Σ (signal_weight × signal_credibility)

L1 weight: 4 (engine match / mismatch is decisive)
L2 weight: 2
L3 weight: 3
L4 weight: 5 (consistency wins)

Credibility 0.0–1.0 based on signal clarity:
  - explicit dep / explicit token: 1.0
  - aggregated heuristic (multiple weak signals): 0.6–0.8
  - single weak signal: 0.3–0.5
```

### Confidence thresholds

| Top score | Lead over #2 | Action |
|---|---|---|
| ≥ 8.0 | ≥ 2.0 | **Silent execution** + post-hoc transparency note (see template below) |
| 6.0–7.9 | ≥ 1.0 | **Silent + 1-line explanation in delivery** |
| < 6.0 OR lead < 1.0 | — | **REFLECT then escalate** (see below) |

### Phase 2 miss — REFLECT before escalating (LOCKED)

When scoring lands in the "< 6.0 / lead < 1.0" bucket, do NOT mechanically jump to Phase 3. First reflect:

1. **Why are scores flat?** Common causes:
   - Candidate pool too large (Phase 1 didn't narrow enough) → the scoring is diluted. Reflect: can I re-read the user's words and extract a functional category NOW that I missed in Phase 1? If yes, re-narrow the pool and re-score. This is NOT "looping back to Phase 2" — it's completing Phase 1 reflection mid-stream.
   - Multiple candidates are genuinely equivalent for this project (e.g. ClickSpark and StarBorder both work for "click animation" in this project) → this is fine. Pick the top 2–3 and proceed to Phase 3 as a genuine choice question.
   - Scoring signals conflict (red line #1) → acknowledge the conflict and ask the user about the specific conflict, not a generic preference.

2. **Can I answer "why is the user asking for THIS?" from context?**
   - The user said "点击动效" on a dashboard → they probably want subtle click feedback on buttons/cards → ClickSpark (particle burst) or GlareHover (shine on hover+click). I can infer this without asking.
   - If I can narrow to 1–2 clear winners through this reasoning: proceed to silent execution.
   - If I genuinely can't tell (e.g. "加个动效" with no target specified): **read the target page** (same as Phase 1 miss Step A–C) and ask a grounded question based on what UI elements actually exist in their code. The question references their real page structure — "你的 dashboard 里有侧边栏、数据卡片、活动列表，你想给哪个加？" — not abstract categories.

3. **Escalation to Phase 3 is the LAST resort**, not the default. The escalation question must be about the specific ambiguity I identified during reflection, not a generic "pick one of these 3".

### Red lines (must escalate regardless of score)

1. Top-2 signals contradict each other (e.g. marketing copy + minimalist palette)
2. No L2 OR L3 signal at all (brand-new empty project — no personality to infer). **Important**: this means Phase 2 is exhausted; do NOT loop back from Phase 3 to Phase 2 — use the circuit breaker path instead.
3. User has previously rejected this exact recommendation (read profile)
4. Top candidate requires a heavy new dep (`three`, `ogl`, > 500 KB) and project doesn't already have it
5. Top-2 candidates are visually drastically different (e.g. restrained vs glitch) and scores are within 1 point
6. **Scene mismatch**: candidate is tagged `scene: landing` but detected project scene is DASHBOARD (or vice versa). This is a hard filter, not a soft penalty — skip the candidate entirely rather than penalizing its score.

### Progressive sniffing

First pass: read only files 1–3 (profile, package.json, tailwind.config). If that gives ≥ 70% confidence on the variant + tonality, stop. Only deepen sniffing if Phase 3 is escalated or user pushes back.

### Post-hoc transparency template (used in silent-execution mode)

After installing, deliver with this 3-element sign-off:

```
✅ Installed <ComponentName>.

I picked it because <signal evidence — concrete: "your project uses Geist + neutral palette + framer-motion already, so I went with the motion-engine restrained option">.

If you want a more <opposite-direction adjective> alternative, say the word and I'll swap to <ComponentName-B> or <ComponentName-C>.
```

This satisfies "user無感 but can still correct" — invisible decision, visible reasoning, easy reversal.

---

## Phase 3: Visual clarification (escape hatch)

Trigger only when Phase 2 lands in the "< 6.0 / red line" bucket. Use `AskUserQuestion`.

### Question template (LOCKED format)

- Question: "<concrete framing — e.g. '标题动画的感觉更偏向哪种？'>"
- Options: 1–3, each with:
  - Component name (technical anchor)
  - One-line visual description (≤ 15 chars Chinese / ≤ 25 words English)
  - Mass-culture analogy (movie / brand / product reference)
  - Live demo URL: `https://reactbits.dev/<category>/<slug>`
- Always include an implicit fourth: "Other" (auto-provided by AskUserQuestion) for "you pick — match my project tonality."

### Anti-overload rules (LOCKED)

- Max 3 substantive candidates per question
- After 2 clarification rounds without convergence → drop to **silent best-guess** mode and tell the user "I'll go with <X> based on your project — replace if it doesn't fit"

### Phase 3 miss — too many candidates? REFLECT, don't loop (LOCKED)

If the candidate pool reaching Phase 3 is > 3, this means upstream (Phase 1 + Phase 2) failed to narrow sufficiently. The old rule "go back to Phase 2 and re-narrow" is **REMOVED** — it causes infinite loops when Phase 2 has no new information to offer.

Instead, when candidates > 3 at Phase 3 entry:

1. **Reflect on WHY the pool is still large.** It's almost always because the user's functional intent wasn't pinned down. Ask yourself: "What is the ONE piece of information that, if I knew it, would cut the pool from N to 2–3?"
   - Usually: the animation TARGET (which UI element on their page?)
   - Sometimes: the intensity preference (subtle feedback vs dramatic effect)
   - Rarely: the engine preference (this should come from project sniffing)

2. **Read the user's page and ask a grounded question.** Same principle as Phase 1 miss: read their actual target page, identify the UI elements present, and frame the question around what they HAVE.
   - GOOD: "看了你的 dashboard，有侧导航、标题区、3 个数据卡片和一个活动列表。你想给哪个加动效？"
   - BAD: "你想给哪类元素加点击效果？按钮、卡片、还是整个区域？" (too generic — doesn't reflect their actual page)
   - The user's page IS the source of options. Never invent UI elements they don't have.

3. **After the user answers, re-enter Phase 2 scoring with the newly narrowed pool.** This is NOT a "loop" — it's a single directed retry with genuinely new information from the user.

4. **If after 1 user-answer the pool is still > 3**: force-rank by popularity within the narrowed category and present top 3. Never ask more than 2 rounds total.

### Never-loop guarantee

The skill must NEVER re-enter a phase it has already completed without new information. "New information" means either (a) user provided an answer, or (b) a file was read that wasn't read before. Re-entering Phase 2 with the same files and same user input is FORBIDDEN — that's a loop, not a retry.

### Visual rendering tiers (v0.1 → v0.3)

| Tier | When | How |
|---|---|---|
| v0.1 (now) | Always | Live demo URL link in the option (user clicks to see real render on reactbits.dev) |
| v0.2 (la

…(truncated)
