# Loopwind

> Generate images and videos from React + Tailwind CSS templates using the loopwind CLI.

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

---


# loopwind

A CLI tool for generating images and videos from JSX templates using Tailwind CSS and Satori. Templates live in a `.loopwind/` directory alongside your codebase.

## Quick Start

Loopwind is a CLI tool for generating images and videos with React and Tailwind CSS. It's designed to be used with AI Agents and Cursor.

### Installation

```bash
curl -fsSL https://loopwind.dev/install.sh | bash
```

This installs loopwind to `~/.loopwind/` and adds the `loopwind` command to your PATH. Requires Node.js 18+.

### Initialize in Your Project

Navigate to any project folder and run:

```bash
loopwind init
```

This creates `.loopwind/loopwind.json` — a configuration file with your project's theme colors.

### Install AI Skill

Give your AI agent expertise in loopwind:

```bash
npx skills add https://loopwind.dev/skill.md
```

This installs a skill that teaches Claude Code (or other AI agents) how to create templates, use animation classes, and render images/videos.

### Use with Claude Code

With the loopwind skill installed, Claude has deep knowledge of template structure, animation classes, and Tailwind CSS patterns for Satori. Just ask:

```
Create an OG image for my blog post about TypeScript tips
```

```
Create an animated intro video for my YouTube channel
```

Claude will create optimized templates and render the final output automatically.

### Install a Template

#### 1. Official Templates

```bash
loopwind add image-template
loopwind add video-template
```

Templates are installed to: `.loopwind/<template>/`

**Benefits:**
- Templates are local to your project
- Version controlled with your project
- Easy to share within your team

### Render a Template

```bash
loopwind render template-name '{"title":"Hello World","subtitle":"Built with loopwind"}'
```
or use a local props file:

```bash
loopwind render template-name props.json
```
## Commands

### `loopwind add <source>`

Install a template from various sources:

```bash
# Official templates
loopwind add image-template
loopwind add video-template
```
These will be downloaded to `.loopwind/<template>/`

### `loopwind list`

List all installed templates:

```bash
loopwind list
```

### `loopwind render <template> <props> [options]`

Render an image or video:

```bash
# Image with inline props
loopwind render banner-hero '{"title":"Hello World"}'

# Video with inline props
loopwind render video-intro '{"title":"Welcome"}'

# Using a props file
loopwind render banner-hero props.json

# Custom output
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png

# Different format
loopwind render banner-hero '{"title":"Hello"}' --format jpeg
```

Options:
- `--out, -o` - Output filename (default: `<template>.<ext>` in current directory)
- `--format` - Output format: `png`, `jpeg`, `svg` (images only)
- `--quality` - JPEG quality 1-100 (default: 92)

### `loopwind validate <template>`

Validate a template:

```bash
loopwind validate banner-hero
```

Checks:
- Template file exists and is valid React
- `export const meta` exists and is valid
- Required props are defined
- Fonts exist (if specified)

### `loopwind init`

Initialize loopwind in a project:

```bash
loopwind init
```

Creates `.loopwind/loopwind.json` configuration file with your project's design tokens.

## Animation Classes (Video Only)

Use Tailwind-style animation classes - no manual calculations needed:

```tsx
// Fade in: starts at 0ms, lasts 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>

// Loop: ping effect every 500ms
<div style={tw('loop-ping/500')} />

// Combined with easing
<h1 style={tw('ease-out enter-bounce-in-up/0/600')}>Title</h1>
```

See [Animation](/animation) for the complete reference.

## Next Steps

- [Templates](/templates)
- [Embedding Images](/images)
- [Animation](/animation)
- [Helpers (QR, Template Composition)](/helpers)
- [Styling with Tailwind & shadcn/ui](/styling)
- [Custom Fonts](/fonts)
- [AI Agent Integration](/agents)



# Templates

Templates are React components that define your images and videos. They use Tailwind CSS for styling and export metadata that loopwind uses for rendering.

## Installing Templates

### Official Templates

```bash
loopwind add image-template
loopwind add video-template
```

Templates are installed to `.loopwind/<template-name>/`.

### Direct URLs

```bash
loopwind add https://example.com/templates/my-template.json
```

### Local Filesystem

```bash
loopwind add ./my-templates/banner-hero
loopwind add /Users/you/templates/social-card
```

---

## Image Templates

### Basic Structure

```tsx
// .loopwind/banner-hero/template.tsx
export const meta = {
  name: "banner-hero",
  type: "image",
  description: "Hero banner with gradient background",
  size: { width: 1600, height: 900 },
  props: { title: "string", subtitle: "string" }
};

export default function BannerHero({ title, subtitle, tw }) {
  return (
    <div style={tw('flex flex-col justify-center items-center w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>
      <h1 style={tw('text-7xl font-bold text-white mb-4')}>
        {title}
      </h1>
      <p style={tw('text-2xl text-white/80')}>
        {subtitle}
      </p>
    </div>
  );
}
```

### Rendering Images

```bash
# Render with inline props
loopwind render banner-hero '{"title":"Hello World","subtitle":"Welcome"}'

# Custom output name
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png

# Different format
loopwind render banner-hero '{"title":"Hello"}' --format jpeg --quality 95

# Use a props file
loopwind render banner-hero props.json
```

### Output Formats

| Format | Best For |
|--------|----------|
| **PNG** (default) | Transparency, sharp text, logos |
| **JPEG** | Photographs, gradients, smaller files |
| **SVG** | Vector graphics, scalable designs |

---

## Video Templates

### Basic Structure

```tsx
// .loopwind/video-intro/template.tsx
export const meta = {
  name: "video-intro",
  type: "video",
  description: "Animated intro with bounce-in title",
  size: { width: 1920, height: 1080 },
  video: { fps: 30, duration: 3 },
  props: { title: "string" }
};

export default function VideoIntro({ tw, title }) {
  return (
    <div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>
      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/600')}>
        {title}
      </h1>
    </div>
  );
}
```

### Rendering Videos

```bash
# Render with inline props
loopwind render video-intro '{"title":"Welcome!"}' --out intro.mp4

# Faster encoding with FFmpeg
loopwind render video-intro '{"title":"Welcome!"}' --ffmpeg

# Higher quality (lower CRF = better)
loopwind render video-intro '{"title":"Welcome!"}' --crf 18
```

### FPS and Duration

```tsx
video: { fps: 30, duration: 3 }  // 90 frames total
```

| FPS | Use Case |
|-----|----------|
| **24** | Cinematic look, smaller files |
| **30** | Standard web video |
| **60** | Smooth animations |

### Video-Specific Props

Templates receive these additional props:

- **`frame`** - Current frame number (0 to totalFrames - 1)
- **`progress`** - Animation progress from 0 to 1

```tsx
export default function MyVideo({ frame, progress }) {
  // frame: 0, 1, 2, ... 89 (for 3s @ 30fps)
  // progress: 0.0 at start, 0.5 at middle, 1.0 at end
}
```

### Encoding Options

| Encoder | Command | Use Case |
|---------|---------|----------|
| **WASM** (default) | `loopwind render ...` | CI/CD, no dependencies |
| **FFmpeg** | `loopwind render ... --ffmpeg` | Faster, smaller files |

Install FFmpeg: `brew install ffmpeg` (macOS)

---

## Animation Classes

Use Tailwind-style animation classes for videos:

```tsx
// Enter animations: enter-{type}/{delay}/{duration}
<h1 style={tw('enter-fade-in/0/500')}>Fade in at start</h1>
<h1 style={tw('enter-bounce-in-up/300/400')}>Bounce in after 300ms</h1>

// Exit animations: exit-{type}/{start}/{duration}
<div style={tw('exit-fade-out/2500/500')}>Fade out at 2.5s</div>

// Loop animations: loop-{type}/{duration}
<div style={tw('loop-float/1000')}>Continuous floating</div>
<div style={tw('loop-spin/1000')}>Spinning</div>

// Easing
<h1 style={tw('ease-out enter-slide-left/0/500')}>Smooth slide</h1>
```

See the full [Animation documentation](/animation) for all classes.

---

## Common Sizes

### Social Media
- **Twitter/X Card**: 1200x675
- **Facebook/OG**: 1200x630
- **Instagram Post**: 1080x1080
- **LinkedIn Post**: 1200x627

### Web Graphics
- **Hero Banner**: 1920x1080
- **Blog Header**: 1600x900
- **Thumbnail**: 640x360

---

## Example Templates

### Open Graph Image

```tsx
export const meta = {
  name: "og-image",
  type: "image",
  size: { width: 1200, height: 630 },
  props: { title: "string", description: "string" }
};

export default function OGImage({ tw, image, title, description }) {
  return (
    <div style={tw('flex w-full h-full bg-white')}>
      <div style={tw('flex-1 flex flex-col justify-between p-12')}>
        <img src={image('logo.svg')} style={tw('h-12 w-auto')} />
        <div>
          <h1 style={tw('text-5xl font-bold text-gray-900 mb-4')}>{title}</h1>
          <p style={tw('text-xl text-gray-600')}>{description}</p>
        </div>
        <p style={tw('text-gray-400')}>yoursite.com</p>
      </div>
    </div>
  );
}
```

### Animated Intro

```tsx
export const meta = {
  name: "animated-intro",
  type: "video",
  size: { width: 1920, height: 1080 },
  video: { fps: 60, duration: 3 },
  props: { title: "string", subtitle: "string" }
};

export default function AnimatedIntro({ tw, title, subtitle }) {
  return (
    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-background')}>
      <h1 style={tw('text-8xl font-bold text-foreground ease-out enter-bounce-in-up/0/400')}>
        {title}
      </h1>
      <p style={tw('text-2xl text-muted-foreground mt-4 ease-out enter-fade-in-up/300/400')}>
        {subtitle}
      </p>
    </div>
  );
}
```

---

## Next Steps

- [Layouts](/layouts) - Wrap templates with reusable layouts
- [Embedding Images](/images) - Using the `image()` helper
- [Animation](/animation) - Full animation reference
- [Styling](/styling) - Tailwind & shadcn/ui integration
- [Fonts](/fonts) - Custom fonts


# Layouts

Layouts let you wrap templates with consistent headers, footers, and styling. A child template specifies a layout in its meta, and the layout receives the child content as a `children` prop.

## Basic Usage

### Layout Template

Create a layout template that receives `children`:

```tsx
// .loopwind/base-layout/template.tsx
export const meta = {
  name: 'base-layout',
  type: 'image',
  size: { width: 1200, height: 630 },
  props: {},
};

export default function BaseLayout({ tw, children }) {
  return (
    <div style={tw('flex flex-col w-full h-full bg-background')}>
      {/* Header */}
      <div style={tw('flex items-center px-8 py-4 border-b border-border')}>
        <span style={tw('text-2xl font-bold text-primary')}>loopwind</span>
      </div>

      {/* Content slot */}
      <div style={tw('flex flex-1')}>
        {children}
      </div>

      {/* Footer */}
      <div style={tw('flex items-center justify-between px-8 py-4 border-t border-border')}>
        <span style={tw('text-muted-foreground')}>loopwind.dev</span>
      </div>
    </div>
  );
}
```

### Usage in Templates

Reference the layout using a relative path:

```tsx
// .loopwind/blog-post/template.tsx
export const meta = {
  name: 'blog-post',
  type: 'image',
  layout: '../base-layout', // Layout controls size
  props: {
    title: 'string',
    excerpt: 'string',
  },
};

export default function BlogPost({ tw, title, excerpt }) {
  return (
    <div style={tw('flex flex-col justify-center p-12')}>
      <h1 style={tw('text-5xl font-bold text-foreground mb-4 text-balance')}>
        {title}
      </h1>
      <p style={tw('text-xl text-muted-foreground leading-relaxed')}>
        {excerpt}
      </p>
    </div>
  );
}
```

### Render

```bash
loopwind render blog-post '{"title":"Hello World","excerpt":"My first post"}'
```

The output uses the layout's size (1200x630) with the child content inside.

---

## Key Concepts

### Size

When using a layout, the **layout's size** controls the final output dimensions. The child template doesn't need a `size` property.

### Path Resolution

Use relative paths to reference layouts:

```tsx
layout: '../base-layout'      // Sibling directory
layout: './shared/layout'     // Subdirectory
layout: '../../layouts/main'  // Parent's sibling
```

### Props Flow

The layout receives:
- All standard helpers (`tw`, `image`, `qr`, `template`, etc.)
- `children` prop containing the rendered child content
- Animation context (`frame`, `progress`) for video layouts

```tsx
export default function Layout({ tw, children, frame, progress }) {
  // tw, image, qr, template, path, textPath all available
  return (
    <div style={tw('flex w-full h-full')}>
      {children}
    </div>
  );
}
```

---

## Video Layouts

Layouts work with video templates. Both the layout and child can use animations:

```tsx
// .loopwind/video-layout/template.tsx
export const meta = {
  name: 'video-layout',
  type: 'video',
  size: { width: 1920, height: 1080 },
  video: { fps: 60, duration: 4 },
  props: {},
};

export default function VideoLayout({ tw, children }) {
  return (
    <div style={tw('flex flex-col w-full h-full bg-background')}>
      {/* Animated header */}
      <div style={tw('flex items-center px-12 py-6 ease-out enter-slide-down/0/500')}>
        <span style={tw('text-3xl font-bold text-primary')}>loopwind</span>
      </div>

      {/* Content */}
      <div style={tw('flex flex-1')}>
        {children}
      </div>

      {/* Animated footer */}
      <div style={tw('flex px-12 py-6 ease-out enter-fade-in/500/400')}>
        <span style={tw('text-muted-foreground')}>loopwind.dev</span>
      </div>
    </div>
  );
}
```

---

## Example: Consistent OG Images

Create a layout for all your OG images:

```tsx
// .loopwind/og-layout/template.tsx
export const meta = {
  name: 'og-layout',
  type: 'image',
  size: { width: 1200, height: 630 },
  props: {},
};

export default function OGLayout({ tw, image, children }) {
  return (
    <div style={tw('flex w-full h-full bg-background')}>
      {/* Content area */}
      <div style={tw('flex flex-col flex-1 p-12')}>
        {/* Logo */}
        <div style={tw('flex items-center gap-3 mb-auto')}>
          <img src={image('logo.svg')} style={tw('h-10 w-auto')} />
          <span style={tw('text-2xl font-bold')}>MyBrand</span>
        </div>

        {/* Slot for page-specific content */}
        <div style={tw('flex flex-1 items-center')}>
          {children}
        </div>

        {/* Domain */}
        <span style={tw('text-muted-foreground mt-auto')}>mybrand.com</span>
      </div>
    </div>
  );
}
```

Then create page-specific templates:

```tsx
// .loopwind/og-blog/template.tsx
export const meta = {
  name: 'og-blog',
  type: 'image',
  layout: '../og-layout',
  props: {
    title: 'string',
    author: 'string',
  },
};

export default function OGBlog({ tw, title, author }) {
  return (
    <div style={tw('flex flex-col')}>
      <span style={tw('text-sm text-muted-foreground uppercase tracking-wider mb-2')}>
        Blog Post
      </span>
      <h1 style={tw('text-4xl font-bold text-foreground mb-4 text-balance')}>
        {title}
      </h1>
      <span style={tw('text-muted-foreground')}>By {author}</span>
    </div>
  );
}
```

---

## Next Steps

- [Templates](/templates) - Template structure and metadata
- [Animation](/animation) - Animation classes for video layouts
- [Helpers](/helpers) - Using image(), qr(), and template()


# Embedding Images

Use the `image()` helper to embed images in your templates. It supports loading from props, template directories, and URLs.

## Prop-based Images

Pass the prop name to load an image path from props:

```tsx
export const meta = {
  name: "product-card",
  type: "image",
  size: { width: 1200, height: 630 },
  props: {
    title: "string",
    background: "string?"
  }
};

export default function ProductCard({ tw, image, title, background }) {
  // Use fallback if no background prop provided
  const bgSrc = background
    ? image('background')
    : 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200';

  return (
    <div style={tw('relative w-full h-full')}>
      <img
        src={bgSrc}
        style={tw('absolute inset-0 w-full h-full object-cover')}
      />
      <div style={tw('relative z-10 p-12')}>
        <h1 style={tw('text-6xl font-bold text-white')}>{title}</h1>
      </div>
    </div>
  );
}
```

The `image('background')` helper loads from the `background` prop value (file path or URL).

## Direct File Images

Load images directly from your template directory by including the file extension:

```tsx
export default function ChangelogItem({ tw, image, text }) {
  return (
    <div style={tw('flex items-center gap-4')}>
      {/* Load check.svg from template directory */}
      <img
        src={image('check.svg')}
        style={tw('w-6 h-6')}
      />
      <span style={tw('text-lg')}>{text}</span>
    </div>
  );
}
```

You can also use subdirectories:

```tsx
<img src={image('assets/icons/star.svg')} />
<img src={image('shared/logo.png')} />
```

**Template directory structure:**
```
.loopwind/my-template/
├── template.tsx
├── check.svg           ← image('check.svg')
└── assets/
    └── icons/
        └── star.svg    ← image('assets/icons/star.svg')
```

## URLs

The `image()` helper also supports loading images from URLs:

```json
{
  "background": "https://example.com/image.jpg"
}
```

## Supported Formats

- **JPEG** (`.jpg`, `.jpeg`)
- **PNG** (`.png`)
- **GIF** (`.gif`)
- **WebP** (`.webp`)
- **SVG** (`.svg`)

## Image Positioning

Use Tailwind's object-fit utilities:

```tsx
export default function ImageGrid({ tw, image, img1, img2, img3 }) {
  return (
    <div style={tw('flex gap-4 w-full h-full p-8 bg-gray-100')}>
      {/* Cover - fills entire area, may crop */}
      <img
        src={image('img1')}
        style={tw('w-full h-full object-cover rounded-lg')}
      />

      {/* Contain - fits within area, may letterbox */}
      <img
        src={image('img2')}
        style={tw('w-full h-full object-contain')}
      />

      {/* Fill - stretches to fill */}
      <img
        src={image('img3')}
        style={tw('w-full h-full object-fill')}
      />
    </div>
  );
}
```

## Troubleshooting

### Images Not Loading

Check file paths are relative to the props file:

```json
{
  "background": "./images/bg.jpg"
}
```

Absolute paths won't work.

### Optimize Image Sizes

Use appropriately sized images before embedding:

```bash
convert large-image.jpg -resize 1600x900 optimized.jpg
```

---

## Next Steps

- [Templates](/templates) - Creating image and video templates
- [Animation](/animation) - Animation classes for videos
- [Styling](/styling) - Tailwind & shadcn/ui integration


# Animation

loopwind provides **Tailwind-style animation classes** that work with time to create smooth video animations without writing custom code.

> **Note:** Animation classes only work with **video templates** and **GIFs**. For static images, animations will have no effect since there's no time context.

## Quick Start

```tsx
export default function MyVideo({ tw, title, subtitle }) {
  return (
    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-black')}>
      {/* Bounce in from below: starts at 0, lasts 400ms */}
      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400')}>
        {title}
      </h1>

      {/* Fade in with upward motion: starts at 300ms, lasts 400ms */}
      <p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/300/400')}>
        {subtitle}
      </p>

      {/* Continuous floating animation: repeats every 1s (1000ms) */}
      <div style={tw('mt-8 text-4xl loop-float/1000')}>
        ⬇️
      </div>
    </div>
  );
}
```

## Animation Format

loopwind uses three types of animations with **millisecond timing**:

| Type | Format | Description |
|------|--------|-------------|
| Enter | `enter-{type}/{start}/{duration}` | Animations that play when entering |
| Exit | `exit-{type}/{start}/{duration}` | Animations that play when exiting |
| Loop | `loop-{type}/{duration}` | Continuous looping animations |

All timing values are in **milliseconds** (1000ms = 1 second).

## Utility-Based Animations

In addition to predefined animations, loopwind supports **Tailwind utility-based animations** that let you animate any transform or opacity property directly:

```tsx
// Slide in 20px from the left
<div style={tw('enter-translate-x-5/0/1000')}>Content</div>

// Rotate 90 degrees on entrance
<div style={tw('enter-rotate-90/0/500')}>Spinning</div>

// Fade to 50% opacity in a loop
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>

// Scale down with negative value
<div style={tw('enter--scale-50/0/800')}>Shrinking</div>
```

### Supported Utilities

| Utility | Format | Description | Example |
|---------|--------|-------------|---------|
| **translate-x** | `enter-translate-x-{value}` | Translate horizontally | `enter-translate-x-5` = 20px<br/>`enter-translate-x-full` = 100%<br/>`enter-translate-x-[20px]` = 20px |
| **translate-y** | `enter-translate-y-{value}` | Translate vertically | `loop-translate-y-10` = 40px<br/>`enter-translate-y-1/2` = 50%<br/>`enter-translate-y-[5rem]` = 80px |
| **opacity** | `enter-opacity-{n}` | Set opacity (0-100) | `enter-opacity-50` = 50% |
| **scale** | `enter-scale-{n}` | Scale element (0-200) | `enter-scale-100` = 1.0x |
| **rotate** | `enter-rotate-{n}` | Rotate in degrees | `enter-rotate-45` = 45° |
| **skew-x** | `enter-skew-x-{n}` | Skew on X axis in degrees | `enter-skew-x-12` = 12° |
| **skew-y** | `enter-skew-y-{n}` | Skew on Y axis in degrees | `exit-skew-y-6` = 6° |

**Translate value formats:**
- **Numeric**: `5` = 20px (Tailwind spacing scale: 1 unit = 4px)
- **Keywords**: `full` = 100%
- **Fractions**: `1/2` = 50%, `1/3` = 33.333%, `2/3` = 66.666%, etc.
- **Arbitrary values**: `[20px]`, `[5rem]`, `[10%]` (rem converts to px: 1rem = 16px)

All utilities work with:
- **All prefixes**: `enter-`, `exit-`, `loop-`, `animate-`
- **Negative values**: Prefix with `-` (e.g., `-translate-x-5`, `-rotate-45`)
- **Timing syntax**: Add `/start/duration` (e.g., `enter-translate-x-5/0/800`)

### Translate Animations

```tsx
// Numeric (Tailwind spacing): 20px (5 * 4px)
<div style={tw('enter-translate-x-5/0/500')}>Content</div>

// Keyword: Full width (100%)
<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>

// Fraction: Half width (50%)
<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>

// Arbitrary values: Exact px or rem
<div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div>
<div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div>

// Loop with fractions
<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div>

// Negative values
<div style={tw('exit--translate-y-8/2000/500')}>Rising</div>
```

### Opacity Animations

```tsx
// Fade to 100% opacity
<div style={tw('enter-opacity-100/0/500')}>Fading In</div>

// Fade to 50% opacity
<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>

// Pulse between 50% and 100%
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>

// Fade out to 0%
<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>
```

### Scale Animations

```tsx
// Scale from 0 to 100% (1.0x)
<div style={tw('enter-scale-100/0/500')}>Growing</div>

// Scale to 150% (1.5x)
<div style={tw('enter-scale-150/0/800')}>Enlarging</div>

// Pulse scale in a loop
<div style={tw('loop-scale-110/1000')}>Breathing</div>

// Scale down to 50%
<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>
```

### Rotate Animations

```tsx
// Rotate 90 degrees
<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>

// Rotate 180 degrees
<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>

// Continuous rotation in loop (360 degrees per cycle)
<div style={tw('loop-rotate-360/2000')}>Spinning</div>

// Rotate backwards with negative value
<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>
```

### Skew Animations

```tsx
// Skew on X axis
<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>

// Skew on Y axis
<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>

// Oscillating skew in loop
<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>

// Negative skew
<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>
```

### Combining Utilities

You can combine multiple utility animations on the same element:

```tsx
// Translate and rotate together
<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>
  Flying In
</div>

// Fade and scale
<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>
  Appearing
</div>

// Enter with translate, exit with rotation
<div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}>
  Slide and Spin
</div>
```

### Bracket Notation

For more CSS-like syntax, you can use brackets with units:

```tsx
// Using bracket notation with seconds
<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>

// Using bracket notation with milliseconds
<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>

// Mix and match - plain numbers are milliseconds
<h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>
```

## Enter Animations

Format: `enter-{type}/{startMs}/{durationMs}`

- `startMs` - when the animation begins (milliseconds from start)
- `durationMs` - how long the animation lasts

When values are omitted (`enter-fade-in`), it uses the full video duration.

### Fade Animations

Simple opacity transitions with optional direction.

```tsx
// Fade in from 0ms to 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>

// Fade in with upward motion
<h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1>
```

| Class | Description |
|-------|-------------|
| `enter-fade-in/0/500` | Fade in (opacity 0 → 1) |
| `enter-fade-in-up/0/500` | Fade in + slide up (30px) |
| `enter-fade-in-down/0/500` | Fade in + slide down (30px) |
| `enter-fade-in-left/0/500` | Fade in + slide from left (30px) |
| `enter-fade-in-right/0/500` | Fade in + slide from right (30px) |

### Slide Animations

Larger movement (100px) with fade.

```tsx
// Slide in from left: starts at 0, lasts 500ms
<div style={tw('enter-slide-left/0/500')}>Content</div>

// Slide up from bottom: starts at 200ms, lasts 600ms
<div style={tw('enter-slide-up/200/600')}>Content</div>
```

| Class | Description |
|-------|-------------|
| `enter-slide-left/0/500` | Slide in from left (100px) |
| `enter-slide-right/0/500` | Slide in from right (100px) |
| `enter-slide-up/0/500` | Slide in from bottom (100px) |
| `enter-slide-down/0/500` | Slide in from top (100px) |

### Bounce Animations

Playful entrance with overshoot effect.

```tsx
// Bounce in with scale overshoot
<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>

// Bounce in from below
<div style={tw('enter-bounce-in-up/0/600')}>Pop!</div>
```

| Class | Description |
|-------|-------------|
| `enter-bounce-in/0/500` | Bounce in with scale overshoot |
| `enter-bounce-in-up/0/500` | Bounce in from below |
| `enter-bounce-in-down/0/500` | Bounce in from above |
| `enter-bounce-in-left/0/500` | Bounce in from left |
| `enter-bounce-in-right/0/500` | Bounce in from right |

### Scale & Zoom Animations

Size-based transitions.

```tsx
// Scale in from 50%
<div style={tw('enter-scale-in/0/500')}>Growing</div>

// Zoom in from 0%
<div style={tw('enter-zoom-in/0/1000')}>Zooming</div>
```

| Class | Description |
|-------|-------------|
| `enter-scale-in/0/500` | Scale up from 50% to 100% |
| `enter-zoom-in/0/500` | Zoom in from 0% to 100% |

### Rotate & Flip Animations

Rotation-based transitions.

```tsx
// Rotate in 180 degrees
<div style={tw('enter-rotate-in/0/500')}>Spinning</div>

// 3D flip on X axis
<div style={tw('enter-flip-in-x/0/500')}>Flipping</div>
```

| Class | Description |
|-------|-------------|
| `enter-rotate-in/0/500` | Rotate in from -180° |
| `enter-flip-in-x/0/500` | 3D flip on horizontal axis |
| `enter-flip-in-y/0/500` | 3D flip on vertical axis |

## Exit Animations

Format: `exit-{type}/{startMs}/{durationMs}`

- `startMs` - when the exit animation begins
- `durationMs` - how long the exit animation lasts

Exit animations use the same timing system but animate elements out.

```tsx
// Fade out starting at 2500ms, lasting 500ms (ends at 3000ms)
<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>

// Combined enter and exit on same element
<h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}>
  Hello and Goodbye
</h1>
```

| Class | Description |
|-------|-------------|
| `exit-fade-out/2500/500` | Fade out (opacity 1 → 0) |
| `exit-fade-out-up/2500/500` | Fade out + slide up |
| `exit-fade-out-down/2500/500` | Fade out + slide down |
| `exit-fade-out-left/2500/500` | Fade out + slide left |
| `exit-fade-out-right/2500/500` | Fade out + slide right |
| `exit-slide-up/2500/500` | Slide out upward (100px) |
| `exit-slide-down/2500/500` | Slide out downward (100px) |
| `exit-slide-left/2500/500` | Slide out to left (100px) |
| `exit-slide-right/2500/500` | Slide out to right (100px) |
| `exit-scale-out/2500/500` | Scale out to 150% |
| `exit-zoom-out/2500/500` | Zoom out to 200% |
| `exit-rotate-out/2500/500` | Rotate out to 180° |
| `exit-bounce-out/2500/500` | Bounce out with scale |
| `exit-bounce-out-up/2500/500` | Bounce out upward |
| `exit-bounce-out-down/2500/500` | Bounce out downward |
| `exit-bounce-out-left/2500/500` | Bounce out to left |
| `exit-bounce-out-right/2500/500` | Bounce out to right |

## Loop Animations

Format: `loop-{type}/{durationMs}`

Loop animations repeat every `{durationMs}` milliseconds:
- `/1000` = 1 second loop
- `/500` = 0.5 second loop
- `/2000` = 2 second loop

When duration is omitted (`loop-bounce`), it defaults to 1000ms (1 second).

```tsx
// Pulse opacity every 500ms
<div style={tw('loop-fade/500')}>Pulsing</div>

// Bounce every 800ms
<div style={tw('loop-bounce/800')}>Bouncing</div>

// Full rotation every 2000ms
<div style={tw('loop-spin/2000')}>Spinning</div>
```

| Class | Description |
|-------|-------------|
| `loop-fade/{ms}` | Opacity pulse (0.5 → 1 → 0.5) |
| `loop-bounce/{ms}` | Bounce up and down |
| `loop-spin/{ms}` | Full 360° rotation |
| `loop-ping/{ms}` | Scale up + fade out (radar effect) |
| `loop-wiggle/{ms}` | Side to side wiggle |
| `loop-float/{ms}` | Gentle up and down floating |
| `loop-pulse/{ms}` | Scale pulse (1.0 → 1.05 → 1.0) |
| `loop-shake/{ms}` | Shake side to side |

## Easing Functions

Add an easing class **before** the animation class to control the timing curve.

```tsx
// Ease in (accelerate)
<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>

// Ease out (decelerate) - default
<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>

// Ease in-out (smooth)
<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>

// Strong cubic easing
<h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1>
```

| Class | Description | Best For |
|-------|-------------|----------|
| `linear` | Constant speed | Mechanical motion |
| `ease-in` | Slow start, fast end | Exit animations |
| `ease-out` | Fast start, slow end (default) | Enter animations |
| `ease-in-out` | Slow start and end | Subtle transitions |
| `ease-in-cubic` | Strong slow start | Dramatic exits |
| `ease-out-cubic` | Strong fast start | Impactful entrances |
| `ease-in-out-cubic` | Strong both ends | Emphasis animations |
| `ease-in-quart` | Very strong slow start | Powerful exits |
| `ease-out-quart` | Very strong fast start | Punchy entrances |
| `ease-in-out-quart` | Very strong both ends | Maximum drama |

### Per-Animation-Type Easing

You can apply **different easing functions** to enter, exit, and loop animations on the same element using `enter-ease-*`, `exit-ease-*`, and `loop-ease-*` classes.

```tsx
// Different easing for enter and exit
<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>
  Smooth entrance, sharp exit
</h1>

// Loop with linear easing, enter with bounce
<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>
  Bouncy entrance, linear loop
</div>

// Default easing still works (applies to all animations)
<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>
  Same easing for both
</div>

// Mix default with specific overrides
<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>
  Default ease-out for enter, cubic-in for exit
</div>
```

**How it works:**

1. **Default easing** (`ease-*`) applies to ALL animations if no specific override is set
2. **Specific easing** (`enter-ease-*`, `exit-ease-*`, `loop-ease-*`) overrides the default for that animation type
3. If both are present, specific easing takes priority for its animation type

**Available easing classes:**

| Default (all animations) | Enter only | Exit only | Loop only |
|--------------------------|------------|-----------|-----------|
| `ease-in` | `enter-ease-in` | `exit-ease-in` | `loop-ease-in` |
| `ease-out` | `enter-ease-out` | `exit-ease-out` | `loop-ease-out` |
| `ease-in-out` | `enter-ease-in-out` | `exit-ease-in-out` | `loop-ease-in-out` |
| `ease-in-cubic` | `enter-ease-in-cubic` | `exit-ease-in-cubic` | `loop-ease-in-cubic` |
| `ease-out-cubic` | `enter-ease-out-cubic` | `exit-ease-out-cubic` | `loop-ease-out-cubic` |
| `ease-in-out-cubic` | `enter-ease-in-out-cubic` | `exit-ease-in-out-cubic` | `loop-ease-in-out-cubic` |
| `ease-in-quart` | `enter-ease-in-quart` | `exit-ease-in-quart` | `loop-ease-in-quart` |
| `ease-out-quart` | `enter-ease-out-quart` | `exit-ease-out-quart` | `loop-ease-out-quart` |
| `ease-in-out-quart` | `enter-ease-in-out-quart` | `exit-ease-in-out-quart` | `loop-ease-in-out-quart` |
| `linear` | `enter-ease-linear` | `exit-ease-linear` | `loop-ease-linear` |
| `ease-spring` | `enter-ease-spring` | `exit-ease-spring` | `loop-ease-spring` |

### Spring Easing

Spring easing creates natural, physics-based bouncy animations. Use the built-in `ease-spring` easing or create custom springs with configurable parameters.

```tsx
// Default spring easing
<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>

// Per-animation-type spring
<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>
  Spring entrance, smooth exit
</div>

// Custom spring with parameters: ease-spring/mass/stiffness/damping
<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>
  Custom spring (mass=1, stiffness=100, damping=10)
</h1>

// More bouncy spring (lower damping)
<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>
  Extra bouncy!
</div>

// Stiffer spring (higher stiffness, faster)
<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>
  Snappy spring
</div>

// Per-animation-type custom springs
<div style={tw('enter-ease-spring/1/150/10 enter-fade-in/0/500 exit-ease-spring/1/100/15 exit-fade-out/2500/500')}>
  Different springs for enter and exit
</div>
```

**Spring parameters:**

| Parameter | Description | Effect when increased | Default |
|-----------|-------------|----------------------|---------|
| **mass** | Mass of the spring | Slower, more inertia | 1 |
| **stiffness** | Spring stiffness | Faster, snappier | 100 |
| **damping** | Damping coefficient | Less bounce, smoother | 10 |

**Common spring presets:**

```tsx
// Gentle bounce (default)
ease-spring/1/100/10

// Extra bouncy
ease-spring/1/170/8

// Snappy (no bounce)
ease-spring/1/200/15

// Slow and bouncy
ease-spring/2/100/8

// Fast and tight
ease-spring/0.5/300/20
```

**How spring works:**

1. **Default `ease-spring`** - Uses a pre-calculated spring curve optimized for most use cases
2. **Custom `ease-spring/mass/stiffness/damping`** - Generates a physics-based spring curve using the [damped harmonic oscillator](https://www.kvin.me/css-springs) formula
3. The spring automatically calculates its ideal duration to reach the final state
4. Works with all animation types: `ease-spring`, `enter-ease-spring`, `exit-ease-spring`, `loop-ease-spring`

## Combining Enter and Exit

You can use both enter and exit animations on the same element:

```tsx
export default function EnterExit({ tw, title }) {
  return (
    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>
      {/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}
      <h1 style={tw('text-8xl font-bold text-white enter-fade-in/0/500 exit-fade-out/2500/500')}>
        {title}
      </h1>
    </div>
  );
}
```

The opacities from multiple animations are **multiplied together**, so you get smooth transitions that combine properly.

## Staggered Animations

Create sequenced animations by offsetting start times:

```tsx
export default function StaggeredList({ tw, items }) {
  return (
    <div style={tw('flex flex-col gap-4')}>
      {/* First item: starts at 0ms, lasts 300ms */}
      <div style={tw('ease-out enter-fade-in-left/0/300')}>
        {items[0]}
      </div>

      {/* Second item: starts at 100ms, lasts 300ms */}
      <div style={tw('ease-out enter-fade-in-left/100/300')}>
        {items[1]}
      </div>

      {/* Third item: starts at 200ms, lasts 300ms */}
      <div style={tw('ease-out enter-fade-in-left/200/300')}>
        {items[2]}
      </div>
    </div>
  );
}
```

### Dynamic Staggering

For dynamic lists, calculate the timing programmatically:

```tsx
export default function DynamicStagger({ tw, items }) {
  return (
    <div style={tw('flex flex-col gap-4')}>
      {items.map((item, i) => {
        const start = i * 100;      // Each item starts 100ms later
        const duration = 300;       // Each animation lasts 300ms

        return (
          <div
            key={i}
            style={tw(`ease-out enter-fade-in-up/${start}/${duration}`)}
          >
            {item}
          </div>
        );
      })}
    </div>
  );
}
```

## Common Patterns

### Intro Sequence

```tsx
export default function IntroVideo({ tw, title, subtitle, logo }) {
  return (
    <div style={tw('flex flex-col items-center justify-cen

…(truncated)
