---
trigger: glob
globs: 'app/components/**/*.tsx,app/pages/**/*.tsx,app/styles/**/*.css'
---
# Animation Standards

**Consistent, accessible animations using tw-animate-css, Radix lifecycle, and design tokens.**

## Animation Stack

1. **tw-animate-css** — Base utilities (`animate-in`, `fade-in`, `slide-in-from-*`)
2. **Radix data-[state]** — Overlay enter/exit lifecycle
3. **Custom keyframes** — Domain-specific animations (`@app/styles/utilities/animations.css`)
4. **Design tokens** — `duration-300`, `duration-700`, `ease-smooth`, `ease-expo`

## CSS Over JavaScript

**✅ Correct:** CSS animation classes

```tsx
<div className="animate-in fade-in duration-300" />
```

**❌ Wrong:** JavaScript-driven animations for simple transitions

```tsx
const [opacity, setOpacity] = useState(0);
useEffect(() => setOpacity(1), []);
<div style={{ opacity }} />;
```

## Radix Overlay Animations

**Use `data-[state]` attributes** for enter/exit animations on Dialogs, BottomSheets, Tooltips:

**✅ Correct:** Radix state-driven animations

```tsx
<DialogPrimitive.Overlay
  className={`
    fixed inset-0 z-70
    data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:duration-200
    data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:duration-200
  `}
/>

<DialogPrimitive.Content
  className={`
    fixed z-75
    data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom data-[state=open]:duration-200
    data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=closed]:duration-200
  `}
/>
```

**❌ Wrong:** Manual animation timing with useState

```tsx
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
  if (open) setTimeout(() => setIsVisible(true), 10);
}, [open]);
```

**Why:** Radix's `data-[state]` triggers at the correct lifecycle moment.

## Collapsible Animations

Use Radix collapsible height variables for smooth expand/collapse:

```tsx
<CollapsiblePrimitive.Content
  className={`
    overflow-hidden
    data-[state=closed]:animate-[collapsible-up_300ms_var(--ease-smooth)]
    data-[state=open]:animate-[collapsible-down_300ms_var(--ease-smooth)]
  `}
/>
```

See keyframes defined in `@app/styles/utilities/animations.css`.

## Reduced Motion

**Every animation MUST have a reduced motion fallback:**

**✅ Correct:** Motion-safe with fallback

```css
.animate-skeleton-breathe {
  animation: skeleton-breathe 2.5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .animate-skeleton-breathe {
    animation: none;
  }
}
```

**✅ Correct:** Tailwind motion-reduce

```tsx
<div className="animate-in fade-in motion-reduce:animate-none" />
```

**❌ Wrong:** No reduced motion consideration

```tsx
<div className="animate-bounce" />
```

## Duration Tokens

Use design tokens, not arbitrary values:

| Token          | Value                           | Use Case                 |
| -------------- | ------------------------------- | ------------------------ |
| `duration-300` | 300ms                           | Quick micro-interactions |
| `duration-700` | 700ms                           | Standard transitions     |
| `duration-900` | 900ms                           | Emphasis animations      |
| `ease-smooth`  | `cubic-bezier(0.4, 0, 0.2, 1)`  | General purpose          |
| `ease-expo`    | `cubic-bezier(0.16, 1, 0.3, 1)` | Snappy, delightful feel  |

**✅ Correct:** Semantic tokens

```tsx
className = "transition-all duration-700 ease-smooth";
```

**❌ Wrong:** Arbitrary values

```tsx
className = "transition-all duration-[350ms]";
```

## Custom Animations

Add domain-specific animations to `@app/styles/utilities/animations.css`:

1. Define `@keyframes`
2. Create `.animate-*` utility class
3. Add `@media (prefers-reduced-motion: reduce)` fallback
4. Use design token easings (e.g., `var(--ease-smooth)`)

**Existing animations:** `bean-pulse`, `skeleton-breathe`, `marquee`, `hero-breathe`, `waveform`, `shimmer`, `spring-in`, `coffee-pulse`, `rocket-lift`

## Quick Reference

| Pattern        | Example                                                                 |
| -------------- | ----------------------------------------------------------------------- |
| Fade in        | `animate-in fade-in duration-300`                                       |
| Slide up       | `animate-in slide-in-from-bottom duration-300`                          |
| Overlay enter  | `data-[state=open]:animate-in data-[state=open]:fade-in-0`              |
| Overlay exit   | `data-[state=closed]:animate-out data-[state=closed]:fade-out-0`        |
| Collapsible    | `data-[state=open]:animate-[collapsible-down_300ms_var(--ease-smooth)]` |
| Reduced motion | `motion-reduce:animate-none`                                            |

## References

- @app/styles/utilities/animations.css — Custom keyframes library
- @app/styles/foundations/primitives.css — Duration/easing tokens
- @app/components/ui/Dialog/Dialog.tsx — Radix overlay pattern
- @app/components/ui/BottomSheet/BottomSheet.tsx — Mobile sheet animations
- @.rulesync/rules/accessibility.md — Reduced motion requirement
