{
  "schemaVersion": "1.0",
  "item": {
    "slug": "loopwind",
    "name": "Loopwind",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/tomtev/loopwind",
    "canonicalUrl": "https://clawhub.ai/tomtev/loopwind",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/loopwind",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=loopwind",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/loopwind"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/loopwind",
    "agentPageUrl": "https://openagent3.xyz/skills/loopwind/agent",
    "manifestUrl": "https://openagent3.xyz/skills/loopwind/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/loopwind/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "loopwind",
        "body": "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."
      },
      {
        "title": "Quick Start",
        "body": "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."
      },
      {
        "title": "Installation",
        "body": "curl -fsSL https://loopwind.dev/install.sh | bash\n\nThis installs loopwind to ~/.loopwind/ and adds the loopwind command to your PATH. Requires Node.js 18+."
      },
      {
        "title": "Initialize in Your Project",
        "body": "Navigate to any project folder and run:\n\nloopwind init\n\nThis creates .loopwind/loopwind.json — a configuration file with your project's theme colors."
      },
      {
        "title": "Install AI Skill",
        "body": "Give your AI agent expertise in loopwind:\n\nnpx skills add https://loopwind.dev/skill.md\n\nThis installs a skill that teaches Claude Code (or other AI agents) how to create templates, use animation classes, and render images/videos."
      },
      {
        "title": "Use with Claude Code",
        "body": "With the loopwind skill installed, Claude has deep knowledge of template structure, animation classes, and Tailwind CSS patterns for Satori. Just ask:\n\nCreate an OG image for my blog post about TypeScript tips\n\nCreate an animated intro video for my YouTube channel\n\nClaude will create optimized templates and render the final output automatically."
      },
      {
        "title": "Install a Template",
        "body": "1. Official Templates\n\nloopwind add image-template\nloopwind add video-template\n\nTemplates are installed to: .loopwind/<template>/\n\nBenefits:\n\nTemplates are local to your project\nVersion controlled with your project\nEasy to share within your team"
      },
      {
        "title": "Render a Template",
        "body": "loopwind render template-name '{\"title\":\"Hello World\",\"subtitle\":\"Built with loopwind\"}'\n\nor use a local props file:\n\nloopwind render template-name props.json"
      },
      {
        "title": "loopwind add <source>",
        "body": "Install a template from various sources:\n\n# Official templates\nloopwind add image-template\nloopwind add video-template\n\nThese will be downloaded to .loopwind/<template>/"
      },
      {
        "title": "loopwind list",
        "body": "List all installed templates:\n\nloopwind list"
      },
      {
        "title": "loopwind render <template> <props> [options]",
        "body": "Render an image or video:\n\n# Image with inline props\nloopwind render banner-hero '{\"title\":\"Hello World\"}'\n\n# Video with inline props\nloopwind render video-intro '{\"title\":\"Welcome\"}'\n\n# Using a props file\nloopwind render banner-hero props.json\n\n# Custom output\nloopwind render banner-hero '{\"title\":\"Hello\"}' --out custom-name.png\n\n# Different format\nloopwind render banner-hero '{\"title\":\"Hello\"}' --format jpeg\n\nOptions:\n\n--out, -o - Output filename (default: <template>.<ext> in current directory)\n--format - Output format: png, jpeg, svg (images only)\n--quality - JPEG quality 1-100 (default: 92)"
      },
      {
        "title": "loopwind validate <template>",
        "body": "Validate a template:\n\nloopwind validate banner-hero\n\nChecks:\n\nTemplate file exists and is valid React\nexport const meta exists and is valid\nRequired props are defined\nFonts exist (if specified)"
      },
      {
        "title": "loopwind init",
        "body": "Initialize loopwind in a project:\n\nloopwind init\n\nCreates .loopwind/loopwind.json configuration file with your project's design tokens."
      },
      {
        "title": "Animation Classes (Video Only)",
        "body": "Use Tailwind-style animation classes - no manual calculations needed:\n\n// Fade in: starts at 0ms, lasts 500ms\n<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>\n\n// Loop: ping effect every 500ms\n<div style={tw('loop-ping/500')} />\n\n// Combined with easing\n<h1 style={tw('ease-out enter-bounce-in-up/0/600')}>Title</h1>\n\nSee Animation for the complete reference."
      },
      {
        "title": "Next Steps",
        "body": "Templates\nEmbedding Images\nAnimation\nHelpers (QR, Template Composition)\nStyling with Tailwind & shadcn/ui\nCustom Fonts\nAI Agent Integration"
      },
      {
        "title": "Templates",
        "body": "Templates are React components that define your images and videos. They use Tailwind CSS for styling and export metadata that loopwind uses for rendering."
      },
      {
        "title": "Official Templates",
        "body": "loopwind add image-template\nloopwind add video-template\n\nTemplates are installed to .loopwind/<template-name>/."
      },
      {
        "title": "Direct URLs",
        "body": "loopwind add https://example.com/templates/my-template.json"
      },
      {
        "title": "Local Filesystem",
        "body": "loopwind add ./my-templates/banner-hero\nloopwind add /Users/you/templates/social-card"
      },
      {
        "title": "Basic Structure",
        "body": "// .loopwind/banner-hero/template.tsx\nexport const meta = {\n  name: \"banner-hero\",\n  type: \"image\",\n  description: \"Hero banner with gradient background\",\n  size: { width: 1600, height: 900 },\n  props: { title: \"string\", subtitle: \"string\" }\n};\n\nexport default function BannerHero({ title, subtitle, tw }) {\n  return (\n    <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')}>\n      <h1 style={tw('text-7xl font-bold text-white mb-4')}>\n        {title}\n      </h1>\n      <p style={tw('text-2xl text-white/80')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}"
      },
      {
        "title": "Rendering Images",
        "body": "# Render with inline props\nloopwind render banner-hero '{\"title\":\"Hello World\",\"subtitle\":\"Welcome\"}'\n\n# Custom output name\nloopwind render banner-hero '{\"title\":\"Hello\"}' --out custom-name.png\n\n# Different format\nloopwind render banner-hero '{\"title\":\"Hello\"}' --format jpeg --quality 95\n\n# Use a props file\nloopwind render banner-hero props.json"
      },
      {
        "title": "Output Formats",
        "body": "FormatBest ForPNG (default)Transparency, sharp text, logosJPEGPhotographs, gradients, smaller filesSVGVector graphics, scalable designs"
      },
      {
        "title": "Basic Structure",
        "body": "// .loopwind/video-intro/template.tsx\nexport const meta = {\n  name: \"video-intro\",\n  type: \"video\",\n  description: \"Animated intro with bounce-in title\",\n  size: { width: 1920, height: 1080 },\n  video: { fps: 30, duration: 3 },\n  props: { title: \"string\" }\n};\n\nexport default function VideoIntro({ tw, title }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>\n      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/600')}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "Rendering Videos",
        "body": "# Render with inline props\nloopwind render video-intro '{\"title\":\"Welcome!\"}' --out intro.mp4\n\n# Faster encoding with FFmpeg\nloopwind render video-intro '{\"title\":\"Welcome!\"}' --ffmpeg\n\n# Higher quality (lower CRF = better)\nloopwind render video-intro '{\"title\":\"Welcome!\"}' --crf 18"
      },
      {
        "title": "FPS and Duration",
        "body": "video: { fps: 30, duration: 3 }  // 90 frames total\n\nFPSUse Case24Cinematic look, smaller files30Standard web video60Smooth animations"
      },
      {
        "title": "Video-Specific Props",
        "body": "Templates receive these additional props:\n\nframe - Current frame number (0 to totalFrames - 1)\nprogress - Animation progress from 0 to 1\n\nexport default function MyVideo({ frame, progress }) {\n  // frame: 0, 1, 2, ... 89 (for 3s @ 30fps)\n  // progress: 0.0 at start, 0.5 at middle, 1.0 at end\n}"
      },
      {
        "title": "Encoding Options",
        "body": "EncoderCommandUse CaseWASM (default)loopwind render ...CI/CD, no dependenciesFFmpegloopwind render ... --ffmpegFaster, smaller files\n\nInstall FFmpeg: brew install ffmpeg (macOS)"
      },
      {
        "title": "Animation Classes",
        "body": "Use Tailwind-style animation classes for videos:\n\n// Enter animations: enter-{type}/{delay}/{duration}\n<h1 style={tw('enter-fade-in/0/500')}>Fade in at start</h1>\n<h1 style={tw('enter-bounce-in-up/300/400')}>Bounce in after 300ms</h1>\n\n// Exit animations: exit-{type}/{start}/{duration}\n<div style={tw('exit-fade-out/2500/500')}>Fade out at 2.5s</div>\n\n// Loop animations: loop-{type}/{duration}\n<div style={tw('loop-float/1000')}>Continuous floating</div>\n<div style={tw('loop-spin/1000')}>Spinning</div>\n\n// Easing\n<h1 style={tw('ease-out enter-slide-left/0/500')}>Smooth slide</h1>\n\nSee the full Animation documentation for all classes."
      },
      {
        "title": "Social Media",
        "body": "Twitter/X Card: 1200x675\nFacebook/OG: 1200x630\nInstagram Post: 1080x1080\nLinkedIn Post: 1200x627"
      },
      {
        "title": "Web Graphics",
        "body": "Hero Banner: 1920x1080\nBlog Header: 1600x900\nThumbnail: 640x360"
      },
      {
        "title": "Open Graph Image",
        "body": "export const meta = {\n  name: \"og-image\",\n  type: \"image\",\n  size: { width: 1200, height: 630 },\n  props: { title: \"string\", description: \"string\" }\n};\n\nexport default function OGImage({ tw, image, title, description }) {\n  return (\n    <div style={tw('flex w-full h-full bg-white')}>\n      <div style={tw('flex-1 flex flex-col justify-between p-12')}>\n        <img src={image('logo.svg')} style={tw('h-12 w-auto')} />\n        <div>\n          <h1 style={tw('text-5xl font-bold text-gray-900 mb-4')}>{title}</h1>\n          <p style={tw('text-xl text-gray-600')}>{description}</p>\n        </div>\n        <p style={tw('text-gray-400')}>yoursite.com</p>\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Animated Intro",
        "body": "export const meta = {\n  name: \"animated-intro\",\n  type: \"video\",\n  size: { width: 1920, height: 1080 },\n  video: { fps: 60, duration: 3 },\n  props: { title: \"string\", subtitle: \"string\" }\n};\n\nexport default function AnimatedIntro({ tw, title, subtitle }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-background')}>\n      <h1 style={tw('text-8xl font-bold text-foreground ease-out enter-bounce-in-up/0/400')}>\n        {title}\n      </h1>\n      <p style={tw('text-2xl text-muted-foreground mt-4 ease-out enter-fade-in-up/300/400')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}"
      },
      {
        "title": "Next Steps",
        "body": "Layouts - Wrap templates with reusable layouts\nEmbedding Images - Using the image() helper\nAnimation - Full animation reference\nStyling - Tailwind & shadcn/ui integration\nFonts - Custom fonts"
      },
      {
        "title": "Layouts",
        "body": "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."
      },
      {
        "title": "Layout Template",
        "body": "Create a layout template that receives children:\n\n// .loopwind/base-layout/template.tsx\nexport const meta = {\n  name: 'base-layout',\n  type: 'image',\n  size: { width: 1200, height: 630 },\n  props: {},\n};\n\nexport default function BaseLayout({ tw, children }) {\n  return (\n    <div style={tw('flex flex-col w-full h-full bg-background')}>\n      {/* Header */}\n      <div style={tw('flex items-center px-8 py-4 border-b border-border')}>\n        <span style={tw('text-2xl font-bold text-primary')}>loopwind</span>\n      </div>\n\n      {/* Content slot */}\n      <div style={tw('flex flex-1')}>\n        {children}\n      </div>\n\n      {/* Footer */}\n      <div style={tw('flex items-center justify-between px-8 py-4 border-t border-border')}>\n        <span style={tw('text-muted-foreground')}>loopwind.dev</span>\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Usage in Templates",
        "body": "Reference the layout using a relative path:\n\n// .loopwind/blog-post/template.tsx\nexport const meta = {\n  name: 'blog-post',\n  type: 'image',\n  layout: '../base-layout', // Layout controls size\n  props: {\n    title: 'string',\n    excerpt: 'string',\n  },\n};\n\nexport default function BlogPost({ tw, title, excerpt }) {\n  return (\n    <div style={tw('flex flex-col justify-center p-12')}>\n      <h1 style={tw('text-5xl font-bold text-foreground mb-4 text-balance')}>\n        {title}\n      </h1>\n      <p style={tw('text-xl text-muted-foreground leading-relaxed')}>\n        {excerpt}\n      </p>\n    </div>\n  );\n}"
      },
      {
        "title": "Render",
        "body": "loopwind render blog-post '{\"title\":\"Hello World\",\"excerpt\":\"My first post\"}'\n\nThe output uses the layout's size (1200x630) with the child content inside."
      },
      {
        "title": "Size",
        "body": "When using a layout, the layout's size controls the final output dimensions. The child template doesn't need a size property."
      },
      {
        "title": "Path Resolution",
        "body": "Use relative paths to reference layouts:\n\nlayout: '../base-layout'      // Sibling directory\nlayout: './shared/layout'     // Subdirectory\nlayout: '../../layouts/main'  // Parent's sibling"
      },
      {
        "title": "Props Flow",
        "body": "The layout receives:\n\nAll standard helpers (tw, image, qr, template, etc.)\nchildren prop containing the rendered child content\nAnimation context (frame, progress) for video layouts\n\nexport default function Layout({ tw, children, frame, progress }) {\n  // tw, image, qr, template, path, textPath all available\n  return (\n    <div style={tw('flex w-full h-full')}>\n      {children}\n    </div>\n  );\n}"
      },
      {
        "title": "Video Layouts",
        "body": "Layouts work with video templates. Both the layout and child can use animations:\n\n// .loopwind/video-layout/template.tsx\nexport const meta = {\n  name: 'video-layout',\n  type: 'video',\n  size: { width: 1920, height: 1080 },\n  video: { fps: 60, duration: 4 },\n  props: {},\n};\n\nexport default function VideoLayout({ tw, children }) {\n  return (\n    <div style={tw('flex flex-col w-full h-full bg-background')}>\n      {/* Animated header */}\n      <div style={tw('flex items-center px-12 py-6 ease-out enter-slide-down/0/500')}>\n        <span style={tw('text-3xl font-bold text-primary')}>loopwind</span>\n      </div>\n\n      {/* Content */}\n      <div style={tw('flex flex-1')}>\n        {children}\n      </div>\n\n      {/* Animated footer */}\n      <div style={tw('flex px-12 py-6 ease-out enter-fade-in/500/400')}>\n        <span style={tw('text-muted-foreground')}>loopwind.dev</span>\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Example: Consistent OG Images",
        "body": "Create a layout for all your OG images:\n\n// .loopwind/og-layout/template.tsx\nexport const meta = {\n  name: 'og-layout',\n  type: 'image',\n  size: { width: 1200, height: 630 },\n  props: {},\n};\n\nexport default function OGLayout({ tw, image, children }) {\n  return (\n    <div style={tw('flex w-full h-full bg-background')}>\n      {/* Content area */}\n      <div style={tw('flex flex-col flex-1 p-12')}>\n        {/* Logo */}\n        <div style={tw('flex items-center gap-3 mb-auto')}>\n          <img src={image('logo.svg')} style={tw('h-10 w-auto')} />\n          <span style={tw('text-2xl font-bold')}>MyBrand</span>\n        </div>\n\n        {/* Slot for page-specific content */}\n        <div style={tw('flex flex-1 items-center')}>\n          {children}\n        </div>\n\n        {/* Domain */}\n        <span style={tw('text-muted-foreground mt-auto')}>mybrand.com</span>\n      </div>\n    </div>\n  );\n}\n\nThen create page-specific templates:\n\n// .loopwind/og-blog/template.tsx\nexport const meta = {\n  name: 'og-blog',\n  type: 'image',\n  layout: '../og-layout',\n  props: {\n    title: 'string',\n    author: 'string',\n  },\n};\n\nexport default function OGBlog({ tw, title, author }) {\n  return (\n    <div style={tw('flex flex-col')}>\n      <span style={tw('text-sm text-muted-foreground uppercase tracking-wider mb-2')}>\n        Blog Post\n      </span>\n      <h1 style={tw('text-4xl font-bold text-foreground mb-4 text-balance')}>\n        {title}\n      </h1>\n      <span style={tw('text-muted-foreground')}>By {author}</span>\n    </div>\n  );\n}"
      },
      {
        "title": "Next Steps",
        "body": "Templates - Template structure and metadata\nAnimation - Animation classes for video layouts\nHelpers - Using image(), qr(), and template()"
      },
      {
        "title": "Embedding Images",
        "body": "Use the image() helper to embed images in your templates. It supports loading from props, template directories, and URLs."
      },
      {
        "title": "Prop-based Images",
        "body": "Pass the prop name to load an image path from props:\n\nexport const meta = {\n  name: \"product-card\",\n  type: \"image\",\n  size: { width: 1200, height: 630 },\n  props: {\n    title: \"string\",\n    background: \"string?\"\n  }\n};\n\nexport default function ProductCard({ tw, image, title, background }) {\n  // Use fallback if no background prop provided\n  const bgSrc = background\n    ? image('background')\n    : 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200';\n\n  return (\n    <div style={tw('relative w-full h-full')}>\n      <img\n        src={bgSrc}\n        style={tw('absolute inset-0 w-full h-full object-cover')}\n      />\n      <div style={tw('relative z-10 p-12')}>\n        <h1 style={tw('text-6xl font-bold text-white')}>{title}</h1>\n      </div>\n    </div>\n  );\n}\n\nThe image('background') helper loads from the background prop value (file path or URL)."
      },
      {
        "title": "Direct File Images",
        "body": "Load images directly from your template directory by including the file extension:\n\nexport default function ChangelogItem({ tw, image, text }) {\n  return (\n    <div style={tw('flex items-center gap-4')}>\n      {/* Load check.svg from template directory */}\n      <img\n        src={image('check.svg')}\n        style={tw('w-6 h-6')}\n      />\n      <span style={tw('text-lg')}>{text}</span>\n    </div>\n  );\n}\n\nYou can also use subdirectories:\n\n<img src={image('assets/icons/star.svg')} />\n<img src={image('shared/logo.png')} />\n\nTemplate directory structure:\n\n.loopwind/my-template/\n├── template.tsx\n├── check.svg           ← image('check.svg')\n└── assets/\n    └── icons/\n        └── star.svg    ← image('assets/icons/star.svg')"
      },
      {
        "title": "URLs",
        "body": "The image() helper also supports loading images from URLs:\n\n{\n  \"background\": \"https://example.com/image.jpg\"\n}"
      },
      {
        "title": "Supported Formats",
        "body": "JPEG (.jpg, .jpeg)\nPNG (.png)\nGIF (.gif)\nWebP (.webp)\nSVG (.svg)"
      },
      {
        "title": "Image Positioning",
        "body": "Use Tailwind's object-fit utilities:\n\nexport default function ImageGrid({ tw, image, img1, img2, img3 }) {\n  return (\n    <div style={tw('flex gap-4 w-full h-full p-8 bg-gray-100')}>\n      {/* Cover - fills entire area, may crop */}\n      <img\n        src={image('img1')}\n        style={tw('w-full h-full object-cover rounded-lg')}\n      />\n\n      {/* Contain - fits within area, may letterbox */}\n      <img\n        src={image('img2')}\n        style={tw('w-full h-full object-contain')}\n      />\n\n      {/* Fill - stretches to fill */}\n      <img\n        src={image('img3')}\n        style={tw('w-full h-full object-fill')}\n      />\n    </div>\n  );\n}"
      },
      {
        "title": "Images Not Loading",
        "body": "Check file paths are relative to the props file:\n\n{\n  \"background\": \"./images/bg.jpg\"\n}\n\nAbsolute paths won't work."
      },
      {
        "title": "Optimize Image Sizes",
        "body": "Use appropriately sized images before embedding:\n\nconvert large-image.jpg -resize 1600x900 optimized.jpg"
      },
      {
        "title": "Next Steps",
        "body": "Templates - Creating image and video templates\nAnimation - Animation classes for videos\nStyling - Tailwind & shadcn/ui integration"
      },
      {
        "title": "Animation",
        "body": "loopwind provides Tailwind-style animation classes that work with time to create smooth video animations without writing custom code.\n\nNote: Animation classes only work with video templates and GIFs. For static images, animations will have no effect since there's no time context."
      },
      {
        "title": "Quick Start",
        "body": "export default function MyVideo({ tw, title, subtitle }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-black')}>\n      {/* Bounce in from below: starts at 0, lasts 400ms */}\n      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400')}>\n        {title}\n      </h1>\n\n      {/* Fade in with upward motion: starts at 300ms, lasts 400ms */}\n      <p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/300/400')}>\n        {subtitle}\n      </p>\n\n      {/* Continuous floating animation: repeats every 1s (1000ms) */}\n      <div style={tw('mt-8 text-4xl loop-float/1000')}>\n        ⬇️\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Animation Format",
        "body": "loopwind uses three types of animations with millisecond timing:\n\nTypeFormatDescriptionEnterenter-{type}/{start}/{duration}Animations that play when enteringExitexit-{type}/{start}/{duration}Animations that play when exitingLooploop-{type}/{duration}Continuous looping animations\n\nAll timing values are in milliseconds (1000ms = 1 second)."
      },
      {
        "title": "Utility-Based Animations",
        "body": "In addition to predefined animations, loopwind supports Tailwind utility-based animations that let you animate any transform or opacity property directly:\n\n// Slide in 20px from the left\n<div style={tw('enter-translate-x-5/0/1000')}>Content</div>\n\n// Rotate 90 degrees on entrance\n<div style={tw('enter-rotate-90/0/500')}>Spinning</div>\n\n// Fade to 50% opacity in a loop\n<div style={tw('loop-opacity-50/1000')}>Pulsing</div>\n\n// Scale down with negative value\n<div style={tw('enter--scale-50/0/800')}>Shrinking</div>"
      },
      {
        "title": "Supported Utilities",
        "body": "UtilityFormatDescriptionExampletranslate-xenter-translate-x-{value}Translate horizontallyenter-translate-x-5 = 20px<br/>enter-translate-x-full = 100%<br/>enter-translate-x-[20px] = 20pxtranslate-yenter-translate-y-{value}Translate verticallyloop-translate-y-10 = 40px<br/>enter-translate-y-1/2 = 50%<br/>enter-translate-y-[5rem] = 80pxopacityenter-opacity-{n}Set opacity (0-100)enter-opacity-50 = 50%scaleenter-scale-{n}Scale element (0-200)enter-scale-100 = 1.0xrotateenter-rotate-{n}Rotate in degreesenter-rotate-45 = 45°skew-xenter-skew-x-{n}Skew on X axis in degreesenter-skew-x-12 = 12°skew-yenter-skew-y-{n}Skew on Y axis in degreesexit-skew-y-6 = 6°\n\nTranslate value formats:\n\nNumeric: 5 = 20px (Tailwind spacing scale: 1 unit = 4px)\nKeywords: full = 100%\nFractions: 1/2 = 50%, 1/3 = 33.333%, 2/3 = 66.666%, etc.\nArbitrary values: [20px], [5rem], [10%] (rem converts to px: 1rem = 16px)\n\nAll utilities work with:\n\nAll prefixes: enter-, exit-, loop-, animate-\nNegative values: Prefix with - (e.g., -translate-x-5, -rotate-45)\nTiming syntax: Add /start/duration (e.g., enter-translate-x-5/0/800)"
      },
      {
        "title": "Translate Animations",
        "body": "// Numeric (Tailwind spacing): 20px (5 * 4px)\n<div style={tw('enter-translate-x-5/0/500')}>Content</div>\n\n// Keyword: Full width (100%)\n<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>\n\n// Fraction: Half width (50%)\n<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>\n\n// Arbitrary values: Exact px or rem\n<div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div>\n<div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div>\n\n// Loop with fractions\n<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div>\n\n// Negative values\n<div style={tw('exit--translate-y-8/2000/500')}>Rising</div>"
      },
      {
        "title": "Opacity Animations",
        "body": "// Fade to 100% opacity\n<div style={tw('enter-opacity-100/0/500')}>Fading In</div>\n\n// Fade to 50% opacity\n<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>\n\n// Pulse between 50% and 100%\n<div style={tw('loop-opacity-50/1000')}>Pulsing</div>\n\n// Fade out to 0%\n<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>"
      },
      {
        "title": "Scale Animations",
        "body": "// Scale from 0 to 100% (1.0x)\n<div style={tw('enter-scale-100/0/500')}>Growing</div>\n\n// Scale to 150% (1.5x)\n<div style={tw('enter-scale-150/0/800')}>Enlarging</div>\n\n// Pulse scale in a loop\n<div style={tw('loop-scale-110/1000')}>Breathing</div>\n\n// Scale down to 50%\n<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>"
      },
      {
        "title": "Rotate Animations",
        "body": "// Rotate 90 degrees\n<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>\n\n// Rotate 180 degrees\n<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>\n\n// Continuous rotation in loop (360 degrees per cycle)\n<div style={tw('loop-rotate-360/2000')}>Spinning</div>\n\n// Rotate backwards with negative value\n<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>"
      },
      {
        "title": "Skew Animations",
        "body": "// Skew on X axis\n<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>\n\n// Skew on Y axis\n<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>\n\n// Oscillating skew in loop\n<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>\n\n// Negative skew\n<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>"
      },
      {
        "title": "Combining Utilities",
        "body": "You can combine multiple utility animations on the same element:\n\n// Translate and rotate together\n<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>\n  Flying In\n</div>\n\n// Fade and scale\n<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>\n  Appearing\n</div>\n\n// Enter with translate, exit with rotation\n<div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}>\n  Slide and Spin\n</div>"
      },
      {
        "title": "Bracket Notation",
        "body": "For more CSS-like syntax, you can use brackets with units:\n\n// Using bracket notation with seconds\n<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>\n\n// Using bracket notation with milliseconds\n<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>\n\n// Mix and match - plain numbers are milliseconds\n<h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>"
      },
      {
        "title": "Enter Animations",
        "body": "Format: enter-{type}/{startMs}/{durationMs}\n\nstartMs - when the animation begins (milliseconds from start)\ndurationMs - how long the animation lasts\n\nWhen values are omitted (enter-fade-in), it uses the full video duration."
      },
      {
        "title": "Fade Animations",
        "body": "Simple opacity transitions with optional direction.\n\n// Fade in from 0ms to 500ms\n<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>\n\n// Fade in with upward motion\n<h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1>\n\nClassDescriptionenter-fade-in/0/500Fade in (opacity 0 → 1)enter-fade-in-up/0/500Fade in + slide up (30px)enter-fade-in-down/0/500Fade in + slide down (30px)enter-fade-in-left/0/500Fade in + slide from left (30px)enter-fade-in-right/0/500Fade in + slide from right (30px)"
      },
      {
        "title": "Slide Animations",
        "body": "Larger movement (100px) with fade.\n\n// Slide in from left: starts at 0, lasts 500ms\n<div style={tw('enter-slide-left/0/500')}>Content</div>\n\n// Slide up from bottom: starts at 200ms, lasts 600ms\n<div style={tw('enter-slide-up/200/600')}>Content</div>\n\nClassDescriptionenter-slide-left/0/500Slide in from left (100px)enter-slide-right/0/500Slide in from right (100px)enter-slide-up/0/500Slide in from bottom (100px)enter-slide-down/0/500Slide in from top (100px)"
      },
      {
        "title": "Bounce Animations",
        "body": "Playful entrance with overshoot effect.\n\n// Bounce in with scale overshoot\n<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>\n\n// Bounce in from below\n<div style={tw('enter-bounce-in-up/0/600')}>Pop!</div>\n\nClassDescriptionenter-bounce-in/0/500Bounce in with scale overshootenter-bounce-in-up/0/500Bounce in from belowenter-bounce-in-down/0/500Bounce in from aboveenter-bounce-in-left/0/500Bounce in from leftenter-bounce-in-right/0/500Bounce in from right"
      },
      {
        "title": "Scale & Zoom Animations",
        "body": "Size-based transitions.\n\n// Scale in from 50%\n<div style={tw('enter-scale-in/0/500')}>Growing</div>\n\n// Zoom in from 0%\n<div style={tw('enter-zoom-in/0/1000')}>Zooming</div>\n\nClassDescriptionenter-scale-in/0/500Scale up from 50% to 100%enter-zoom-in/0/500Zoom in from 0% to 100%"
      },
      {
        "title": "Rotate & Flip Animations",
        "body": "Rotation-based transitions.\n\n// Rotate in 180 degrees\n<div style={tw('enter-rotate-in/0/500')}>Spinning</div>\n\n// 3D flip on X axis\n<div style={tw('enter-flip-in-x/0/500')}>Flipping</div>\n\nClassDescriptionenter-rotate-in/0/500Rotate in from -180°enter-flip-in-x/0/5003D flip on horizontal axisenter-flip-in-y/0/5003D flip on vertical axis"
      },
      {
        "title": "Exit Animations",
        "body": "Format: exit-{type}/{startMs}/{durationMs}\n\nstartMs - when the exit animation begins\ndurationMs - how long the exit animation lasts\n\nExit animations use the same timing system but animate elements out.\n\n// Fade out starting at 2500ms, lasting 500ms (ends at 3000ms)\n<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>\n\n// Combined enter and exit on same element\n<h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}>\n  Hello and Goodbye\n</h1>\n\nClassDescriptionexit-fade-out/2500/500Fade out (opacity 1 → 0)exit-fade-out-up/2500/500Fade out + slide upexit-fade-out-down/2500/500Fade out + slide downexit-fade-out-left/2500/500Fade out + slide leftexit-fade-out-right/2500/500Fade out + slide rightexit-slide-up/2500/500Slide out upward (100px)exit-slide-down/2500/500Slide out downward (100px)exit-slide-left/2500/500Slide out to left (100px)exit-slide-right/2500/500Slide out to right (100px)exit-scale-out/2500/500Scale out to 150%exit-zoom-out/2500/500Zoom out to 200%exit-rotate-out/2500/500Rotate out to 180°exit-bounce-out/2500/500Bounce out with scaleexit-bounce-out-up/2500/500Bounce out upwardexit-bounce-out-down/2500/500Bounce out downwardexit-bounce-out-left/2500/500Bounce out to leftexit-bounce-out-right/2500/500Bounce out to right"
      },
      {
        "title": "Loop Animations",
        "body": "Format: loop-{type}/{durationMs}\n\nLoop animations repeat every {durationMs} milliseconds:\n\n/1000 = 1 second loop\n/500 = 0.5 second loop\n/2000 = 2 second loop\n\nWhen duration is omitted (loop-bounce), it defaults to 1000ms (1 second).\n\n// Pulse opacity every 500ms\n<div style={tw('loop-fade/500')}>Pulsing</div>\n\n// Bounce every 800ms\n<div style={tw('loop-bounce/800')}>Bouncing</div>\n\n// Full rotation every 2000ms\n<div style={tw('loop-spin/2000')}>Spinning</div>\n\nClassDescriptionloop-fade/{ms}Opacity pulse (0.5 → 1 → 0.5)loop-bounce/{ms}Bounce up and downloop-spin/{ms}Full 360° rotationloop-ping/{ms}Scale up + fade out (radar effect)loop-wiggle/{ms}Side to side wiggleloop-float/{ms}Gentle up and down floatingloop-pulse/{ms}Scale pulse (1.0 → 1.05 → 1.0)loop-shake/{ms}Shake side to side"
      },
      {
        "title": "Easing Functions",
        "body": "Add an easing class before the animation class to control the timing curve.\n\n// Ease in (accelerate)\n<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>\n\n// Ease out (decelerate) - default\n<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>\n\n// Ease in-out (smooth)\n<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>\n\n// Strong cubic easing\n<h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1>\n\nClassDescriptionBest ForlinearConstant speedMechanical motionease-inSlow start, fast endExit animationsease-outFast start, slow end (default)Enter animationsease-in-outSlow start and endSubtle transitionsease-in-cubicStrong slow startDramatic exitsease-out-cubicStrong fast startImpactful entrancesease-in-out-cubicStrong both endsEmphasis animationsease-in-quartVery strong slow startPowerful exitsease-out-quartVery strong fast startPunchy entrancesease-in-out-quartVery strong both endsMaximum drama"
      },
      {
        "title": "Per-Animation-Type Easing",
        "body": "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.\n\n// Different easing for enter and exit\n<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>\n  Smooth entrance, sharp exit\n</h1>\n\n// Loop with linear easing, enter with bounce\n<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>\n  Bouncy entrance, linear loop\n</div>\n\n// Default easing still works (applies to all animations)\n<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>\n  Same easing for both\n</div>\n\n// Mix default with specific overrides\n<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>\n  Default ease-out for enter, cubic-in for exit\n</div>\n\nHow it works:\n\nDefault easing (ease-*) applies to ALL animations if no specific override is set\nSpecific easing (enter-ease-*, exit-ease-*, loop-ease-*) overrides the default for that animation type\nIf both are present, specific easing takes priority for its animation type\n\nAvailable easing classes:\n\nDefault (all animations)Enter onlyExit onlyLoop onlyease-inenter-ease-inexit-ease-inloop-ease-inease-outenter-ease-outexit-ease-outloop-ease-outease-in-outenter-ease-in-outexit-ease-in-outloop-ease-in-outease-in-cubicenter-ease-in-cubicexit-ease-in-cubicloop-ease-in-cubicease-out-cubicenter-ease-out-cubicexit-ease-out-cubicloop-ease-out-cubicease-in-out-cubicenter-ease-in-out-cubicexit-ease-in-out-cubicloop-ease-in-out-cubicease-in-quartenter-ease-in-quartexit-ease-in-quartloop-ease-in-quartease-out-quartenter-ease-out-quartexit-ease-out-quartloop-ease-out-quartease-in-out-quartenter-ease-in-out-quartexit-ease-in-out-quartloop-ease-in-out-quartlinearenter-ease-linearexit-ease-linearloop-ease-linearease-springenter-ease-springexit-ease-springloop-ease-spring"
      },
      {
        "title": "Spring Easing",
        "body": "Spring easing creates natural, physics-based bouncy animations. Use the built-in ease-spring easing or create custom springs with configurable parameters.\n\n// Default spring easing\n<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>\n\n// Per-animation-type spring\n<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>\n  Spring entrance, smooth exit\n</div>\n\n// Custom spring with parameters: ease-spring/mass/stiffness/damping\n<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>\n  Custom spring (mass=1, stiffness=100, damping=10)\n</h1>\n\n// More bouncy spring (lower damping)\n<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>\n  Extra bouncy!\n</div>\n\n// Stiffer spring (higher stiffness, faster)\n<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>\n  Snappy spring\n</div>\n\n// Per-animation-type custom springs\n<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')}>\n  Different springs for enter and exit\n</div>\n\nSpring parameters:\n\nParameterDescriptionEffect when increasedDefaultmassMass of the springSlower, more inertia1stiffnessSpring stiffnessFaster, snappier100dampingDamping coefficientLess bounce, smoother10\n\nCommon spring presets:\n\n// Gentle bounce (default)\nease-spring/1/100/10\n\n// Extra bouncy\nease-spring/1/170/8\n\n// Snappy (no bounce)\nease-spring/1/200/15\n\n// Slow and bouncy\nease-spring/2/100/8\n\n// Fast and tight\nease-spring/0.5/300/20\n\nHow spring works:\n\nDefault ease-spring - Uses a pre-calculated spring curve optimized for most use cases\nCustom ease-spring/mass/stiffness/damping - Generates a physics-based spring curve using the damped harmonic oscillator formula\nThe spring automatically calculates its ideal duration to reach the final state\nWorks with all animation types: ease-spring, enter-ease-spring, exit-ease-spring, loop-ease-spring"
      },
      {
        "title": "Combining Enter and Exit",
        "body": "You can use both enter and exit animations on the same element:\n\nexport default function EnterExit({ tw, title }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>\n      {/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}\n      <h1 style={tw('text-8xl font-bold text-white enter-fade-in/0/500 exit-fade-out/2500/500')}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nThe opacities from multiple animations are multiplied together, so you get smooth transitions that combine properly."
      },
      {
        "title": "Staggered Animations",
        "body": "Create sequenced animations by offsetting start times:\n\nexport default function StaggeredList({ tw, items }) {\n  return (\n    <div style={tw('flex flex-col gap-4')}>\n      {/* First item: starts at 0ms, lasts 300ms */}\n      <div style={tw('ease-out enter-fade-in-left/0/300')}>\n        {items[0]}\n      </div>\n\n      {/* Second item: starts at 100ms, lasts 300ms */}\n      <div style={tw('ease-out enter-fade-in-left/100/300')}>\n        {items[1]}\n      </div>\n\n      {/* Third item: starts at 200ms, lasts 300ms */}\n      <div style={tw('ease-out enter-fade-in-left/200/300')}>\n        {items[2]}\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Dynamic Staggering",
        "body": "For dynamic lists, calculate the timing programmatically:\n\nexport default function DynamicStagger({ tw, items }) {\n  return (\n    <div style={tw('flex flex-col gap-4')}>\n      {items.map((item, i) => {\n        const start = i * 100;      // Each item starts 100ms later\n        const duration = 300;       // Each animation lasts 300ms\n\n        return (\n          <div\n            key={i}\n            style={tw(`ease-out enter-fade-in-up/${start}/${duration}`)}\n          >\n            {item}\n          </div>\n        );\n      })}\n    </div>\n  );\n}"
      },
      {
        "title": "Intro Sequence",
        "body": "export default function IntroVideo({ tw, title, subtitle, logo }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>\n      {/* Logo appears first */}\n      <img\n        src={logo}\n        style={tw('h-20 mb-8 ease-out enter-scale-in/0/300')}\n      />\n\n      {/* Title bounces in */}\n      <h1 style={tw('text-7xl font-bold text-white ease-out enter-bounce-in-up/200/500')}>\n        {title}\n      </h1>\n\n      {/* Subtitle fades in last */}\n      <p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/700')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}"
      },
      {
        "title": "Text Reveal",
        "body": "export default function TextReveal({ tw, words }) {\n  return (\n    <div style={tw('flex flex-wrap gap-2 justify-center')}>\n      {words.split(' ').map((word, i) => (\n        <span\n          key={i}\n          style={tw(`text-4xl font-bold ease-out enter-fade-in-up/${i * 100}/200`)}\n        >\n          {word}\n        </span>\n      ))}\n    </div>\n  );\n}"
      },
      {
        "title": "Looping Background Element",
        "body": "export default function AnimatedBackground({ tw, children }) {\n  return (\n    <div style={tw('relative w-full h-full')}>\n      {/* Floating background circles */}\n      <div style={tw('absolute top-10 left-10 w-20 h-20 rounded-full bg-white/10 loop-float/2000')} />\n      <div style={tw('absolute bottom-20 right-20 w-32 h-32 rounded-full bg-white/10 loop-fade/1500')} />\n\n      {/* Main content */}\n      <div style={tw('relative z-10')}>\n        {children}\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Full Enter/Exit Animation",
        "body": "export default function FullAnimation({ tw, title }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>\n      {/* Enter: starts at 0, lasts 400ms. Exit: starts at 2600ms, lasts 400ms */}\n      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400 exit-fade-out-up/2600/400')}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "Programmatic Animations",
        "body": "For complete control beyond animation classes, use progress and frame directly."
      },
      {
        "title": "Available Props",
        "body": "PropTypeDescriptionprogressnumber0 to 1 through the video (0% to 100%)framenumberCurrent frame number (0, 1, 2, ... totalFrames-1)\n\nThese are only available in video templates. Use them when animation classes aren't flexible enough."
      },
      {
        "title": "Using frame",
        "body": "export default function FrameAnimation({ tw, frame, title }) {\n  // Color cycling using frame number\n  const hue = (frame * 5) % 360; // Cycle through colors\n\n  // Pulsing based on frame\n  const fps = 30;\n  const pulse = Math.sin(frame / fps * Math.PI * 2) * 0.2 + 0.8; // 0.6 to 1.0\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>\n      <h1 style={{\n        ...tw('text-8xl font-bold'),\n        color: `hsl(${hue}, 70%, 60%)`,\n        transform: `scale(${pulse})`\n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "Using progress",
        "body": "export default function ProgressAnimation({ tw, progress, title }) {\n  // Custom fade based on progress\n  const opacity = progress < 0.3 ? progress / 0.3 : 1;\n\n  // Custom scale based on progress\n  const scale = 0.8 + progress * 0.2; // 0.8 to 1.0\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>\n      <h1 style={{\n        ...tw('text-8xl font-bold text-white'),\n        opacity,\n        transform: `scale(${scale})`\n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "Custom Easing",
        "body": "export default function CustomEasing({ tw, progress, title }) {\n  // Smoothstep easing\n  const eased = progress * progress * (3 - 2 * progress);\n\n  // Elastic easing\n  const elastic = Math.pow(2, -10 * progress) * Math.sin((progress - 0.075) * (2 * Math.PI) / 0.3) + 1;\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full')}>\n      <h1 style={{\n        ...tw('text-8xl font-bold'),\n        opacity: eased,\n        transform: `translateY(${(1 - elastic) * 100}px)`\n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "When to Use Programmatic Animations",
        "body": "Use progress/frame instead of animation classes when you need:\n\nCustom easing functions (elastic, bounce with specific curves beyond built-in ease-spring)\nColor cycling or gradients based on time\nMathematical animations (sine waves, spirals, etc.)\nComplex multi-property animations that need precise coordination\nConditional logic based on specific frame numbers\n\nFor everything else, prefer animation classes - they're simpler and more maintainable."
      },
      {
        "title": "Animating Along Paths",
        "body": "Animate elements along SVG paths with proper rotation using built-in path helpers:\n\nexport default function PathFollowing({ tw, progress, path }) {\n  // Follow a quadratic Bezier curve - one line!\n  const rocket = path.followQuadratic(\n    { x: 200, y: 400 },   // Start point\n    { x: 960, y: 150 },   // Control point\n    { x: 1720, y: 400 },  // End point\n    progress\n  );\n\n  return (\n    <div style={{ display: 'flex', ...tw('relative w-full h-full bg-gray-900') }}>\n      {/* Draw the path (optional) */}\n      <svg width=\"1920\" height=\"1080\" style={{ position: 'absolute' }}>\n        <path\n          d=\"M 200 400 Q 960 150 1720 400\"\n          stroke=\"rgba(255,255,255,0.2)\"\n          strokeWidth={2}\n          fill=\"none\"\n        />\n      </svg>\n\n      {/* Element following the path */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: rocket.x,\n          top: rocket.y,\n          transform: `translate(-50%, -50%) rotate(${rocket.angle}deg)`,\n          fontSize: '48px'\n        }}\n      >\n        🚀\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Text Path Animations",
        "body": "Combine textPath helpers with animation classes to create animated text along curves:\n\nRotating text around a circle:\n\nexport default function RotatingCircleText({ tw, textPath, progress }) {\n  return (\n    <div style={tw('relative w-full h-full bg-black')}>\n      {/* Text rotates around circle using progress */}\n      {textPath.onCircle(\n        \"SPINNING TEXT • AROUND • \",\n        960,      // center x\n        540,      // center y\n        400,      // radius\n        progress, // rotation offset (0-1 animates full rotation)\n        {\n          fontSize: \"3xl\",\n          fontWeight: \"bold\",\n          color: \"yellow-300\"\n        }\n      )}\n    </div>\n  );\n}\n\nAnimated text reveal along a path:\n\nexport default function PathTextReveal({ tw, textPath, progress }) {\n  // Create custom path follower that animates position\n  const pathFollower = (t) => {\n    // Only show characters up to current progress\n    const visibleProgress = progress * 1.5; // Extend range for smooth reveal\n    const opacity = t < visibleProgress ? 1 : 0;\n\n    // Follow quadratic curve\n    const pos = {\n      x: (1 - t) * (1 - t) * 200 + 2 * (1 - t) * t * 960 + t * t * 1720,\n      y: (1 - t) * (1 - t) * 400 + 2 * (1 - t) * t * 150 + t * t * 400,\n      angle: 0\n    };\n\n    return { ...pos, opacity };\n  };\n\n  return (\n    <div style={tw('relative w-full h-full bg-gray-900')}>\n      {textPath.onPath(\n        \"REVEALING TEXT\",\n        pathFollower,\n        {\n          fontSize: \"4xl\",\n          fontWeight: \"bold\",\n          color: \"blue-300\"\n        }\n      ).map((char, i) => (\n        <div key={i} style={{ ...char.props.style, opacity: char.props.style.opacity || 1 }}>\n          {char}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nStaggered character entrance:\n\nexport default function StaggeredCircleText({ tw, textPath }) {\n  const text = \"HELLO WORLD\";\n\n  return (\n    <div style={tw('relative w-full h-full bg-slate-900')}>\n      {textPath.onCircle(\n        text,\n        960, 540, 400, 0,\n        { fontSize: \"4xl\", fontWeight: \"bold\", color: \"white\" }\n      ).map((char, i) => {\n        // Stagger fade-in: each character starts 50ms later\n        const staggerDelay = i * 50;\n        return (\n          <div\n            key={i}\n            style={{\n              ...char.props.style,\n              ...tw(`enter-fade-in/${staggerDelay}/300 enter-scale-100/${staggerDelay}/300`)\n            }}\n          >\n            {char.props.children}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nText with bounce entrance along arc:\n\nexport default function BouncyArcText({ tw, textPath }) {\n  return (\n    <div style={tw('relative w-full h-full bg-gradient-to-br from-purple-600 to-blue-500')}>\n      {/* Draw the arc path */}\n      <svg width=\"1920\" height=\"1080\" style={{ position: 'absolute' }}>\n        <path\n          d=\"M 300 900 A 600 600 0 0 1 1620 900\"\n          stroke=\"rgba(255,255,255,0.2)\"\n          strokeWidth={2}\n          fill=\"none\"\n          strokeDasharray=\"5 5\"\n        />\n      </svg>\n\n      {/* Text follows arc with staggered bounce */}\n      {textPath.onArc(\n        \"BOUNCING ON ARC\",\n        960,  // cx\n        300,  // cy\n        600,  // radius\n        180,  // start angle\n        360,  // end angle\n        { fontSize: \"3xl\", fontWeight: \"bold\", color: \"white\" }\n      ).map((char, i) => (\n        <div\n          key={i}\n          style={{\n            ...char.props.style,\n            ...tw(`ease-out enter-bounce-in-up/${i * 80}/500`)\n          }}\n        >\n          {char.props.children}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nLoop animation with text on curve:\n\nexport default function LoopingCurveText({ tw, textPath, frame }) {\n  // Calculate wave effect using frame\n  const waveOffset = Math.sin(frame / 30 * Math.PI * 2) * 0.1;\n\n  return (\n    <div style={tw('relative w-full h-full bg-black')}>\n      {textPath.onQuadratic(\n        \"WAVY TEXT\",\n        { x: 200, y: 400 },\n        { x: 960, y: 150 },\n        { x: 1720, y: 400 },\n        { fontSize: \"4xl\", fontWeight: \"bold\", color: \"pink-300\" }\n      ).map((char, i) => (\n        <div\n          key={i}\n          style={{\n            ...char.props.style,\n            transform: `${char.props.style.transform} translateY(${Math.sin((i + frame) / 5) * 10}px)`\n          }}\n        >\n          {char.props.children}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nTips for animating text paths:\n\nUse progress for smooth rotation on circles and arcs\nMap over returned characters to apply individual animations\nCombine with animation classes like enter-fade-in, enter-bounce-in, etc.\nStagger character animations by calculating delays: i * delayMs\nUse frame for continuous effects like waves or pulsing\nPreserve the original transform when adding animations: transform: '${char.props.style.transform} ...'\n\nCommon path types:\n\nQuadratic Bezier (Q command):\n\n// Position: (1-t)²·P0 + 2(1-t)t·P1 + t²·P2\nfunction pointOnQuadraticBezier(p0, p1, p2, t) {\n  const x = (1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;\n  const y = (1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;\n  return { x, y };\n}\n\n// Tangent angle\nfunction angleOnQuadraticBezier(p0, p1, p2, t) {\n  const dx = 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x);\n  const dy = 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y);\n  return Math.atan2(dy, dx) * (180 / Math.PI);\n}\n\nCubic Bezier (C command):\n\n// Position: (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3\nfunction pointOnCubicBezier(p0, p1, p2, p3, t) {\n  const mt = 1 - t;\n  const mt2 = mt * mt;\n  const mt3 = mt2 * mt;\n  const t2 = t * t;\n  const t3 = t2 * t;\n  const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;\n  const y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y;\n  return { x, y };\n}\n\n// Tangent angle\nfunction angleOnCubicBezier(p0, p1, p2, p3, t) {\n  const mt = 1 - t;\n  const mt2 = mt * mt;\n  const t2 = t * t;\n  const dx = -3 * mt2 * p0.x + 3 * mt2 * p1.x - 6 * mt * t * p1.x - 3 * t2 * p2.x + 6 * mt * t * p2.x + 3 * t2 * p3.x;\n  const dy = -3 * mt2 * p0.y + 3 * mt2 * p1.y - 6 * mt * t * p1.y - 3 * t2 * p2.y + 6 * mt * t * p2.y + 3 * t2 * p3.y;\n  return Math.atan2(dy, dx) * (180 / Math.PI);\n}\n\nCircle:\n\nfunction pointOnCircle(cx, cy, radius, angleRadians) {\n  return {\n    x: cx + radius * Math.cos(angleRadians),\n    y: cy + radius * Math.sin(angleRadians)\n  };\n}\n\n// Usage\nconst angleRadians = progress * Math.PI * 2;\nconst pos = pointOnCircle(300, 300, 100, angleRadians);\nconst tangentAngle = (angleRadians * 180 / Math.PI) + 90; // Tangent is perpendicular\n\nTips:\n\nUse progress (0-1) for smooth animation\nThe translate(-50%, -50%) centers the element on the path\nCombine rotation with the translate: translate(-50%, -50%) rotate(${angle}deg)\nFor text following a path, you can animate individual characters at different progress values"
      },
      {
        "title": "SVG Stroke Animations",
        "body": "Animate SVG path strokes with the stroke-dash classes, perfect for drawing or erasing line art, icons, and illustrations."
      },
      {
        "title": "How It Works",
        "body": "SVG stroke animations use strokeDasharray and strokeDashoffset CSS properties to create drawing effects:\n\nEnter animations - Draw the stroke from start to finish\nExit animations - Erase the stroke from finish to start\nLoop animations - Continuously draw and erase"
      },
      {
        "title": "Format",
        "body": "All stroke-dash animations require the path length in brackets:\n\nenter-stroke-dash-[length]/start/duration\nexit-stroke-dash-[length]/start/duration\nloop-stroke-dash-[length]/duration"
      },
      {
        "title": "Basic Examples",
        "body": "export default function SVGAnimation({ tw }) {\n  return (\n    <svg width=\"400\" height=\"200\" viewBox=\"0 0 400 200\">\n      {/* Draw a curve over 1 second */}\n      <path\n        d=\"M10 150 Q 95 10 180 150\"\n        stroke=\"black\"\n        strokeWidth={4}\n        fill=\"none\"\n        style={tw('enter-stroke-dash-[300]/0/1000')}\n      />\n    </svg>\n  );\n}"
      },
      {
        "title": "Enter Animations (Drawing)",
        "body": "Draw strokes from 0% to 100%:\n\n// Draw a 300px path over 1 second\n<path style={tw('enter-stroke-dash-[300]/0/1000')} />\n\n// Draw with spring easing\n<path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} />\n\n// Stagger multiple paths\n<path style={tw('enter-stroke-dash-[200]/0/600')} />\n<path style={tw('enter-stroke-dash-[200]/200/600')} />\n<path style={tw('enter-stroke-dash-[200]/400/600')} />"
      },
      {
        "title": "Exit Animations (Erasing)",
        "body": "Erase strokes from 100% to 0%:\n\n// Erase starting at 2000ms, lasting 500ms\n<path style={tw('exit-stroke-dash-[300]/2000/500')} />\n\n// Draw then erase the same path\n<path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />"
      },
      {
        "title": "Loop Animations",
        "body": "Continuously draw and erase:\n\n// Loop every 2 seconds (draws in first half, erases in second half)\n<path style={tw('loop-stroke-dash-[300]/2000')} />\n\n// Faster loop\n<path style={tw('loop-stroke-dash-[200]/1000')} />"
      },
      {
        "title": "Getting Path Length",
        "body": "To find the path length for your SVG:\n\n// In browser console or component:\nconst path = document.querySelector('path');\nconst length = path.getTotalLength();\nconsole.log(length); // e.g., 347.89\n\nThen use that value:\n\n<path style={tw('enter-stroke-dash-[347.89]/0/1000')} />"
      },
      {
        "title": "Complete Example",
        "body": "export default function DrawingEffect({ tw }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>\n      <svg width=\"600\" height=\"400\" viewBox=\"0 0 600 400\">\n        {/* Checkmark icon drawn in sequence */}\n        <path\n          d=\"M100 200 L 200 300 L 400 100\"\n          stroke=\"#10b981\"\n          strokeWidth={8}\n          fill=\"none\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          style={tw('ease-out enter-stroke-dash-[600]/0/1200')}\n        />\n\n        {/* Circle drawn after checkmark */}\n        <circle\n          cx=\"250\"\n          cy=\"200\"\n          r=\"150\"\n          stroke=\"#10b981\"\n          strokeWidth={6}\n          fill=\"none\"\n          style={tw('ease-out enter-stroke-dash-[942]/1000/1000')}\n        />\n      </svg>\n    </div>\n  );\n}"
      },
      {
        "title": "Combining with Other Animations",
        "body": "Stroke animations work alongside other animation classes:\n\n// Fade in while drawing\n<path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} />\n\n// Draw with pulsing color\n<svg>\n  <path\n    stroke=\"url(#gradient)\"\n    style={tw('enter-stroke-dash-[500]/0/1500')}\n  />\n  <defs>\n    <linearGradient id=\"gradient\">\n      <stop offset=\"0%\" stopColor=\"#8b5cf6\" />\n      <stop offset=\"100%\" stopColor=\"#ec4899\" />\n    </linearGradient>\n  </defs>\n</svg>"
      },
      {
        "title": "Animated Dashed Strokes (Marching Ants)",
        "body": "For marching ants or animated dashed patterns, use frame or progress directly instead of animation classes:\n\nexport default function MarchingAnts({ tw, frame }) {\n  // Calculate animated offset (loops every 30 frames)\n  const dashOffset = -(frame % 30) * 2;\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>\n      <svg width=\"600\" height=\"400\" viewBox=\"0 0 600 400\">\n        {/* Marching ants border */}\n        <rect\n          x=\"50\"\n          y=\"50\"\n          width=\"500\"\n          height=\"300\"\n          fill=\"none\"\n          stroke=\"#3b82f6\"\n          strokeWidth={3}\n          strokeDasharray=\"10 5\"\n          strokeDashoffset={dashOffset}\n        />\n\n        {/* Animated circle with different speed */}\n        <circle\n          cx=\"300\"\n          cy=\"200\"\n          r=\"80\"\n          fill=\"none\"\n          stroke=\"#10b981\"\n          strokeWidth={4}\n          strokeDasharray=\"15 8\"\n          strokeDashoffset={dashOffset * 1.5}\n        />\n      </svg>\n    </div>\n  );\n}\n\nTips:\n\nstrokeDasharray=\"10 5\" - 10px dash, 5px gap\nstrokeDashoffset={dashOffset} - animates the pattern position\nNegative offset moves forward, positive moves backward\nDifferent speeds: multiply by different values (e.g., dashOffset * 2)\n\nThis technique is different from stroke-dash classes:\n\nstroke-dash classes - Draw/erase the stroke (reveal animation)\nMarching ants - Move a dashed pattern along the stroke"
      },
      {
        "title": "Performance Tips",
        "body": "Use Tailwind classes when possible - they're optimized for the renderer\nAvoid too many nested animations - each adds computation per frame\nUse loop animations sparingly - they're computed every frame\nPrefer opacity and transform - they're the most performant properties"
      },
      {
        "title": "Next Steps",
        "body": "Templates - Creating image and video templates\nHelpers - QR codes, images, and more"
      },
      {
        "title": "Template Helpers",
        "body": "Additional helpers for creating powerful, composable templates."
      },
      {
        "title": "Overview",
        "body": "Beyond the basics, loopwind provides:\n\ntemplate() - Compose templates together\nqr() - Generate QR codes on the fly\nconfig - Access user configuration\n\nFor image embedding, see the Images page."
      },
      {
        "title": "Template Composition",
        "body": "Compose multiple templates together to create complex designs."
      },
      {
        "title": "Usage",
        "body": "export default function CompositeCard({ tw, template, title, author, avatar }) {\n  return (\n    <div style={tw('w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>\n      <div style={tw('bg-white rounded-2xl p-8 shadow-xl')}>\n        <h1 style={tw('text-4xl font-bold text-gray-900 mb-6')}>{title}</h1>\n        \n        {/* Embed another template */}\n        <div style={tw('mb-6')}>\n          {template('user-badge', {\n            name: author,\n            avatar: avatar\n          })}\n        </div>\n        \n        <p style={tw('text-gray-600')}>Published by {author}</p>\n      </div>\n    </div>\n  );\n}\n\nHow it works:\n\ntemplate(name, props) renders another installed template\nThe embedded template is rendered at its specified size\nYou can embed multiple templates in one design\nTemplates can be nested (template within a template)"
      },
      {
        "title": "Use Cases",
        "body": "1. Reusable components:\n\n// Create a logo template once, use it everywhere\n<div>{template('company-logo', { variant: 'dark' })}</div>\n\n2. Complex layouts:\n\n// Combine multiple templates into one design\n<div style={tw('grid grid-cols-2 gap-4')}>\n  {template('product-card', { product: product1 })}\n  {template('product-card', { product: product2 })}\n</div>\n\n3. Dynamic content:\n\n// Render templates based on data\n{users.map(user => \n  template('user-avatar', { name: user.name, image: user.avatar })\n)}"
      },
      {
        "title": "Best Practices",
        "body": "Keep templates focused - Each template should do one thing well\nPass minimal props - Only pass what the embedded template needs\nDocument dependencies - Note which templates are required in your README\nAvoid deep nesting - Too many nested templates can be hard to debug"
      },
      {
        "title": "QR Codes",
        "body": "Generate QR codes dynamically in your templates."
      },
      {
        "title": "Usage",
        "body": "export default function QRCard({ tw, qr, title, url }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-white p-10')}>\n      <h1 style={tw('text-4xl font-bold text-black mb-8')}>{title}</h1>\n      \n      {/* Generate QR code for the URL */}\n      <img src={qr(url)} style={tw('w-64 h-64')} />\n      \n      <p style={tw('text-gray-600 mt-4')}>{url}</p>\n    </div>\n  );\n}\n\nProps format:\n\n{\n  \"title\": \"Scan Me\",\n  \"url\": \"https://example.com\"\n}"
      },
      {
        "title": "QR Options",
        "body": "You can customize QR code appearance:\n\n// Basic QR code\n<img src={qr('https://example.com')} />\n\n// With error correction level\n<img src={qr('https://example.com', { errorCorrectionLevel: 'H' })} />\n\n// With custom size\n<img src={qr('https://example.com', { width: 512 })} />\n\nError correction levels:\n\nL - Low (~7% correction)\nM - Medium (~15% correction) - default\nQ - Quartile (~25% correction)\nH - High (~30% correction)"
      },
      {
        "title": "User Configuration",
        "body": "Access user settings from .loopwind/loopwind.json using the config prop:\n\nexport default function BrandedTemplate({ tw, config, title }) {\n  // Access custom colors from loopwind.json\n  const primaryColor = config?.colors?.brand || '#6366f1';\n  \n  return (\n    <div style={tw('w-full h-full p-12')}>\n      <h1 style={{ \n        ...tw('text-6xl font-bold'),\n        color: primaryColor \n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nUser's .loopwind/loopwind.json:\n\n{\n  \"colors\": {\n    \"brand\": \"#ff6b6b\"\n  },\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"system-ui\", \"sans-serif\"]\n  }\n}\n\nThis allows templates to adapt to user preferences and brand guidelines."
      },
      {
        "title": "Text on Path",
        "body": "Render text along curves, circles, and custom paths with automatic character positioning and rotation."
      },
      {
        "title": "Usage",
        "body": "export default function CircleText({ tw, textPath, message }) {\n  return (\n    <div style={tw('relative w-full h-full bg-slate-900')}>\n      {textPath.onCircle(\n        message,\n        960,  // center x\n        540,  // center y\n        400,  // radius\n        0,    // rotation offset (0-1)\n        {\n          fontSize: \"4xl\",\n          fontWeight: \"bold\",\n          color: \"white\",\n          letterSpacing: 0.05\n        }\n      )}\n    </div>\n  );\n}"
      },
      {
        "title": "Available Functions",
        "body": "All textPath functions return an array of positioned character elements:\n\ntextPath.onCircle(text, cx, cy, radius, offset, options?)\n\n// Text around a circle\ntextPath.onCircle(\"HELLO WORLD\", 960, 540, 400, 0, {\n  fontSize: \"4xl\",\n  color: \"white\"\n})\n\ntextPath.onPath(text, pathFollower, options?)\n\n// Text along any custom path\ntextPath.onPath(\"CUSTOM PATH\", (t) => ({\n  x: 100 + t * 800,\n  y: 200 + Math.sin(t * Math.PI) * 100,\n  angle: Math.cos(t * Math.PI) * 20\n}), {\n  fontSize: \"2xl\",\n  fontWeight: \"semibold\"\n})\n\ntextPath.onQuadratic(text, p0, p1, p2, options?)\n\n// Text along a quadratic Bezier curve\ntextPath.onQuadratic(\n  \"CURVED TEXT\",\n  { x: 200, y: 400 },   // start\n  { x: 960, y: 100 },   // control point\n  { x: 1720, y: 400 },  // end\n  { fontSize: \"3xl\", color: \"blue-300\" }\n)\n\ntextPath.onCubic(text, p0, p1, p2, p3, options?)\n\n// Text along a cubic Bezier curve\ntextPath.onCubic(\n  \"S-CURVE\",\n  { x: 200, y: 600 },   // start\n  { x: 600, y: 400 },   // control 1\n  { x: 1320, y: 800 },  // control 2\n  { x: 1720, y: 600 },  // end\n  { fontSize: \"3xl\", color: \"purple-300\" }\n)\n\ntextPath.onArc(text, cx, cy, radius, startAngle, endAngle, options?)\n\n// Text along a circular arc\ntextPath.onArc(\n  \"ARC TEXT\",\n  960,   // center x\n  540,   // center y\n  400,   // radius\n  0,     // start angle (degrees)\n  180,   // end angle (degrees)\n  { fontSize: \"2xl\", color: \"pink-300\" }\n)"
      },
      {
        "title": "Options",
        "body": "All textPath functions accept an optional options object:\n\n{\n  fontSize?: string;      // Tailwind size: \"xl\", \"2xl\", \"4xl\", etc.\n  fontWeight?: string;    // Tailwind weight: \"bold\", \"semibold\", etc.\n  color?: string;         // Tailwind color: \"white\", \"blue-500\", etc.\n  letterSpacing?: number; // Space between characters (0-1, default: 0)\n  style?: any;           // Additional inline styles\n}"
      },
      {
        "title": "Examples",
        "body": "Animated rotating text:\n\nexport default function RotatingText({ tw, textPath, progress }) {\n  return (\n    <div style={tw('relative w-full h-full bg-black')}>\n      {textPath.onCircle(\n        \"SPINNING • TEXT • \",\n        960, 540, 400,\n        progress,  // Rotate based on video progress\n        { fontSize: \"3xl\", color: \"yellow-300\" }\n      )}\n    </div>\n  );\n}\n\nMultiple text paths:\n\nexport default function MultiPath({ tw, textPath }) {\n  return (\n    <div style={tw('relative w-full h-full bg-gradient-to-br from-slate-900 to-slate-700')}>\n      {/* Text on outer circle */}\n      {textPath.onCircle(\n        \"OUTER RING\",\n        960, 540, 500, 0,\n        { fontSize: \"5xl\", fontWeight: \"bold\", color: \"white\" }\n      )}\n\n      {/* Text on inner circle */}\n      {textPath.onCircle(\n        \"inner ring\",\n        960, 540, 300, 0.5,  // offset by 50% for rotation\n        { fontSize: \"2xl\", color: \"white/60\" }\n      )}\n    </div>\n  );\n}\n\nText following a drawn path:\n\nexport default function PathText({ tw, textPath }) {\n  return (\n    <div style={tw('relative w-full h-full bg-gray-900')}>\n      {/* Draw the path */}\n      <svg width=\"1920\" height=\"1080\" style={{ position: 'absolute' }}>\n        <path\n          d=\"M 200 400 Q 960 150 1720 400\"\n          stroke=\"rgba(255,255,255,0.2)\"\n          strokeWidth={2}\n          fill=\"none\"\n        />\n      </svg>\n\n      {/* Text following the path */}\n      {textPath.onQuadratic(\n        \"FOLLOWING THE CURVE\",\n        { x: 200, y: 400 },\n        { x: 960, y: 150 },\n        { x: 1720, y: 400 },\n        { fontSize: \"3xl\", fontWeight: \"bold\", color: \"blue-300\" }\n      )}\n    </div>\n  );\n}\n\nFor animated text paths, see Text Path Animations."
      },
      {
        "title": "Reserved Prop Names",
        "body": "The following prop names are reserved and cannot be used in your template's meta.props:\n\ntw, qr, image, template - Core helpers\npath, textPath - Path and text helpers\nconfig, frame, progress - System props\n\nWhy? These names are used for loopwind's built-in helpers. Using them as prop names would cause conflicts.\n\nExample:\n\n// ❌ BAD - 'image' is reserved\nexport const meta = {\n  props: {\n    title: \"string\",\n    image: \"string\"  // Error!\n  }\n};\n\n// ✅ GOOD - Use descriptive alternatives\nexport const meta = {\n  props: {\n    title: \"string\",\n    imageUrl: \"string\",    // or imageSrc, photoUrl, etc.\n    logoUrl: \"string\"\n  }\n};\n\nIf you try to use a reserved name, you'll get a helpful error:\n\nTemplate uses reserved prop names: image\n\nTry renaming: \"image\" → \"imageUrl\" or \"imageSrc\"\n\nReserved names: tw, qr, image, template, path, textPath, config, frame, progress"
      },
      {
        "title": "All Props Reference",
        "body": "Every template receives these props:\n\nexport default function MyTemplate({\n  // Core helpers (RESERVED - cannot be used as prop names)\n  tw,        // Tailwind class converter\n  qr,        // QR code generator (this page)\n  template,  // Template composer (this page)\n  config,    // User config from loopwind.json (this page)\n  textPath,  // Text on path helpers (this page)\n\n  // Media helpers (RESERVED)\n  image,     // Image embedder → see /images\n  path,      // Path following → see /animation\n\n  // Video-specific (RESERVED - only in video templates)\n  frame,     // Current frame number → see /templates\n  progress,  // Animation progress 0-1 → see /templates\n\n  // Your custom props (use any names EXCEPT the reserved ones above)\n  ...props   // Any props from your meta.props\n}) {\n  // Your template code\n}"
      },
      {
        "title": "Next Steps",
        "body": "Embedding Images\nTemplates\nStyling with Tailwind & shadcn/ui\nCustom Fonts"
      },
      {
        "title": "Styling Templates",
        "body": "Style your templates with Tailwind utility classes and shadcn/ui's beautiful design system."
      },
      {
        "title": "Quick Start",
        "body": "export default function MyTemplate({ title, tw }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>\n      <h1 style={tw('text-7xl font-bold text-white')}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "The tw() Function",
        "body": "Every template receives a tw() function that converts Tailwind classes to inline styles compatible with Satori:\n\n// Tailwind classes\ntw('flex items-center justify-center p-8 bg-blue-500')\n\n// Converts to inline styles:\n{\n  display: 'flex',\n  alignItems: 'center',\n  justifyContent: 'center',\n  padding: '2rem',\n  backgroundColor: '#3b82f6'\n}"
      },
      {
        "title": "Basic Usage",
        "body": "export default function Banner({ title, subtitle, tw }) {\n  return (\n    <div style={tw('w-full h-full p-12 bg-gray-50')}>\n      <h1 style={tw('text-6xl font-bold text-gray-900 mb-4')}>\n        {title}\n      </h1>\n      <p style={tw('text-2xl text-gray-600')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}"
      },
      {
        "title": "Combining with Custom Styles",
        "body": "Mix Tailwind classes with custom styles using the spread operator:\n\nexport default function CustomGradient({ title, tw }) {\n  return (\n    <div\n      style={{\n        ...tw('flex flex-col items-center justify-center w-full h-full p-20'),\n        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',\n      }}\n    >\n      <h1 style={tw('text-8xl font-bold text-white')}>{title}</h1>\n    </div>\n  );\n}"
      },
      {
        "title": "shadcn/ui Design System",
        "body": "loopwind uses shadcn/ui's design system by default, providing semantic color tokens for beautiful, consistent designs."
      },
      {
        "title": "Default Color Palette",
        "body": "All templates automatically have access to these semantic colors defined in .loopwind/loopwind.json:\n\ncolors: {\n  // Primary colors\n  primary: '#18181b',           // Main brand color\n  'primary-foreground': '#fafafa',\n\n  // Secondary colors\n  secondary: '#f4f4f5',         // Subtle accents\n  'secondary-foreground': '#18181b',\n\n  // Background\n  background: '#ffffff',        // Page background\n  foreground: '#09090b',        // Main text color\n\n  // Muted\n  muted: '#f4f4f5',            // Subtle backgrounds\n  'muted-foreground': '#71717a', // Muted text\n\n  // Accent\n  accent: '#f4f4f5',           // Highlight color\n  'accent-foreground': '#18181b',\n\n  // Destructive\n  destructive: '#ef4444',       // Error/danger states\n  'destructive-foreground': '#fafafa',\n\n  // UI Elements\n  border: '#e4e4e7',           // Border color\n  input: '#e4e4e7',            // Input borders\n  ring: '#18181b',             // Focus rings\n  card: '#ffffff',             // Card background\n  'card-foreground': '#09090b',\n}"
      },
      {
        "title": "Using Semantic Colors",
        "body": "export default function SemanticCard({ title, description, price, tw }) {\n  return (\n    <div style={tw('bg-card border border-border rounded-lg p-6')}>\n      <h2 style={tw('text-card-foreground text-2xl font-bold mb-2')}>\n        {title}\n      </h2>\n      <p style={tw('text-muted-foreground mb-4')}>\n        {description}\n      </p>\n      <div style={tw('text-primary text-3xl font-bold')}>\n        ${price}\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Opacity Modifiers",
        "body": "Use Tailwind's slash syntax for opacity with any color:\n\nexport default function OpacityExample({ tw }) {\n  return (\n    <div style={tw('bg-primary/50')}>          {/* 50% opacity */}\n      <p style={tw('text-muted-foreground/75')}> {/* 75% opacity */}\n        Subtle text\n      </p>\n      <div style={tw('border border-border/30')}> {/* 30% opacity */}\n        Faint border\n      </div>\n    </div>\n  );\n}\n\nSupported syntax:\n\nbg-{color}/{opacity} - Background with opacity\ntext-{color}/{opacity} - Text with opacity\nborder-{color}/{opacity} - Border with opacity"
      },
      {
        "title": "Text Hierarchy",
        "body": "// Primary text\ntw('text-foreground')\n\n// Secondary/muted text\ntw('text-muted-foreground')\n\n// Accent/brand text\ntw('text-primary')\n\n// Destructive/error text\ntw('text-destructive')"
      },
      {
        "title": "Backgrounds",
        "body": "// Page background\ntw('bg-background')\n\n// Card/elevated surfaces\ntw('bg-card')\n\n// Subtle backgrounds\ntw('bg-muted')\n\n// Accent backgrounds\ntw('bg-accent')"
      },
      {
        "title": "Layout",
        "body": "Display: flex, inline-flex, block, inline-block, hidden\nFlex Direction: flex-row, flex-col, flex-row-reverse, flex-col-reverse\nJustify: justify-start, justify-end, justify-center, justify-between, justify-around\nAlign: items-start, items-end, items-center, items-baseline, items-stretch"
      },
      {
        "title": "Spacing",
        "body": "Padding: p-{n}, px-{n}, py-{n}, pt-{n}, pb-{n}, pl-{n}, pr-{n}\nMargin: m-{n}, mx-{n}, my-{n}, mt-{n}, mb-{n}, ml-{n}, mr-{n}\nGap: gap-{n}, gap-x-{n}, gap-y-{n}\nSizes: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64\n\nExamples:\n\ntw('p-4')      // padding: 1rem\ntw('px-8')     // paddingLeft: 2rem, paddingRight: 2rem\ntw('m-6')      // margin: 1.5rem\ntw('gap-4')    // gap: 1rem"
      },
      {
        "title": "Sizing",
        "body": "Width: w-{n}, w-full, w-screen, w-1/2, w-1/3, w-2/3\nHeight: h-{n}, h-full, h-screen\n\nExamples:\n\ntw('w-full')   // width: 100%\ntw('h-64')     // height: 16rem\ntw('w-1/2')    // width: 50%"
      },
      {
        "title": "Typography",
        "body": "Font Size: text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, text-4xl, text-5xl, text-6xl, text-7xl, text-8xl, text-9xl\nFont Weight: font-thin, font-light, font-normal, font-medium, font-semibold, font-bold, font-extrabold, font-black\nText Align: text-left, text-center, text-right\nLine Height: leading-none, leading-tight, leading-normal, leading-relaxed, leading-loose"
      },
      {
        "title": "Colors",
        "body": "All standard Tailwind colors plus shadcn semantic colors:\n\nStandard colors:\n\ntext-{color}-{shade}, bg-{color}-{shade}, border-{color}-{shade}\nColors: red, blue, green, yellow, purple, pink, gray, indigo, teal, orange\nShades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900\n\nshadcn semantic colors:\n\ntext-foreground, text-primary, text-muted-foreground, text-destructive\nbg-background, bg-card, bg-muted, bg-accent, bg-primary\nborder-border, border-input\n\ntw('text-blue-500')        // Standard Tailwind color\ntw('bg-purple-600')        // Standard Tailwind color\ntw('text-primary')         // shadcn semantic color\ntw('bg-card')              // shadcn semantic color"
      },
      {
        "title": "Position & Layout",
        "body": "Position: relative, absolute, fixed, sticky\nInset: inset-0, top-0, bottom-0, left-0, right-0\nZ-Index: z-0, z-10, z-20, z-30, z-40, z-50"
      },
      {
        "title": "Borders",
        "body": "Border Width: border, border-{n}, border-t, border-b, border-l, border-r\nBorder Radius: rounded, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-3xl, rounded-full\nBorder Color: border-{color}-{shade}, border-border, border-input"
      },
      {
        "title": "Effects",
        "body": "Shadow: shadow-sm, shadow, shadow-md, shadow-lg, shadow-xl, shadow-2xl\nOpacity: opacity-0, opacity-25, opacity-50, opacity-75, opacity-100"
      },
      {
        "title": "Filters",
        "body": "Blur: blur-none, blur-sm, blur, blur-md, blur-lg, blur-xl\nBrightness: brightness-0, brightness-50, brightness-100, brightness-150, brightness-200\nContrast: contrast-0, contrast-50, contrast-100, contrast-150, contrast-200"
      },
      {
        "title": "Linear Gradients",
        "body": "// Gradient direction\ntw('bg-gradient-to-r')      // left to right\ntw('bg-gradient-to-br')     // top-left to bottom-right\ntw('bg-gradient-to-t')      // bottom to top\n\n// Gradient colors\ntw('from-blue-500')         // Start color\ntw('via-purple-500')        // Middle color\ntw('to-pink-500')           // End color\n\n// Complete gradient\ntw('bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500')"
      },
      {
        "title": "Gradient Examples",
        "body": "export default function GradientCard({ title, tw }) {\n  return (\n    <div style={tw('w-full h-full bg-gradient-to-br from-cyan-500 to-blue-600 p-12')}>\n      <h1 style={tw('text-white text-6xl font-bold')}>\n        {title}\n      </h1>\n    </div>\n  );\n}"
      },
      {
        "title": "Custom Theme Colors",
        "body": "You can override the default shadcn colors or add your own custom colors in .loopwind/loopwind.json:\n\n{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#3b82f6\",\n      \"primary-foreground\": \"#ffffff\",\n      \"accent\": \"#10b981\",\n      \"brand\": \"#ff6b6b\"\n    }\n  }\n}\n\nThen use these custom colors in your templates:\n\ntw('text-brand')    // Uses your custom brand color\ntw('bg-primary')    // Uses your custom primary color\ntw('bg-accent')     // Uses your custom accent color"
      },
      {
        "title": "Auto-Detection from tailwind.config.js",
        "body": "loopwind automatically detects and loads your project's Tailwind configuration:\n\nyour-project/\n├── tailwind.config.js  ← Automatically detected\n└── .loopwind/\n    ├── loopwind.json\n    └── templates/\n\nThis includes:\n\nCustom colors\nCustom spacing values\nCustom fonts\nTheme extensions\nCustom utilities"
      },
      {
        "title": "Complete Example",
        "body": "export default function ModernCard({ \n  tw, \n  image,\n  title, \n  description, \n  category,\n  author,\n  avatar \n}) {\n  return (\n    <div style={tw('w-full h-full bg-card')}>\n      {/* Hero image */}\n      <div style={tw('relative h-2/3')}>\n        <img \n          src={image(hero)} \n          style={tw('w-full h-full object-cover')}\n        />\n        {/* Category badge */}\n        <div style={tw('absolute top-4 left-4 bg-primary/90 backdrop-blur px-4 py-2 rounded-full')}>\n          <span style={tw('text-sm font-semibold text-primary-foreground')}>\n            {category}\n          </span>\n        </div>\n      </div>\n      \n      {/* Content */}\n      <div style={tw('h-1/3 p-8 flex flex-col justify-between')}>\n        <div>\n          <h2 style={tw('text-3xl font-bold text-foreground mb-2')}>\n            {title}\n          </h2>\n          <p style={tw('text-muted-foreground line-clamp-2')}>\n            {description}\n          </p>\n        </div>\n        \n        {/* Author */}\n        <div style={tw('flex items-center gap-3')}>\n          <img \n            src={image(avatar)} \n            style={tw('w-10 h-10 rounded-full border-2 border-border')}\n          />\n          <span style={tw('text-sm text-muted-foreground')}>\n            {author}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}"
      },
      {
        "title": "Why This Approach?",
        "body": "Semantic naming: text-primary instead of text-blue-600\nConsistency: All templates use the same design language\nFlexibility: Easy to customize entire theme\nAccessibility: Pre-tested color contrasts\nModern: Same system as shadcn/ui components\nFamiliar: Standard Tailwind syntax"
      },
      {
        "title": "Next Steps",
        "body": "Custom Fonts\nBuilt-in Helpers\nTemplates\nEmbedding Images"
      },
      {
        "title": "Font Handling in loopwind",
        "body": "The recommended way to use fonts is through loopwind.json - configure fonts once, use everywhere."
      },
      {
        "title": "Using Fonts from loopwind.json (Recommended)",
        "body": "Configure fonts in your .loopwind/loopwind.json and use Tailwind classes in templates."
      },
      {
        "title": "Simple Setup",
        "body": "Define font families in .loopwind/loopwind.json without loading custom fonts (uses system fonts):\n\n{\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"system-ui\", \"-apple-system\", \"sans-serif\"],\n    \"serif\": [\"Georgia\", \"serif\"],\n    \"mono\": [\"Courier New\", \"monospace\"]\n  }\n}\n\nTemplate usage:\n\nexport default function({ title, tw }) {\n  return (\n    <div style={tw('w-full h-full')}>\n      {/* Uses fonts.sans from loopwind.json */}\n      <h1 style={tw('font-sans text-6xl font-bold')}>\n        {title}\n      </h1>\n\n      {/* Uses fonts.mono from loopwind.json */}\n      <code style={tw('font-mono text-sm')}>\n        {code}\n      </code>\n    </div>\n  );\n}\n\nResult: Uses system fonts, falls back to Inter for rendering."
      },
      {
        "title": "Complete Setup (With Font Files)",
        "body": "Load custom font files for brand-specific typography in .loopwind/loopwind.json:\n\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"system-ui\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    },\n    \"mono\": {\n      \"family\": [\"JetBrains Mono\", \"monospace\"],\n      \"files\": [\n        { \"path\": \"./fonts/JetBrainsMono-Regular.woff\", \"weight\": 400 }\n      ]\n    }\n  }\n}\n\nProject structure:\n\nyour-project/\n├── .loopwind/\n│   ├── loopwind.json\n│   └── templates/\n└── fonts/\n    ├── Inter-Regular.woff\n    ├── Inter-Bold.woff\n    └── JetBrainsMono-Regular.woff\n\nTemplate usage (same as before):\n\n<h1 style={tw('font-sans font-bold')}>\n  {/* Uses Inter Bold from loopwind.json */}\n  {title}\n</h1>\n\nAvailable classes:\n\nfont-sans - Uses fonts.sans from loopwind.json\nfont-serif - Uses fonts.serif from loopwind.json\nfont-mono - Uses fonts.mono from loopwind.json\n\nSupported formats:\n\n✅ WOFF (.woff) - Recommended for best compatibility\n✅ TTF (.ttf) - Also supported\n✅ OTF (.otf) - Also supported\n❌ WOFF2 (.woff2) - Not supported by renderer"
      },
      {
        "title": "Font Loading Priority",
        "body": "loopwind loads fonts in this order:\n\nloopwind.json fonts (if configured with files)\nBundled Inter fonts (included with CLI)\n\nThis ensures fonts work out of the box with no configuration."
      },
      {
        "title": "Default Fonts",
        "body": "If no fonts are configured, loopwind uses Inter (Regular 400, Bold 700) which is bundled with the CLI. This means fonts work offline with no configuration required."
      },
      {
        "title": "Best Practices",
        "body": "✅ Use loopwind.json for project-wide fonts - Configure once, use everywhere\n✅ Use font classes - tw('font-sans') instead of fontFamily: 'Inter'\n✅ Include fallbacks - Always add system fonts: [\"Inter\", \"system-ui\", \"sans-serif\"]\n✅ Match names - First font in family array is used as the loaded font name\n✅ Relative paths - Font paths are relative to loopwind.json location"
      },
      {
        "title": "Minimal Setup (System Fonts)",
        "body": "{\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"-apple-system\", \"sans-serif\"]\n  }\n}\n\nUses system Inter if available, falls back to Noto Sans for rendering."
      },
      {
        "title": "Brand Fonts Setup",
        "body": "{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Montserrat\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Montserrat-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Montserrat-Bold.woff\", \"weight\": 700 }\n      ]\n    }\n  }\n}\n\nLoads and uses Montserrat for all templates."
      },
      {
        "title": "Multi-Font Setup",
        "body": "{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    },\n    \"serif\": {\n      \"family\": [\"Playfair Display\", \"serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Playfair-Regular.woff\", \"weight\": 400 }\n      ]\n    },\n    \"mono\": {\n      \"family\": [\"Fira Code\", \"monospace\"],\n      \"files\": [\n        { \"path\": \"./fonts/FiraCode-Regular.woff\", \"weight\": 400 }\n      ]\n    }\n  }\n}\n\nLoads different fonts for each style class."
      },
      {
        "title": "External Font URLs",
        "body": "Load fonts directly from CDNs without downloading files:\n\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff\",\n          \"weight\": 400\n        },\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff\",\n          \"weight\": 700\n        }\n      ]\n    }\n  }\n}\n\nYou can also mix local and external fonts:\n\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff\",\n          \"weight\": 700\n        }\n      ]\n    }\n  }\n}\n\nNote: Use WOFF format (.woff) for best compatibility. WOFF2 is not supported by the underlying renderer."
      },
      {
        "title": "Performance",
        "body": "✅ Font caching - Fonts load once and are cached for all renders\n✅ Video optimization - 90-frame video loads fonts once, not 90 times\n✅ No CDN delays - Local fonts load instantly"
      },
      {
        "title": "Next Steps",
        "body": "Styling with Tailwind & shadcn/ui\nBuilt-in Helpers\nTemplates\nEmbedding Images"
      },
      {
        "title": "loopwind.json",
        "body": "Configure colors and fonts for all your templates in .loopwind/loopwind.json."
      },
      {
        "title": "File Location",
        "body": "your-project/\n├── .loopwind/\n│   ├── loopwind.json     ← Configuration file\n│   └── templates/"
      },
      {
        "title": "Minimal Example",
        "body": "{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#3b82f6\",\n      \"background\": \"#ffffff\"\n    }\n  }\n}"
      },
      {
        "title": "Default shadcn/ui Palette",
        "body": "{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#18181b\",\n      \"primary-foreground\": \"#fafafa\",\n      \n      \"secondary\": \"#f4f4f5\",\n      \"secondary-foreground\": \"#18181b\",\n      \n      \"background\": \"#ffffff\",\n      \"foreground\": \"#09090b\",\n      \n      \"muted\": \"#f4f4f5\",\n      \"muted-foreground\": \"#71717a\",\n      \n      \"accent\": \"#f4f4f5\",\n      \"accent-foreground\": \"#18181b\",\n      \n      \"destructive\": \"#ef4444\",\n      \"destructive-foreground\": \"#fafafa\",\n      \n      \"border\": \"#e4e4e7\",\n      \"input\": \"#e4e4e7\",\n      \"ring\": \"#18181b\",\n      \n      \"card\": \"#ffffff\",\n      \"card-foreground\": \"#09090b\"\n    }\n  }\n}"
      },
      {
        "title": "Custom Colors",
        "body": "{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#3b82f6\",\n      \"brand\": \"#ff6b6b\",\n      \"success\": \"#22c55e\",\n      \"warning\": \"#f59e0b\"\n    }\n  }\n}\n\nUse in templates:\n\ntw('text-brand')      // #ff6b6b\ntw('bg-success')      // #22c55e\ntw('border-warning')  // #f59e0b"
      },
      {
        "title": "Simple (System Fonts)",
        "body": "{\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"system-ui\", \"sans-serif\"],\n    \"serif\": [\"Georgia\", \"serif\"],\n    \"mono\": [\"Courier New\", \"monospace\"]\n  }\n}"
      },
      {
        "title": "With Font Files",
        "body": "{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"system-ui\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    }\n  }\n}\n\nPaths are relative to loopwind.json.\n\nSupported formats:\n\n✅ WOFF (.woff)\n✅ TTF (.ttf)\n✅ OTF (.otf)\n❌ WOFF2 (.woff2)"
      },
      {
        "title": "External URLs",
        "body": "{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff\",\n          \"weight\": 400\n        }\n      ]\n    }\n  }\n}"
      },
      {
        "title": "Complete Example",
        "body": "{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#6366f1\",\n      \"primary-foreground\": \"#ffffff\",\n      \"background\": \"#ffffff\",\n      \"foreground\": \"#0f172a\",\n      \"muted\": \"#f1f5f9\",\n      \"muted-foreground\": \"#64748b\",\n      \"border\": \"#e2e8f0\",\n      \"card\": \"#ffffff\",\n      \"brand\": \"#8b5cf6\"\n    }\n  },\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    },\n    \"serif\": {\n      \"family\": [\"Playfair Display\", \"serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Playfair-Regular.woff\", \"weight\": 400 }\n      ]\n    }\n  }\n}"
      },
      {
        "title": "Schema",
        "body": "{\n  \"theme\"?: {\n    \"colors\"?: {\n      [name: string]: string;  // Hex color\n    }\n  },\n  \"fonts\"?: {\n    [class: string]: string[] | {\n      family: string[];\n      files: Array<{\n        path: string;    // Local or URL\n        weight: number;  // 100-900\n      }>;\n    }\n  }\n}"
      },
      {
        "title": "Auto-Detection",
        "body": "If no loopwind.json exists, loopwind auto-detects tailwind.config.js:\n\nyour-project/\n├── tailwind.config.js  ← Auto-detected\n└── .loopwind/\n    └── templates/\n\nPriority:\n\n.loopwind/loopwind.json\ntailwind.config.js\nBuilt-in defaults"
      },
      {
        "title": "Next Steps",
        "body": "Styling - Use colors in templates\nFonts - Font configuration details\nGetting Started - Setup guide"
      }
    ],
    "body": "loopwind\n\nA CLI tool for generating images and videos from JSX templates using Tailwind CSS and Satori. Templates live in a .loopwind/ directory alongside your codebase.\n\nQuick Start\n\nLoopwind 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.\n\nInstallation\ncurl -fsSL https://loopwind.dev/install.sh | bash\n\n\nThis installs loopwind to ~/.loopwind/ and adds the loopwind command to your PATH. Requires Node.js 18+.\n\nInitialize in Your Project\n\nNavigate to any project folder and run:\n\nloopwind init\n\n\nThis creates .loopwind/loopwind.json — a configuration file with your project's theme colors.\n\nInstall AI Skill\n\nGive your AI agent expertise in loopwind:\n\nnpx skills add https://loopwind.dev/skill.md\n\n\nThis installs a skill that teaches Claude Code (or other AI agents) how to create templates, use animation classes, and render images/videos.\n\nUse with Claude Code\n\nWith the loopwind skill installed, Claude has deep knowledge of template structure, animation classes, and Tailwind CSS patterns for Satori. Just ask:\n\nCreate an OG image for my blog post about TypeScript tips\n\nCreate an animated intro video for my YouTube channel\n\n\nClaude will create optimized templates and render the final output automatically.\n\nInstall a Template\n1. Official Templates\nloopwind add image-template\nloopwind add video-template\n\n\nTemplates are installed to: .loopwind/<template>/\n\nBenefits:\n\nTemplates are local to your project\nVersion controlled with your project\nEasy to share within your team\nRender a Template\nloopwind render template-name '{\"title\":\"Hello World\",\"subtitle\":\"Built with loopwind\"}'\n\n\nor use a local props file:\n\nloopwind render template-name props.json\n\nCommands\nloopwind add <source>\n\nInstall a template from various sources:\n\n# Official templates\nloopwind add image-template\nloopwind add video-template\n\n\nThese will be downloaded to .loopwind/<template>/\n\nloopwind list\n\nList all installed templates:\n\nloopwind list\n\nloopwind render <template> <props> [options]\n\nRender an image or video:\n\n# Image with inline props\nloopwind render banner-hero '{\"title\":\"Hello World\"}'\n\n# Video with inline props\nloopwind render video-intro '{\"title\":\"Welcome\"}'\n\n# Using a props file\nloopwind render banner-hero props.json\n\n# Custom output\nloopwind render banner-hero '{\"title\":\"Hello\"}' --out custom-name.png\n\n# Different format\nloopwind render banner-hero '{\"title\":\"Hello\"}' --format jpeg\n\n\nOptions:\n\n--out, -o - Output filename (default: <template>.<ext> in current directory)\n--format - Output format: png, jpeg, svg (images only)\n--quality - JPEG quality 1-100 (default: 92)\nloopwind validate <template>\n\nValidate a template:\n\nloopwind validate banner-hero\n\n\nChecks:\n\nTemplate file exists and is valid React\nexport const meta exists and is valid\nRequired props are defined\nFonts exist (if specified)\nloopwind init\n\nInitialize loopwind in a project:\n\nloopwind init\n\n\nCreates .loopwind/loopwind.json configuration file with your project's design tokens.\n\nAnimation Classes (Video Only)\n\nUse Tailwind-style animation classes - no manual calculations needed:\n\n// Fade in: starts at 0ms, lasts 500ms\n<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>\n\n// Loop: ping effect every 500ms\n<div style={tw('loop-ping/500')} />\n\n// Combined with easing\n<h1 style={tw('ease-out enter-bounce-in-up/0/600')}>Title</h1>\n\n\nSee Animation for the complete reference.\n\nNext Steps\nTemplates\nEmbedding Images\nAnimation\nHelpers (QR, Template Composition)\nStyling with Tailwind & shadcn/ui\nCustom Fonts\nAI Agent Integration\nTemplates\n\nTemplates are React components that define your images and videos. They use Tailwind CSS for styling and export metadata that loopwind uses for rendering.\n\nInstalling Templates\nOfficial Templates\nloopwind add image-template\nloopwind add video-template\n\n\nTemplates are installed to .loopwind/<template-name>/.\n\nDirect URLs\nloopwind add https://example.com/templates/my-template.json\n\nLocal Filesystem\nloopwind add ./my-templates/banner-hero\nloopwind add /Users/you/templates/social-card\n\nImage Templates\nBasic Structure\n// .loopwind/banner-hero/template.tsx\nexport const meta = {\n  name: \"banner-hero\",\n  type: \"image\",\n  description: \"Hero banner with gradient background\",\n  size: { width: 1600, height: 900 },\n  props: { title: \"string\", subtitle: \"string\" }\n};\n\nexport default function BannerHero({ title, subtitle, tw }) {\n  return (\n    <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')}>\n      <h1 style={tw('text-7xl font-bold text-white mb-4')}>\n        {title}\n      </h1>\n      <p style={tw('text-2xl text-white/80')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}\n\nRendering Images\n# Render with inline props\nloopwind render banner-hero '{\"title\":\"Hello World\",\"subtitle\":\"Welcome\"}'\n\n# Custom output name\nloopwind render banner-hero '{\"title\":\"Hello\"}' --out custom-name.png\n\n# Different format\nloopwind render banner-hero '{\"title\":\"Hello\"}' --format jpeg --quality 95\n\n# Use a props file\nloopwind render banner-hero props.json\n\nOutput Formats\nFormat\tBest For\nPNG (default)\tTransparency, sharp text, logos\nJPEG\tPhotographs, gradients, smaller files\nSVG\tVector graphics, scalable designs\nVideo Templates\nBasic Structure\n// .loopwind/video-intro/template.tsx\nexport const meta = {\n  name: \"video-intro\",\n  type: \"video\",\n  description: \"Animated intro with bounce-in title\",\n  size: { width: 1920, height: 1080 },\n  video: { fps: 30, duration: 3 },\n  props: { title: \"string\" }\n};\n\nexport default function VideoIntro({ tw, title }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>\n      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/600')}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nRendering Videos\n# Render with inline props\nloopwind render video-intro '{\"title\":\"Welcome!\"}' --out intro.mp4\n\n# Faster encoding with FFmpeg\nloopwind render video-intro '{\"title\":\"Welcome!\"}' --ffmpeg\n\n# Higher quality (lower CRF = better)\nloopwind render video-intro '{\"title\":\"Welcome!\"}' --crf 18\n\nFPS and Duration\nvideo: { fps: 30, duration: 3 }  // 90 frames total\n\nFPS\tUse Case\n24\tCinematic look, smaller files\n30\tStandard web video\n60\tSmooth animations\nVideo-Specific Props\n\nTemplates receive these additional props:\n\nframe - Current frame number (0 to totalFrames - 1)\nprogress - Animation progress from 0 to 1\nexport default function MyVideo({ frame, progress }) {\n  // frame: 0, 1, 2, ... 89 (for 3s @ 30fps)\n  // progress: 0.0 at start, 0.5 at middle, 1.0 at end\n}\n\nEncoding Options\nEncoder\tCommand\tUse Case\nWASM (default)\tloopwind render ...\tCI/CD, no dependencies\nFFmpeg\tloopwind render ... --ffmpeg\tFaster, smaller files\n\nInstall FFmpeg: brew install ffmpeg (macOS)\n\nAnimation Classes\n\nUse Tailwind-style animation classes for videos:\n\n// Enter animations: enter-{type}/{delay}/{duration}\n<h1 style={tw('enter-fade-in/0/500')}>Fade in at start</h1>\n<h1 style={tw('enter-bounce-in-up/300/400')}>Bounce in after 300ms</h1>\n\n// Exit animations: exit-{type}/{start}/{duration}\n<div style={tw('exit-fade-out/2500/500')}>Fade out at 2.5s</div>\n\n// Loop animations: loop-{type}/{duration}\n<div style={tw('loop-float/1000')}>Continuous floating</div>\n<div style={tw('loop-spin/1000')}>Spinning</div>\n\n// Easing\n<h1 style={tw('ease-out enter-slide-left/0/500')}>Smooth slide</h1>\n\n\nSee the full Animation documentation for all classes.\n\nCommon Sizes\nSocial Media\nTwitter/X Card: 1200x675\nFacebook/OG: 1200x630\nInstagram Post: 1080x1080\nLinkedIn Post: 1200x627\nWeb Graphics\nHero Banner: 1920x1080\nBlog Header: 1600x900\nThumbnail: 640x360\nExample Templates\nOpen Graph Image\nexport const meta = {\n  name: \"og-image\",\n  type: \"image\",\n  size: { width: 1200, height: 630 },\n  props: { title: \"string\", description: \"string\" }\n};\n\nexport default function OGImage({ tw, image, title, description }) {\n  return (\n    <div style={tw('flex w-full h-full bg-white')}>\n      <div style={tw('flex-1 flex flex-col justify-between p-12')}>\n        <img src={image('logo.svg')} style={tw('h-12 w-auto')} />\n        <div>\n          <h1 style={tw('text-5xl font-bold text-gray-900 mb-4')}>{title}</h1>\n          <p style={tw('text-xl text-gray-600')}>{description}</p>\n        </div>\n        <p style={tw('text-gray-400')}>yoursite.com</p>\n      </div>\n    </div>\n  );\n}\n\nAnimated Intro\nexport const meta = {\n  name: \"animated-intro\",\n  type: \"video\",\n  size: { width: 1920, height: 1080 },\n  video: { fps: 60, duration: 3 },\n  props: { title: \"string\", subtitle: \"string\" }\n};\n\nexport default function AnimatedIntro({ tw, title, subtitle }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-background')}>\n      <h1 style={tw('text-8xl font-bold text-foreground ease-out enter-bounce-in-up/0/400')}>\n        {title}\n      </h1>\n      <p style={tw('text-2xl text-muted-foreground mt-4 ease-out enter-fade-in-up/300/400')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}\n\nNext Steps\nLayouts - Wrap templates with reusable layouts\nEmbedding Images - Using the image() helper\nAnimation - Full animation reference\nStyling - Tailwind & shadcn/ui integration\nFonts - Custom fonts\nLayouts\n\nLayouts 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.\n\nBasic Usage\nLayout Template\n\nCreate a layout template that receives children:\n\n// .loopwind/base-layout/template.tsx\nexport const meta = {\n  name: 'base-layout',\n  type: 'image',\n  size: { width: 1200, height: 630 },\n  props: {},\n};\n\nexport default function BaseLayout({ tw, children }) {\n  return (\n    <div style={tw('flex flex-col w-full h-full bg-background')}>\n      {/* Header */}\n      <div style={tw('flex items-center px-8 py-4 border-b border-border')}>\n        <span style={tw('text-2xl font-bold text-primary')}>loopwind</span>\n      </div>\n\n      {/* Content slot */}\n      <div style={tw('flex flex-1')}>\n        {children}\n      </div>\n\n      {/* Footer */}\n      <div style={tw('flex items-center justify-between px-8 py-4 border-t border-border')}>\n        <span style={tw('text-muted-foreground')}>loopwind.dev</span>\n      </div>\n    </div>\n  );\n}\n\nUsage in Templates\n\nReference the layout using a relative path:\n\n// .loopwind/blog-post/template.tsx\nexport const meta = {\n  name: 'blog-post',\n  type: 'image',\n  layout: '../base-layout', // Layout controls size\n  props: {\n    title: 'string',\n    excerpt: 'string',\n  },\n};\n\nexport default function BlogPost({ tw, title, excerpt }) {\n  return (\n    <div style={tw('flex flex-col justify-center p-12')}>\n      <h1 style={tw('text-5xl font-bold text-foreground mb-4 text-balance')}>\n        {title}\n      </h1>\n      <p style={tw('text-xl text-muted-foreground leading-relaxed')}>\n        {excerpt}\n      </p>\n    </div>\n  );\n}\n\nRender\nloopwind render blog-post '{\"title\":\"Hello World\",\"excerpt\":\"My first post\"}'\n\n\nThe output uses the layout's size (1200x630) with the child content inside.\n\nKey Concepts\nSize\n\nWhen using a layout, the layout's size controls the final output dimensions. The child template doesn't need a size property.\n\nPath Resolution\n\nUse relative paths to reference layouts:\n\nlayout: '../base-layout'      // Sibling directory\nlayout: './shared/layout'     // Subdirectory\nlayout: '../../layouts/main'  // Parent's sibling\n\nProps Flow\n\nThe layout receives:\n\nAll standard helpers (tw, image, qr, template, etc.)\nchildren prop containing the rendered child content\nAnimation context (frame, progress) for video layouts\nexport default function Layout({ tw, children, frame, progress }) {\n  // tw, image, qr, template, path, textPath all available\n  return (\n    <div style={tw('flex w-full h-full')}>\n      {children}\n    </div>\n  );\n}\n\nVideo Layouts\n\nLayouts work with video templates. Both the layout and child can use animations:\n\n// .loopwind/video-layout/template.tsx\nexport const meta = {\n  name: 'video-layout',\n  type: 'video',\n  size: { width: 1920, height: 1080 },\n  video: { fps: 60, duration: 4 },\n  props: {},\n};\n\nexport default function VideoLayout({ tw, children }) {\n  return (\n    <div style={tw('flex flex-col w-full h-full bg-background')}>\n      {/* Animated header */}\n      <div style={tw('flex items-center px-12 py-6 ease-out enter-slide-down/0/500')}>\n        <span style={tw('text-3xl font-bold text-primary')}>loopwind</span>\n      </div>\n\n      {/* Content */}\n      <div style={tw('flex flex-1')}>\n        {children}\n      </div>\n\n      {/* Animated footer */}\n      <div style={tw('flex px-12 py-6 ease-out enter-fade-in/500/400')}>\n        <span style={tw('text-muted-foreground')}>loopwind.dev</span>\n      </div>\n    </div>\n  );\n}\n\nExample: Consistent OG Images\n\nCreate a layout for all your OG images:\n\n// .loopwind/og-layout/template.tsx\nexport const meta = {\n  name: 'og-layout',\n  type: 'image',\n  size: { width: 1200, height: 630 },\n  props: {},\n};\n\nexport default function OGLayout({ tw, image, children }) {\n  return (\n    <div style={tw('flex w-full h-full bg-background')}>\n      {/* Content area */}\n      <div style={tw('flex flex-col flex-1 p-12')}>\n        {/* Logo */}\n        <div style={tw('flex items-center gap-3 mb-auto')}>\n          <img src={image('logo.svg')} style={tw('h-10 w-auto')} />\n          <span style={tw('text-2xl font-bold')}>MyBrand</span>\n        </div>\n\n        {/* Slot for page-specific content */}\n        <div style={tw('flex flex-1 items-center')}>\n          {children}\n        </div>\n\n        {/* Domain */}\n        <span style={tw('text-muted-foreground mt-auto')}>mybrand.com</span>\n      </div>\n    </div>\n  );\n}\n\n\nThen create page-specific templates:\n\n// .loopwind/og-blog/template.tsx\nexport const meta = {\n  name: 'og-blog',\n  type: 'image',\n  layout: '../og-layout',\n  props: {\n    title: 'string',\n    author: 'string',\n  },\n};\n\nexport default function OGBlog({ tw, title, author }) {\n  return (\n    <div style={tw('flex flex-col')}>\n      <span style={tw('text-sm text-muted-foreground uppercase tracking-wider mb-2')}>\n        Blog Post\n      </span>\n      <h1 style={tw('text-4xl font-bold text-foreground mb-4 text-balance')}>\n        {title}\n      </h1>\n      <span style={tw('text-muted-foreground')}>By {author}</span>\n    </div>\n  );\n}\n\nNext Steps\nTemplates - Template structure and metadata\nAnimation - Animation classes for video layouts\nHelpers - Using image(), qr(), and template()\nEmbedding Images\n\nUse the image() helper to embed images in your templates. It supports loading from props, template directories, and URLs.\n\nProp-based Images\n\nPass the prop name to load an image path from props:\n\nexport const meta = {\n  name: \"product-card\",\n  type: \"image\",\n  size: { width: 1200, height: 630 },\n  props: {\n    title: \"string\",\n    background: \"string?\"\n  }\n};\n\nexport default function ProductCard({ tw, image, title, background }) {\n  // Use fallback if no background prop provided\n  const bgSrc = background\n    ? image('background')\n    : 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200';\n\n  return (\n    <div style={tw('relative w-full h-full')}>\n      <img\n        src={bgSrc}\n        style={tw('absolute inset-0 w-full h-full object-cover')}\n      />\n      <div style={tw('relative z-10 p-12')}>\n        <h1 style={tw('text-6xl font-bold text-white')}>{title}</h1>\n      </div>\n    </div>\n  );\n}\n\n\nThe image('background') helper loads from the background prop value (file path or URL).\n\nDirect File Images\n\nLoad images directly from your template directory by including the file extension:\n\nexport default function ChangelogItem({ tw, image, text }) {\n  return (\n    <div style={tw('flex items-center gap-4')}>\n      {/* Load check.svg from template directory */}\n      <img\n        src={image('check.svg')}\n        style={tw('w-6 h-6')}\n      />\n      <span style={tw('text-lg')}>{text}</span>\n    </div>\n  );\n}\n\n\nYou can also use subdirectories:\n\n<img src={image('assets/icons/star.svg')} />\n<img src={image('shared/logo.png')} />\n\n\nTemplate directory structure:\n\n.loopwind/my-template/\n├── template.tsx\n├── check.svg           ← image('check.svg')\n└── assets/\n    └── icons/\n        └── star.svg    ← image('assets/icons/star.svg')\n\nURLs\n\nThe image() helper also supports loading images from URLs:\n\n{\n  \"background\": \"https://example.com/image.jpg\"\n}\n\nSupported Formats\nJPEG (.jpg, .jpeg)\nPNG (.png)\nGIF (.gif)\nWebP (.webp)\nSVG (.svg)\nImage Positioning\n\nUse Tailwind's object-fit utilities:\n\nexport default function ImageGrid({ tw, image, img1, img2, img3 }) {\n  return (\n    <div style={tw('flex gap-4 w-full h-full p-8 bg-gray-100')}>\n      {/* Cover - fills entire area, may crop */}\n      <img\n        src={image('img1')}\n        style={tw('w-full h-full object-cover rounded-lg')}\n      />\n\n      {/* Contain - fits within area, may letterbox */}\n      <img\n        src={image('img2')}\n        style={tw('w-full h-full object-contain')}\n      />\n\n      {/* Fill - stretches to fill */}\n      <img\n        src={image('img3')}\n        style={tw('w-full h-full object-fill')}\n      />\n    </div>\n  );\n}\n\nTroubleshooting\nImages Not Loading\n\nCheck file paths are relative to the props file:\n\n{\n  \"background\": \"./images/bg.jpg\"\n}\n\n\nAbsolute paths won't work.\n\nOptimize Image Sizes\n\nUse appropriately sized images before embedding:\n\nconvert large-image.jpg -resize 1600x900 optimized.jpg\n\nNext Steps\nTemplates - Creating image and video templates\nAnimation - Animation classes for videos\nStyling - Tailwind & shadcn/ui integration\nAnimation\n\nloopwind provides Tailwind-style animation classes that work with time to create smooth video animations without writing custom code.\n\nNote: Animation classes only work with video templates and GIFs. For static images, animations will have no effect since there's no time context.\n\nQuick Start\nexport default function MyVideo({ tw, title, subtitle }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-black')}>\n      {/* Bounce in from below: starts at 0, lasts 400ms */}\n      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400')}>\n        {title}\n      </h1>\n\n      {/* Fade in with upward motion: starts at 300ms, lasts 400ms */}\n      <p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/300/400')}>\n        {subtitle}\n      </p>\n\n      {/* Continuous floating animation: repeats every 1s (1000ms) */}\n      <div style={tw('mt-8 text-4xl loop-float/1000')}>\n        ⬇️\n      </div>\n    </div>\n  );\n}\n\nAnimation Format\n\nloopwind uses three types of animations with millisecond timing:\n\nType\tFormat\tDescription\nEnter\tenter-{type}/{start}/{duration}\tAnimations that play when entering\nExit\texit-{type}/{start}/{duration}\tAnimations that play when exiting\nLoop\tloop-{type}/{duration}\tContinuous looping animations\n\nAll timing values are in milliseconds (1000ms = 1 second).\n\nUtility-Based Animations\n\nIn addition to predefined animations, loopwind supports Tailwind utility-based animations that let you animate any transform or opacity property directly:\n\n// Slide in 20px from the left\n<div style={tw('enter-translate-x-5/0/1000')}>Content</div>\n\n// Rotate 90 degrees on entrance\n<div style={tw('enter-rotate-90/0/500')}>Spinning</div>\n\n// Fade to 50% opacity in a loop\n<div style={tw('loop-opacity-50/1000')}>Pulsing</div>\n\n// Scale down with negative value\n<div style={tw('enter--scale-50/0/800')}>Shrinking</div>\n\nSupported Utilities\nUtility\tFormat\tDescription\tExample\ntranslate-x\tenter-translate-x-{value}\tTranslate horizontally\tenter-translate-x-5 = 20px<br/>enter-translate-x-full = 100%<br/>enter-translate-x-[20px] = 20px\ntranslate-y\tenter-translate-y-{value}\tTranslate vertically\tloop-translate-y-10 = 40px<br/>enter-translate-y-1/2 = 50%<br/>enter-translate-y-[5rem] = 80px\nopacity\tenter-opacity-{n}\tSet opacity (0-100)\tenter-opacity-50 = 50%\nscale\tenter-scale-{n}\tScale element (0-200)\tenter-scale-100 = 1.0x\nrotate\tenter-rotate-{n}\tRotate in degrees\tenter-rotate-45 = 45°\nskew-x\tenter-skew-x-{n}\tSkew on X axis in degrees\tenter-skew-x-12 = 12°\nskew-y\tenter-skew-y-{n}\tSkew on Y axis in degrees\texit-skew-y-6 = 6°\n\nTranslate value formats:\n\nNumeric: 5 = 20px (Tailwind spacing scale: 1 unit = 4px)\nKeywords: full = 100%\nFractions: 1/2 = 50%, 1/3 = 33.333%, 2/3 = 66.666%, etc.\nArbitrary values: [20px], [5rem], [10%] (rem converts to px: 1rem = 16px)\n\nAll utilities work with:\n\nAll prefixes: enter-, exit-, loop-, animate-\nNegative values: Prefix with - (e.g., -translate-x-5, -rotate-45)\nTiming syntax: Add /start/duration (e.g., enter-translate-x-5/0/800)\nTranslate Animations\n// Numeric (Tailwind spacing): 20px (5 * 4px)\n<div style={tw('enter-translate-x-5/0/500')}>Content</div>\n\n// Keyword: Full width (100%)\n<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>\n\n// Fraction: Half width (50%)\n<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>\n\n// Arbitrary values: Exact px or rem\n<div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div>\n<div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div>\n\n// Loop with fractions\n<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div>\n\n// Negative values\n<div style={tw('exit--translate-y-8/2000/500')}>Rising</div>\n\nOpacity Animations\n// Fade to 100% opacity\n<div style={tw('enter-opacity-100/0/500')}>Fading In</div>\n\n// Fade to 50% opacity\n<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>\n\n// Pulse between 50% and 100%\n<div style={tw('loop-opacity-50/1000')}>Pulsing</div>\n\n// Fade out to 0%\n<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>\n\nScale Animations\n// Scale from 0 to 100% (1.0x)\n<div style={tw('enter-scale-100/0/500')}>Growing</div>\n\n// Scale to 150% (1.5x)\n<div style={tw('enter-scale-150/0/800')}>Enlarging</div>\n\n// Pulse scale in a loop\n<div style={tw('loop-scale-110/1000')}>Breathing</div>\n\n// Scale down to 50%\n<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>\n\nRotate Animations\n// Rotate 90 degrees\n<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>\n\n// Rotate 180 degrees\n<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>\n\n// Continuous rotation in loop (360 degrees per cycle)\n<div style={tw('loop-rotate-360/2000')}>Spinning</div>\n\n// Rotate backwards with negative value\n<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>\n\nSkew Animations\n// Skew on X axis\n<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>\n\n// Skew on Y axis\n<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>\n\n// Oscillating skew in loop\n<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>\n\n// Negative skew\n<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>\n\nCombining Utilities\n\nYou can combine multiple utility animations on the same element:\n\n// Translate and rotate together\n<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>\n  Flying In\n</div>\n\n// Fade and scale\n<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>\n  Appearing\n</div>\n\n// Enter with translate, exit with rotation\n<div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}>\n  Slide and Spin\n</div>\n\nBracket Notation\n\nFor more CSS-like syntax, you can use brackets with units:\n\n// Using bracket notation with seconds\n<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>\n\n// Using bracket notation with milliseconds\n<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>\n\n// Mix and match - plain numbers are milliseconds\n<h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>\n\nEnter Animations\n\nFormat: enter-{type}/{startMs}/{durationMs}\n\nstartMs - when the animation begins (milliseconds from start)\ndurationMs - how long the animation lasts\n\nWhen values are omitted (enter-fade-in), it uses the full video duration.\n\nFade Animations\n\nSimple opacity transitions with optional direction.\n\n// Fade in from 0ms to 500ms\n<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>\n\n// Fade in with upward motion\n<h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1>\n\nClass\tDescription\nenter-fade-in/0/500\tFade in (opacity 0 → 1)\nenter-fade-in-up/0/500\tFade in + slide up (30px)\nenter-fade-in-down/0/500\tFade in + slide down (30px)\nenter-fade-in-left/0/500\tFade in + slide from left (30px)\nenter-fade-in-right/0/500\tFade in + slide from right (30px)\nSlide Animations\n\nLarger movement (100px) with fade.\n\n// Slide in from left: starts at 0, lasts 500ms\n<div style={tw('enter-slide-left/0/500')}>Content</div>\n\n// Slide up from bottom: starts at 200ms, lasts 600ms\n<div style={tw('enter-slide-up/200/600')}>Content</div>\n\nClass\tDescription\nenter-slide-left/0/500\tSlide in from left (100px)\nenter-slide-right/0/500\tSlide in from right (100px)\nenter-slide-up/0/500\tSlide in from bottom (100px)\nenter-slide-down/0/500\tSlide in from top (100px)\nBounce Animations\n\nPlayful entrance with overshoot effect.\n\n// Bounce in with scale overshoot\n<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>\n\n// Bounce in from below\n<div style={tw('enter-bounce-in-up/0/600')}>Pop!</div>\n\nClass\tDescription\nenter-bounce-in/0/500\tBounce in with scale overshoot\nenter-bounce-in-up/0/500\tBounce in from below\nenter-bounce-in-down/0/500\tBounce in from above\nenter-bounce-in-left/0/500\tBounce in from left\nenter-bounce-in-right/0/500\tBounce in from right\nScale & Zoom Animations\n\nSize-based transitions.\n\n// Scale in from 50%\n<div style={tw('enter-scale-in/0/500')}>Growing</div>\n\n// Zoom in from 0%\n<div style={tw('enter-zoom-in/0/1000')}>Zooming</div>\n\nClass\tDescription\nenter-scale-in/0/500\tScale up from 50% to 100%\nenter-zoom-in/0/500\tZoom in from 0% to 100%\nRotate & Flip Animations\n\nRotation-based transitions.\n\n// Rotate in 180 degrees\n<div style={tw('enter-rotate-in/0/500')}>Spinning</div>\n\n// 3D flip on X axis\n<div style={tw('enter-flip-in-x/0/500')}>Flipping</div>\n\nClass\tDescription\nenter-rotate-in/0/500\tRotate in from -180°\nenter-flip-in-x/0/500\t3D flip on horizontal axis\nenter-flip-in-y/0/500\t3D flip on vertical axis\nExit Animations\n\nFormat: exit-{type}/{startMs}/{durationMs}\n\nstartMs - when the exit animation begins\ndurationMs - how long the exit animation lasts\n\nExit animations use the same timing system but animate elements out.\n\n// Fade out starting at 2500ms, lasting 500ms (ends at 3000ms)\n<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>\n\n// Combined enter and exit on same element\n<h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}>\n  Hello and Goodbye\n</h1>\n\nClass\tDescription\nexit-fade-out/2500/500\tFade out (opacity 1 → 0)\nexit-fade-out-up/2500/500\tFade out + slide up\nexit-fade-out-down/2500/500\tFade out + slide down\nexit-fade-out-left/2500/500\tFade out + slide left\nexit-fade-out-right/2500/500\tFade out + slide right\nexit-slide-up/2500/500\tSlide out upward (100px)\nexit-slide-down/2500/500\tSlide out downward (100px)\nexit-slide-left/2500/500\tSlide out to left (100px)\nexit-slide-right/2500/500\tSlide out to right (100px)\nexit-scale-out/2500/500\tScale out to 150%\nexit-zoom-out/2500/500\tZoom out to 200%\nexit-rotate-out/2500/500\tRotate out to 180°\nexit-bounce-out/2500/500\tBounce out with scale\nexit-bounce-out-up/2500/500\tBounce out upward\nexit-bounce-out-down/2500/500\tBounce out downward\nexit-bounce-out-left/2500/500\tBounce out to left\nexit-bounce-out-right/2500/500\tBounce out to right\nLoop Animations\n\nFormat: loop-{type}/{durationMs}\n\nLoop animations repeat every {durationMs} milliseconds:\n\n/1000 = 1 second loop\n/500 = 0.5 second loop\n/2000 = 2 second loop\n\nWhen duration is omitted (loop-bounce), it defaults to 1000ms (1 second).\n\n// Pulse opacity every 500ms\n<div style={tw('loop-fade/500')}>Pulsing</div>\n\n// Bounce every 800ms\n<div style={tw('loop-bounce/800')}>Bouncing</div>\n\n// Full rotation every 2000ms\n<div style={tw('loop-spin/2000')}>Spinning</div>\n\nClass\tDescription\nloop-fade/{ms}\tOpacity pulse (0.5 → 1 → 0.5)\nloop-bounce/{ms}\tBounce up and down\nloop-spin/{ms}\tFull 360° rotation\nloop-ping/{ms}\tScale up + fade out (radar effect)\nloop-wiggle/{ms}\tSide to side wiggle\nloop-float/{ms}\tGentle up and down floating\nloop-pulse/{ms}\tScale pulse (1.0 → 1.05 → 1.0)\nloop-shake/{ms}\tShake side to side\nEasing Functions\n\nAdd an easing class before the animation class to control the timing curve.\n\n// Ease in (accelerate)\n<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>\n\n// Ease out (decelerate) - default\n<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>\n\n// Ease in-out (smooth)\n<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>\n\n// Strong cubic easing\n<h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1>\n\nClass\tDescription\tBest For\nlinear\tConstant speed\tMechanical motion\nease-in\tSlow start, fast end\tExit animations\nease-out\tFast start, slow end (default)\tEnter animations\nease-in-out\tSlow start and end\tSubtle transitions\nease-in-cubic\tStrong slow start\tDramatic exits\nease-out-cubic\tStrong fast start\tImpactful entrances\nease-in-out-cubic\tStrong both ends\tEmphasis animations\nease-in-quart\tVery strong slow start\tPowerful exits\nease-out-quart\tVery strong fast start\tPunchy entrances\nease-in-out-quart\tVery strong both ends\tMaximum drama\nPer-Animation-Type Easing\n\nYou can apply different easing functions to enter, exit, and loop animations on the same element using enter-ease-*, exit-ease-*, and loop-ease-* classes.\n\n// Different easing for enter and exit\n<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>\n  Smooth entrance, sharp exit\n</h1>\n\n// Loop with linear easing, enter with bounce\n<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>\n  Bouncy entrance, linear loop\n</div>\n\n// Default easing still works (applies to all animations)\n<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>\n  Same easing for both\n</div>\n\n// Mix default with specific overrides\n<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>\n  Default ease-out for enter, cubic-in for exit\n</div>\n\n\nHow it works:\n\nDefault easing (ease-*) applies to ALL animations if no specific override is set\nSpecific easing (enter-ease-*, exit-ease-*, loop-ease-*) overrides the default for that animation type\nIf both are present, specific easing takes priority for its animation type\n\nAvailable easing classes:\n\nDefault (all animations)\tEnter only\tExit only\tLoop only\nease-in\tenter-ease-in\texit-ease-in\tloop-ease-in\nease-out\tenter-ease-out\texit-ease-out\tloop-ease-out\nease-in-out\tenter-ease-in-out\texit-ease-in-out\tloop-ease-in-out\nease-in-cubic\tenter-ease-in-cubic\texit-ease-in-cubic\tloop-ease-in-cubic\nease-out-cubic\tenter-ease-out-cubic\texit-ease-out-cubic\tloop-ease-out-cubic\nease-in-out-cubic\tenter-ease-in-out-cubic\texit-ease-in-out-cubic\tloop-ease-in-out-cubic\nease-in-quart\tenter-ease-in-quart\texit-ease-in-quart\tloop-ease-in-quart\nease-out-quart\tenter-ease-out-quart\texit-ease-out-quart\tloop-ease-out-quart\nease-in-out-quart\tenter-ease-in-out-quart\texit-ease-in-out-quart\tloop-ease-in-out-quart\nlinear\tenter-ease-linear\texit-ease-linear\tloop-ease-linear\nease-spring\tenter-ease-spring\texit-ease-spring\tloop-ease-spring\nSpring Easing\n\nSpring easing creates natural, physics-based bouncy animations. Use the built-in ease-spring easing or create custom springs with configurable parameters.\n\n// Default spring easing\n<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>\n\n// Per-animation-type spring\n<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>\n  Spring entrance, smooth exit\n</div>\n\n// Custom spring with parameters: ease-spring/mass/stiffness/damping\n<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>\n  Custom spring (mass=1, stiffness=100, damping=10)\n</h1>\n\n// More bouncy spring (lower damping)\n<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>\n  Extra bouncy!\n</div>\n\n// Stiffer spring (higher stiffness, faster)\n<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>\n  Snappy spring\n</div>\n\n// Per-animation-type custom springs\n<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')}>\n  Different springs for enter and exit\n</div>\n\n\nSpring parameters:\n\nParameter\tDescription\tEffect when increased\tDefault\nmass\tMass of the spring\tSlower, more inertia\t1\nstiffness\tSpring stiffness\tFaster, snappier\t100\ndamping\tDamping coefficient\tLess bounce, smoother\t10\n\nCommon spring presets:\n\n// Gentle bounce (default)\nease-spring/1/100/10\n\n// Extra bouncy\nease-spring/1/170/8\n\n// Snappy (no bounce)\nease-spring/1/200/15\n\n// Slow and bouncy\nease-spring/2/100/8\n\n// Fast and tight\nease-spring/0.5/300/20\n\n\nHow spring works:\n\nDefault ease-spring - Uses a pre-calculated spring curve optimized for most use cases\nCustom ease-spring/mass/stiffness/damping - Generates a physics-based spring curve using the damped harmonic oscillator formula\nThe spring automatically calculates its ideal duration to reach the final state\nWorks with all animation types: ease-spring, enter-ease-spring, exit-ease-spring, loop-ease-spring\nCombining Enter and Exit\n\nYou can use both enter and exit animations on the same element:\n\nexport default function EnterExit({ tw, title }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>\n      {/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}\n      <h1 style={tw('text-8xl font-bold text-white enter-fade-in/0/500 exit-fade-out/2500/500')}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\n\nThe opacities from multiple animations are multiplied together, so you get smooth transitions that combine properly.\n\nStaggered Animations\n\nCreate sequenced animations by offsetting start times:\n\nexport default function StaggeredList({ tw, items }) {\n  return (\n    <div style={tw('flex flex-col gap-4')}>\n      {/* First item: starts at 0ms, lasts 300ms */}\n      <div style={tw('ease-out enter-fade-in-left/0/300')}>\n        {items[0]}\n      </div>\n\n      {/* Second item: starts at 100ms, lasts 300ms */}\n      <div style={tw('ease-out enter-fade-in-left/100/300')}>\n        {items[1]}\n      </div>\n\n      {/* Third item: starts at 200ms, lasts 300ms */}\n      <div style={tw('ease-out enter-fade-in-left/200/300')}>\n        {items[2]}\n      </div>\n    </div>\n  );\n}\n\nDynamic Staggering\n\nFor dynamic lists, calculate the timing programmatically:\n\nexport default function DynamicStagger({ tw, items }) {\n  return (\n    <div style={tw('flex flex-col gap-4')}>\n      {items.map((item, i) => {\n        const start = i * 100;      // Each item starts 100ms later\n        const duration = 300;       // Each animation lasts 300ms\n\n        return (\n          <div\n            key={i}\n            style={tw(`ease-out enter-fade-in-up/${start}/${duration}`)}\n          >\n            {item}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nCommon Patterns\nIntro Sequence\nexport default function IntroVideo({ tw, title, subtitle, logo }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>\n      {/* Logo appears first */}\n      <img\n        src={logo}\n        style={tw('h-20 mb-8 ease-out enter-scale-in/0/300')}\n      />\n\n      {/* Title bounces in */}\n      <h1 style={tw('text-7xl font-bold text-white ease-out enter-bounce-in-up/200/500')}>\n        {title}\n      </h1>\n\n      {/* Subtitle fades in last */}\n      <p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/700')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}\n\nText Reveal\nexport default function TextReveal({ tw, words }) {\n  return (\n    <div style={tw('flex flex-wrap gap-2 justify-center')}>\n      {words.split(' ').map((word, i) => (\n        <span\n          key={i}\n          style={tw(`text-4xl font-bold ease-out enter-fade-in-up/${i * 100}/200`)}\n        >\n          {word}\n        </span>\n      ))}\n    </div>\n  );\n}\n\nLooping Background Element\nexport default function AnimatedBackground({ tw, children }) {\n  return (\n    <div style={tw('relative w-full h-full')}>\n      {/* Floating background circles */}\n      <div style={tw('absolute top-10 left-10 w-20 h-20 rounded-full bg-white/10 loop-float/2000')} />\n      <div style={tw('absolute bottom-20 right-20 w-32 h-32 rounded-full bg-white/10 loop-fade/1500')} />\n\n      {/* Main content */}\n      <div style={tw('relative z-10')}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nFull Enter/Exit Animation\nexport default function FullAnimation({ tw, title }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>\n      {/* Enter: starts at 0, lasts 400ms. Exit: starts at 2600ms, lasts 400ms */}\n      <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400 exit-fade-out-up/2600/400')}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nProgrammatic Animations\n\nFor complete control beyond animation classes, use progress and frame directly.\n\nAvailable Props\nProp\tType\tDescription\nprogress\tnumber\t0 to 1 through the video (0% to 100%)\nframe\tnumber\tCurrent frame number (0, 1, 2, ... totalFrames-1)\n\nThese are only available in video templates. Use them when animation classes aren't flexible enough.\n\nUsing frame\nexport default function FrameAnimation({ tw, frame, title }) {\n  // Color cycling using frame number\n  const hue = (frame * 5) % 360; // Cycle through colors\n\n  // Pulsing based on frame\n  const fps = 30;\n  const pulse = Math.sin(frame / fps * Math.PI * 2) * 0.2 + 0.8; // 0.6 to 1.0\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-black')}>\n      <h1 style={{\n        ...tw('text-8xl font-bold'),\n        color: `hsl(${hue}, 70%, 60%)`,\n        transform: `scale(${pulse})`\n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nUsing progress\nexport default function ProgressAnimation({ tw, progress, title }) {\n  // Custom fade based on progress\n  const opacity = progress < 0.3 ? progress / 0.3 : 1;\n\n  // Custom scale based on progress\n  const scale = 0.8 + progress * 0.2; // 0.8 to 1.0\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>\n      <h1 style={{\n        ...tw('text-8xl font-bold text-white'),\n        opacity,\n        transform: `scale(${scale})`\n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nCustom Easing\nexport default function CustomEasing({ tw, progress, title }) {\n  // Smoothstep easing\n  const eased = progress * progress * (3 - 2 * progress);\n\n  // Elastic easing\n  const elastic = Math.pow(2, -10 * progress) * Math.sin((progress - 0.075) * (2 * Math.PI) / 0.3) + 1;\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full')}>\n      <h1 style={{\n        ...tw('text-8xl font-bold'),\n        opacity: eased,\n        transform: `translateY(${(1 - elastic) * 100}px)`\n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nWhen to Use Programmatic Animations\n\nUse progress/frame instead of animation classes when you need:\n\nCustom easing functions (elastic, bounce with specific curves beyond built-in ease-spring)\nColor cycling or gradients based on time\nMathematical animations (sine waves, spirals, etc.)\nComplex multi-property animations that need precise coordination\nConditional logic based on specific frame numbers\n\nFor everything else, prefer animation classes - they're simpler and more maintainable.\n\nAnimating Along Paths\n\nAnimate elements along SVG paths with proper rotation using built-in path helpers:\n\nexport default function PathFollowing({ tw, progress, path }) {\n  // Follow a quadratic Bezier curve - one line!\n  const rocket = path.followQuadratic(\n    { x: 200, y: 400 },   // Start point\n    { x: 960, y: 150 },   // Control point\n    { x: 1720, y: 400 },  // End point\n    progress\n  );\n\n  return (\n    <div style={{ display: 'flex', ...tw('relative w-full h-full bg-gray-900') }}>\n      {/* Draw the path (optional) */}\n      <svg width=\"1920\" height=\"1080\" style={{ position: 'absolute' }}>\n        <path\n          d=\"M 200 400 Q 960 150 1720 400\"\n          stroke=\"rgba(255,255,255,0.2)\"\n          strokeWidth={2}\n          fill=\"none\"\n        />\n      </svg>\n\n      {/* Element following the path */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: rocket.x,\n          top: rocket.y,\n          transform: `translate(-50%, -50%) rotate(${rocket.angle}deg)`,\n          fontSize: '48px'\n        }}\n      >\n        🚀\n      </div>\n    </div>\n  );\n}\n\nText Path Animations\n\nCombine textPath helpers with animation classes to create animated text along curves:\n\nRotating text around a circle:\n\nexport default function RotatingCircleText({ tw, textPath, progress }) {\n  return (\n    <div style={tw('relative w-full h-full bg-black')}>\n      {/* Text rotates around circle using progress */}\n      {textPath.onCircle(\n        \"SPINNING TEXT • AROUND • \",\n        960,      // center x\n        540,      // center y\n        400,      // radius\n        progress, // rotation offset (0-1 animates full rotation)\n        {\n          fontSize: \"3xl\",\n          fontWeight: \"bold\",\n          color: \"yellow-300\"\n        }\n      )}\n    </div>\n  );\n}\n\n\nAnimated text reveal along a path:\n\nexport default function PathTextReveal({ tw, textPath, progress }) {\n  // Create custom path follower that animates position\n  const pathFollower = (t) => {\n    // Only show characters up to current progress\n    const visibleProgress = progress * 1.5; // Extend range for smooth reveal\n    const opacity = t < visibleProgress ? 1 : 0;\n\n    // Follow quadratic curve\n    const pos = {\n      x: (1 - t) * (1 - t) * 200 + 2 * (1 - t) * t * 960 + t * t * 1720,\n      y: (1 - t) * (1 - t) * 400 + 2 * (1 - t) * t * 150 + t * t * 400,\n      angle: 0\n    };\n\n    return { ...pos, opacity };\n  };\n\n  return (\n    <div style={tw('relative w-full h-full bg-gray-900')}>\n      {textPath.onPath(\n        \"REVEALING TEXT\",\n        pathFollower,\n        {\n          fontSize: \"4xl\",\n          fontWeight: \"bold\",\n          color: \"blue-300\"\n        }\n      ).map((char, i) => (\n        <div key={i} style={{ ...char.props.style, opacity: char.props.style.opacity || 1 }}>\n          {char}\n        </div>\n      ))}\n    </div>\n  );\n}\n\n\nStaggered character entrance:\n\nexport default function StaggeredCircleText({ tw, textPath }) {\n  const text = \"HELLO WORLD\";\n\n  return (\n    <div style={tw('relative w-full h-full bg-slate-900')}>\n      {textPath.onCircle(\n        text,\n        960, 540, 400, 0,\n        { fontSize: \"4xl\", fontWeight: \"bold\", color: \"white\" }\n      ).map((char, i) => {\n        // Stagger fade-in: each character starts 50ms later\n        const staggerDelay = i * 50;\n        return (\n          <div\n            key={i}\n            style={{\n              ...char.props.style,\n              ...tw(`enter-fade-in/${staggerDelay}/300 enter-scale-100/${staggerDelay}/300`)\n            }}\n          >\n            {char.props.children}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\n\nText with bounce entrance along arc:\n\nexport default function BouncyArcText({ tw, textPath }) {\n  return (\n    <div style={tw('relative w-full h-full bg-gradient-to-br from-purple-600 to-blue-500')}>\n      {/* Draw the arc path */}\n      <svg width=\"1920\" height=\"1080\" style={{ position: 'absolute' }}>\n        <path\n          d=\"M 300 900 A 600 600 0 0 1 1620 900\"\n          stroke=\"rgba(255,255,255,0.2)\"\n          strokeWidth={2}\n          fill=\"none\"\n          strokeDasharray=\"5 5\"\n        />\n      </svg>\n\n      {/* Text follows arc with staggered bounce */}\n      {textPath.onArc(\n        \"BOUNCING ON ARC\",\n        960,  // cx\n        300,  // cy\n        600,  // radius\n        180,  // start angle\n        360,  // end angle\n        { fontSize: \"3xl\", fontWeight: \"bold\", color: \"white\" }\n      ).map((char, i) => (\n        <div\n          key={i}\n          style={{\n            ...char.props.style,\n            ...tw(`ease-out enter-bounce-in-up/${i * 80}/500`)\n          }}\n        >\n          {char.props.children}\n        </div>\n      ))}\n    </div>\n  );\n}\n\n\nLoop animation with text on curve:\n\nexport default function LoopingCurveText({ tw, textPath, frame }) {\n  // Calculate wave effect using frame\n  const waveOffset = Math.sin(frame / 30 * Math.PI * 2) * 0.1;\n\n  return (\n    <div style={tw('relative w-full h-full bg-black')}>\n      {textPath.onQuadratic(\n        \"WAVY TEXT\",\n        { x: 200, y: 400 },\n        { x: 960, y: 150 },\n        { x: 1720, y: 400 },\n        { fontSize: \"4xl\", fontWeight: \"bold\", color: \"pink-300\" }\n      ).map((char, i) => (\n        <div\n          key={i}\n          style={{\n            ...char.props.style,\n            transform: `${char.props.style.transform} translateY(${Math.sin((i + frame) / 5) * 10}px)`\n          }}\n        >\n          {char.props.children}\n        </div>\n      ))}\n    </div>\n  );\n}\n\n\nTips for animating text paths:\n\nUse progress for smooth rotation on circles and arcs\nMap over returned characters to apply individual animations\nCombine with animation classes like enter-fade-in, enter-bounce-in, etc.\nStagger character animations by calculating delays: i * delayMs\nUse frame for continuous effects like waves or pulsing\nPreserve the original transform when adding animations: transform: '${char.props.style.transform} ...'\n\nCommon path types:\n\nQuadratic Bezier (Q command):\n\n// Position: (1-t)²·P0 + 2(1-t)t·P1 + t²·P2\nfunction pointOnQuadraticBezier(p0, p1, p2, t) {\n  const x = (1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;\n  const y = (1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;\n  return { x, y };\n}\n\n// Tangent angle\nfunction angleOnQuadraticBezier(p0, p1, p2, t) {\n  const dx = 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x);\n  const dy = 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y);\n  return Math.atan2(dy, dx) * (180 / Math.PI);\n}\n\n\nCubic Bezier (C command):\n\n// Position: (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3\nfunction pointOnCubicBezier(p0, p1, p2, p3, t) {\n  const mt = 1 - t;\n  const mt2 = mt * mt;\n  const mt3 = mt2 * mt;\n  const t2 = t * t;\n  const t3 = t2 * t;\n  const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;\n  const y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y;\n  return { x, y };\n}\n\n// Tangent angle\nfunction angleOnCubicBezier(p0, p1, p2, p3, t) {\n  const mt = 1 - t;\n  const mt2 = mt * mt;\n  const t2 = t * t;\n  const dx = -3 * mt2 * p0.x + 3 * mt2 * p1.x - 6 * mt * t * p1.x - 3 * t2 * p2.x + 6 * mt * t * p2.x + 3 * t2 * p3.x;\n  const dy = -3 * mt2 * p0.y + 3 * mt2 * p1.y - 6 * mt * t * p1.y - 3 * t2 * p2.y + 6 * mt * t * p2.y + 3 * t2 * p3.y;\n  return Math.atan2(dy, dx) * (180 / Math.PI);\n}\n\n\nCircle:\n\nfunction pointOnCircle(cx, cy, radius, angleRadians) {\n  return {\n    x: cx + radius * Math.cos(angleRadians),\n    y: cy + radius * Math.sin(angleRadians)\n  };\n}\n\n// Usage\nconst angleRadians = progress * Math.PI * 2;\nconst pos = pointOnCircle(300, 300, 100, angleRadians);\nconst tangentAngle = (angleRadians * 180 / Math.PI) + 90; // Tangent is perpendicular\n\n\nTips:\n\nUse progress (0-1) for smooth animation\nThe translate(-50%, -50%) centers the element on the path\nCombine rotation with the translate: translate(-50%, -50%) rotate(${angle}deg)\nFor text following a path, you can animate individual characters at different progress values\nSVG Stroke Animations\n\nAnimate SVG path strokes with the stroke-dash classes, perfect for drawing or erasing line art, icons, and illustrations.\n\nHow It Works\n\nSVG stroke animations use strokeDasharray and strokeDashoffset CSS properties to create drawing effects:\n\nEnter animations - Draw the stroke from start to finish\nExit animations - Erase the stroke from finish to start\nLoop animations - Continuously draw and erase\nFormat\n\nAll stroke-dash animations require the path length in brackets:\n\nenter-stroke-dash-[length]/start/duration\nexit-stroke-dash-[length]/start/duration\nloop-stroke-dash-[length]/duration\n\nBasic Examples\nexport default function SVGAnimation({ tw }) {\n  return (\n    <svg width=\"400\" height=\"200\" viewBox=\"0 0 400 200\">\n      {/* Draw a curve over 1 second */}\n      <path\n        d=\"M10 150 Q 95 10 180 150\"\n        stroke=\"black\"\n        strokeWidth={4}\n        fill=\"none\"\n        style={tw('enter-stroke-dash-[300]/0/1000')}\n      />\n    </svg>\n  );\n}\n\nEnter Animations (Drawing)\n\nDraw strokes from 0% to 100%:\n\n// Draw a 300px path over 1 second\n<path style={tw('enter-stroke-dash-[300]/0/1000')} />\n\n// Draw with spring easing\n<path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} />\n\n// Stagger multiple paths\n<path style={tw('enter-stroke-dash-[200]/0/600')} />\n<path style={tw('enter-stroke-dash-[200]/200/600')} />\n<path style={tw('enter-stroke-dash-[200]/400/600')} />\n\nExit Animations (Erasing)\n\nErase strokes from 100% to 0%:\n\n// Erase starting at 2000ms, lasting 500ms\n<path style={tw('exit-stroke-dash-[300]/2000/500')} />\n\n// Draw then erase the same path\n<path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />\n\nLoop Animations\n\nContinuously draw and erase:\n\n// Loop every 2 seconds (draws in first half, erases in second half)\n<path style={tw('loop-stroke-dash-[300]/2000')} />\n\n// Faster loop\n<path style={tw('loop-stroke-dash-[200]/1000')} />\n\nGetting Path Length\n\nTo find the path length for your SVG:\n\n// In browser console or component:\nconst path = document.querySelector('path');\nconst length = path.getTotalLength();\nconsole.log(length); // e.g., 347.89\n\n\nThen use that value:\n\n<path style={tw('enter-stroke-dash-[347.89]/0/1000')} />\n\nComplete Example\nexport default function DrawingEffect({ tw }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>\n      <svg width=\"600\" height=\"400\" viewBox=\"0 0 600 400\">\n        {/* Checkmark icon drawn in sequence */}\n        <path\n          d=\"M100 200 L 200 300 L 400 100\"\n          stroke=\"#10b981\"\n          strokeWidth={8}\n          fill=\"none\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          style={tw('ease-out enter-stroke-dash-[600]/0/1200')}\n        />\n\n        {/* Circle drawn after checkmark */}\n        <circle\n          cx=\"250\"\n          cy=\"200\"\n          r=\"150\"\n          stroke=\"#10b981\"\n          strokeWidth={6}\n          fill=\"none\"\n          style={tw('ease-out enter-stroke-dash-[942]/1000/1000')}\n        />\n      </svg>\n    </div>\n  );\n}\n\nCombining with Other Animations\n\nStroke animations work alongside other animation classes:\n\n// Fade in while drawing\n<path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} />\n\n// Draw with pulsing color\n<svg>\n  <path\n    stroke=\"url(#gradient)\"\n    style={tw('enter-stroke-dash-[500]/0/1500')}\n  />\n  <defs>\n    <linearGradient id=\"gradient\">\n      <stop offset=\"0%\" stopColor=\"#8b5cf6\" />\n      <stop offset=\"100%\" stopColor=\"#ec4899\" />\n    </linearGradient>\n  </defs>\n</svg>\n\nAnimated Dashed Strokes (Marching Ants)\n\nFor marching ants or animated dashed patterns, use frame or progress directly instead of animation classes:\n\nexport default function MarchingAnts({ tw, frame }) {\n  // Calculate animated offset (loops every 30 frames)\n  const dashOffset = -(frame % 30) * 2;\n\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>\n      <svg width=\"600\" height=\"400\" viewBox=\"0 0 600 400\">\n        {/* Marching ants border */}\n        <rect\n          x=\"50\"\n          y=\"50\"\n          width=\"500\"\n          height=\"300\"\n          fill=\"none\"\n          stroke=\"#3b82f6\"\n          strokeWidth={3}\n          strokeDasharray=\"10 5\"\n          strokeDashoffset={dashOffset}\n        />\n\n        {/* Animated circle with different speed */}\n        <circle\n          cx=\"300\"\n          cy=\"200\"\n          r=\"80\"\n          fill=\"none\"\n          stroke=\"#10b981\"\n          strokeWidth={4}\n          strokeDasharray=\"15 8\"\n          strokeDashoffset={dashOffset * 1.5}\n        />\n      </svg>\n    </div>\n  );\n}\n\n\nTips:\n\nstrokeDasharray=\"10 5\" - 10px dash, 5px gap\nstrokeDashoffset={dashOffset} - animates the pattern position\nNegative offset moves forward, positive moves backward\nDifferent speeds: multiply by different values (e.g., dashOffset * 2)\n\nThis technique is different from stroke-dash classes:\n\nstroke-dash classes - Draw/erase the stroke (reveal animation)\nMarching ants - Move a dashed pattern along the stroke\nPerformance Tips\nUse Tailwind classes when possible - they're optimized for the renderer\nAvoid too many nested animations - each adds computation per frame\nUse loop animations sparingly - they're computed every frame\nPrefer opacity and transform - they're the most performant properties\nNext Steps\nTemplates - Creating image and video templates\nHelpers - QR codes, images, and more\nTemplate Helpers\n\nAdditional helpers for creating powerful, composable templates.\n\nOverview\n\nBeyond the basics, loopwind provides:\n\ntemplate() - Compose templates together\nqr() - Generate QR codes on the fly\nconfig - Access user configuration\n\nFor image embedding, see the Images page.\n\nTemplate Composition\n\nCompose multiple templates together to create complex designs.\n\nUsage\nexport default function CompositeCard({ tw, template, title, author, avatar }) {\n  return (\n    <div style={tw('w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>\n      <div style={tw('bg-white rounded-2xl p-8 shadow-xl')}>\n        <h1 style={tw('text-4xl font-bold text-gray-900 mb-6')}>{title}</h1>\n        \n        {/* Embed another template */}\n        <div style={tw('mb-6')}>\n          {template('user-badge', {\n            name: author,\n            avatar: avatar\n          })}\n        </div>\n        \n        <p style={tw('text-gray-600')}>Published by {author}</p>\n      </div>\n    </div>\n  );\n}\n\n\nHow it works:\n\ntemplate(name, props) renders another installed template\nThe embedded template is rendered at its specified size\nYou can embed multiple templates in one design\nTemplates can be nested (template within a template)\nUse Cases\n\n1. Reusable components:\n\n// Create a logo template once, use it everywhere\n<div>{template('company-logo', { variant: 'dark' })}</div>\n\n\n2. Complex layouts:\n\n// Combine multiple templates into one design\n<div style={tw('grid grid-cols-2 gap-4')}>\n  {template('product-card', { product: product1 })}\n  {template('product-card', { product: product2 })}\n</div>\n\n\n3. Dynamic content:\n\n// Render templates based on data\n{users.map(user => \n  template('user-avatar', { name: user.name, image: user.avatar })\n)}\n\nBest Practices\nKeep templates focused - Each template should do one thing well\nPass minimal props - Only pass what the embedded template needs\nDocument dependencies - Note which templates are required in your README\nAvoid deep nesting - Too many nested templates can be hard to debug\nQR Codes\n\nGenerate QR codes dynamically in your templates.\n\nUsage\nexport default function QRCard({ tw, qr, title, url }) {\n  return (\n    <div style={tw('flex flex-col items-center justify-center w-full h-full bg-white p-10')}>\n      <h1 style={tw('text-4xl font-bold text-black mb-8')}>{title}</h1>\n      \n      {/* Generate QR code for the URL */}\n      <img src={qr(url)} style={tw('w-64 h-64')} />\n      \n      <p style={tw('text-gray-600 mt-4')}>{url}</p>\n    </div>\n  );\n}\n\n\nProps format:\n\n{\n  \"title\": \"Scan Me\",\n  \"url\": \"https://example.com\"\n}\n\nQR Options\n\nYou can customize QR code appearance:\n\n// Basic QR code\n<img src={qr('https://example.com')} />\n\n// With error correction level\n<img src={qr('https://example.com', { errorCorrectionLevel: 'H' })} />\n\n// With custom size\n<img src={qr('https://example.com', { width: 512 })} />\n\n\nError correction levels:\n\nL - Low (~7% correction)\nM - Medium (~15% correction) - default\nQ - Quartile (~25% correction)\nH - High (~30% correction)\nUser Configuration\n\nAccess user settings from .loopwind/loopwind.json using the config prop:\n\nexport default function BrandedTemplate({ tw, config, title }) {\n  // Access custom colors from loopwind.json\n  const primaryColor = config?.colors?.brand || '#6366f1';\n  \n  return (\n    <div style={tw('w-full h-full p-12')}>\n      <h1 style={{ \n        ...tw('text-6xl font-bold'),\n        color: primaryColor \n      }}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\n\nUser's .loopwind/loopwind.json:\n\n{\n  \"colors\": {\n    \"brand\": \"#ff6b6b\"\n  },\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"system-ui\", \"sans-serif\"]\n  }\n}\n\n\nThis allows templates to adapt to user preferences and brand guidelines.\n\nText on Path\n\nRender text along curves, circles, and custom paths with automatic character positioning and rotation.\n\nUsage\nexport default function CircleText({ tw, textPath, message }) {\n  return (\n    <div style={tw('relative w-full h-full bg-slate-900')}>\n      {textPath.onCircle(\n        message,\n        960,  // center x\n        540,  // center y\n        400,  // radius\n        0,    // rotation offset (0-1)\n        {\n          fontSize: \"4xl\",\n          fontWeight: \"bold\",\n          color: \"white\",\n          letterSpacing: 0.05\n        }\n      )}\n    </div>\n  );\n}\n\nAvailable Functions\n\nAll textPath functions return an array of positioned character elements:\n\ntextPath.onCircle(text, cx, cy, radius, offset, options?)\n\n// Text around a circle\ntextPath.onCircle(\"HELLO WORLD\", 960, 540, 400, 0, {\n  fontSize: \"4xl\",\n  color: \"white\"\n})\n\n\ntextPath.onPath(text, pathFollower, options?)\n\n// Text along any custom path\ntextPath.onPath(\"CUSTOM PATH\", (t) => ({\n  x: 100 + t * 800,\n  y: 200 + Math.sin(t * Math.PI) * 100,\n  angle: Math.cos(t * Math.PI) * 20\n}), {\n  fontSize: \"2xl\",\n  fontWeight: \"semibold\"\n})\n\n\ntextPath.onQuadratic(text, p0, p1, p2, options?)\n\n// Text along a quadratic Bezier curve\ntextPath.onQuadratic(\n  \"CURVED TEXT\",\n  { x: 200, y: 400 },   // start\n  { x: 960, y: 100 },   // control point\n  { x: 1720, y: 400 },  // end\n  { fontSize: \"3xl\", color: \"blue-300\" }\n)\n\n\ntextPath.onCubic(text, p0, p1, p2, p3, options?)\n\n// Text along a cubic Bezier curve\ntextPath.onCubic(\n  \"S-CURVE\",\n  { x: 200, y: 600 },   // start\n  { x: 600, y: 400 },   // control 1\n  { x: 1320, y: 800 },  // control 2\n  { x: 1720, y: 600 },  // end\n  { fontSize: \"3xl\", color: \"purple-300\" }\n)\n\n\ntextPath.onArc(text, cx, cy, radius, startAngle, endAngle, options?)\n\n// Text along a circular arc\ntextPath.onArc(\n  \"ARC TEXT\",\n  960,   // center x\n  540,   // center y\n  400,   // radius\n  0,     // start angle (degrees)\n  180,   // end angle (degrees)\n  { fontSize: \"2xl\", color: \"pink-300\" }\n)\n\nOptions\n\nAll textPath functions accept an optional options object:\n\n{\n  fontSize?: string;      // Tailwind size: \"xl\", \"2xl\", \"4xl\", etc.\n  fontWeight?: string;    // Tailwind weight: \"bold\", \"semibold\", etc.\n  color?: string;         // Tailwind color: \"white\", \"blue-500\", etc.\n  letterSpacing?: number; // Space between characters (0-1, default: 0)\n  style?: any;           // Additional inline styles\n}\n\nExamples\n\nAnimated rotating text:\n\nexport default function RotatingText({ tw, textPath, progress }) {\n  return (\n    <div style={tw('relative w-full h-full bg-black')}>\n      {textPath.onCircle(\n        \"SPINNING • TEXT • \",\n        960, 540, 400,\n        progress,  // Rotate based on video progress\n        { fontSize: \"3xl\", color: \"yellow-300\" }\n      )}\n    </div>\n  );\n}\n\n\nMultiple text paths:\n\nexport default function MultiPath({ tw, textPath }) {\n  return (\n    <div style={tw('relative w-full h-full bg-gradient-to-br from-slate-900 to-slate-700')}>\n      {/* Text on outer circle */}\n      {textPath.onCircle(\n        \"OUTER RING\",\n        960, 540, 500, 0,\n        { fontSize: \"5xl\", fontWeight: \"bold\", color: \"white\" }\n      )}\n\n      {/* Text on inner circle */}\n      {textPath.onCircle(\n        \"inner ring\",\n        960, 540, 300, 0.5,  // offset by 50% for rotation\n        { fontSize: \"2xl\", color: \"white/60\" }\n      )}\n    </div>\n  );\n}\n\n\nText following a drawn path:\n\nexport default function PathText({ tw, textPath }) {\n  return (\n    <div style={tw('relative w-full h-full bg-gray-900')}>\n      {/* Draw the path */}\n      <svg width=\"1920\" height=\"1080\" style={{ position: 'absolute' }}>\n        <path\n          d=\"M 200 400 Q 960 150 1720 400\"\n          stroke=\"rgba(255,255,255,0.2)\"\n          strokeWidth={2}\n          fill=\"none\"\n        />\n      </svg>\n\n      {/* Text following the path */}\n      {textPath.onQuadratic(\n        \"FOLLOWING THE CURVE\",\n        { x: 200, y: 400 },\n        { x: 960, y: 150 },\n        { x: 1720, y: 400 },\n        { fontSize: \"3xl\", fontWeight: \"bold\", color: \"blue-300\" }\n      )}\n    </div>\n  );\n}\n\n\nFor animated text paths, see Text Path Animations.\n\nReserved Prop Names\n\nThe following prop names are reserved and cannot be used in your template's meta.props:\n\ntw, qr, image, template - Core helpers\npath, textPath - Path and text helpers\nconfig, frame, progress - System props\n\nWhy? These names are used for loopwind's built-in helpers. Using them as prop names would cause conflicts.\n\nExample:\n\n// ❌ BAD - 'image' is reserved\nexport const meta = {\n  props: {\n    title: \"string\",\n    image: \"string\"  // Error!\n  }\n};\n\n// ✅ GOOD - Use descriptive alternatives\nexport const meta = {\n  props: {\n    title: \"string\",\n    imageUrl: \"string\",    // or imageSrc, photoUrl, etc.\n    logoUrl: \"string\"\n  }\n};\n\n\nIf you try to use a reserved name, you'll get a helpful error:\n\nTemplate uses reserved prop names: image\n\nTry renaming: \"image\" → \"imageUrl\" or \"imageSrc\"\n\nReserved names: tw, qr, image, template, path, textPath, config, frame, progress\n\nAll Props Reference\n\nEvery template receives these props:\n\nexport default function MyTemplate({\n  // Core helpers (RESERVED - cannot be used as prop names)\n  tw,        // Tailwind class converter\n  qr,        // QR code generator (this page)\n  template,  // Template composer (this page)\n  config,    // User config from loopwind.json (this page)\n  textPath,  // Text on path helpers (this page)\n\n  // Media helpers (RESERVED)\n  image,     // Image embedder → see /images\n  path,      // Path following → see /animation\n\n  // Video-specific (RESERVED - only in video templates)\n  frame,     // Current frame number → see /templates\n  progress,  // Animation progress 0-1 → see /templates\n\n  // Your custom props (use any names EXCEPT the reserved ones above)\n  ...props   // Any props from your meta.props\n}) {\n  // Your template code\n}\n\nNext Steps\nEmbedding Images\nTemplates\nStyling with Tailwind & shadcn/ui\nCustom Fonts\nStyling Templates\n\nStyle your templates with Tailwind utility classes and shadcn/ui's beautiful design system.\n\nQuick Start\nexport default function MyTemplate({ title, tw }) {\n  return (\n    <div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>\n      <h1 style={tw('text-7xl font-bold text-white')}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nThe tw() Function\n\nEvery template receives a tw() function that converts Tailwind classes to inline styles compatible with Satori:\n\n// Tailwind classes\ntw('flex items-center justify-center p-8 bg-blue-500')\n\n// Converts to inline styles:\n{\n  display: 'flex',\n  alignItems: 'center',\n  justifyContent: 'center',\n  padding: '2rem',\n  backgroundColor: '#3b82f6'\n}\n\nBasic Usage\nexport default function Banner({ title, subtitle, tw }) {\n  return (\n    <div style={tw('w-full h-full p-12 bg-gray-50')}>\n      <h1 style={tw('text-6xl font-bold text-gray-900 mb-4')}>\n        {title}\n      </h1>\n      <p style={tw('text-2xl text-gray-600')}>\n        {subtitle}\n      </p>\n    </div>\n  );\n}\n\nCombining with Custom Styles\n\nMix Tailwind classes with custom styles using the spread operator:\n\nexport default function CustomGradient({ title, tw }) {\n  return (\n    <div\n      style={{\n        ...tw('flex flex-col items-center justify-center w-full h-full p-20'),\n        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',\n      }}\n    >\n      <h1 style={tw('text-8xl font-bold text-white')}>{title}</h1>\n    </div>\n  );\n}\n\nshadcn/ui Design System\n\nloopwind uses shadcn/ui's design system by default, providing semantic color tokens for beautiful, consistent designs.\n\nDefault Color Palette\n\nAll templates automatically have access to these semantic colors defined in .loopwind/loopwind.json:\n\ncolors: {\n  // Primary colors\n  primary: '#18181b',           // Main brand color\n  'primary-foreground': '#fafafa',\n\n  // Secondary colors\n  secondary: '#f4f4f5',         // Subtle accents\n  'secondary-foreground': '#18181b',\n\n  // Background\n  background: '#ffffff',        // Page background\n  foreground: '#09090b',        // Main text color\n\n  // Muted\n  muted: '#f4f4f5',            // Subtle backgrounds\n  'muted-foreground': '#71717a', // Muted text\n\n  // Accent\n  accent: '#f4f4f5',           // Highlight color\n  'accent-foreground': '#18181b',\n\n  // Destructive\n  destructive: '#ef4444',       // Error/danger states\n  'destructive-foreground': '#fafafa',\n\n  // UI Elements\n  border: '#e4e4e7',           // Border color\n  input: '#e4e4e7',            // Input borders\n  ring: '#18181b',             // Focus rings\n  card: '#ffffff',             // Card background\n  'card-foreground': '#09090b',\n}\n\nUsing Semantic Colors\nexport default function SemanticCard({ title, description, price, tw }) {\n  return (\n    <div style={tw('bg-card border border-border rounded-lg p-6')}>\n      <h2 style={tw('text-card-foreground text-2xl font-bold mb-2')}>\n        {title}\n      </h2>\n      <p style={tw('text-muted-foreground mb-4')}>\n        {description}\n      </p>\n      <div style={tw('text-primary text-3xl font-bold')}>\n        ${price}\n      </div>\n    </div>\n  );\n}\n\nOpacity Modifiers\n\nUse Tailwind's slash syntax for opacity with any color:\n\nexport default function OpacityExample({ tw }) {\n  return (\n    <div style={tw('bg-primary/50')}>          {/* 50% opacity */}\n      <p style={tw('text-muted-foreground/75')}> {/* 75% opacity */}\n        Subtle text\n      </p>\n      <div style={tw('border border-border/30')}> {/* 30% opacity */}\n        Faint border\n      </div>\n    </div>\n  );\n}\n\n\nSupported syntax:\n\nbg-{color}/{opacity} - Background with opacity\ntext-{color}/{opacity} - Text with opacity\nborder-{color}/{opacity} - Border with opacity\nText Hierarchy\n// Primary text\ntw('text-foreground')\n\n// Secondary/muted text\ntw('text-muted-foreground')\n\n// Accent/brand text\ntw('text-primary')\n\n// Destructive/error text\ntw('text-destructive')\n\nBackgrounds\n// Page background\ntw('bg-background')\n\n// Card/elevated surfaces\ntw('bg-card')\n\n// Subtle backgrounds\ntw('bg-muted')\n\n// Accent backgrounds\ntw('bg-accent')\n\nSupported Tailwind Classes\nLayout\nDisplay: flex, inline-flex, block, inline-block, hidden\nFlex Direction: flex-row, flex-col, flex-row-reverse, flex-col-reverse\nJustify: justify-start, justify-end, justify-center, justify-between, justify-around\nAlign: items-start, items-end, items-center, items-baseline, items-stretch\nSpacing\nPadding: p-{n}, px-{n}, py-{n}, pt-{n}, pb-{n}, pl-{n}, pr-{n}\nMargin: m-{n}, mx-{n}, my-{n}, mt-{n}, mb-{n}, ml-{n}, mr-{n}\nGap: gap-{n}, gap-x-{n}, gap-y-{n}\nSizes: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64\n\nExamples:\n\ntw('p-4')      // padding: 1rem\ntw('px-8')     // paddingLeft: 2rem, paddingRight: 2rem\ntw('m-6')      // margin: 1.5rem\ntw('gap-4')    // gap: 1rem\n\nSizing\nWidth: w-{n}, w-full, w-screen, w-1/2, w-1/3, w-2/3\nHeight: h-{n}, h-full, h-screen\n\nExamples:\n\ntw('w-full')   // width: 100%\ntw('h-64')     // height: 16rem\ntw('w-1/2')    // width: 50%\n\nTypography\nFont Size: text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, text-4xl, text-5xl, text-6xl, text-7xl, text-8xl, text-9xl\nFont Weight: font-thin, font-light, font-normal, font-medium, font-semibold, font-bold, font-extrabold, font-black\nText Align: text-left, text-center, text-right\nLine Height: leading-none, leading-tight, leading-normal, leading-relaxed, leading-loose\nColors\n\nAll standard Tailwind colors plus shadcn semantic colors:\n\nStandard colors:\n\ntext-{color}-{shade}, bg-{color}-{shade}, border-{color}-{shade}\nColors: red, blue, green, yellow, purple, pink, gray, indigo, teal, orange\nShades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900\n\nshadcn semantic colors:\n\ntext-foreground, text-primary, text-muted-foreground, text-destructive\nbg-background, bg-card, bg-muted, bg-accent, bg-primary\nborder-border, border-input\ntw('text-blue-500')        // Standard Tailwind color\ntw('bg-purple-600')        // Standard Tailwind color\ntw('text-primary')         // shadcn semantic color\ntw('bg-card')              // shadcn semantic color\n\nPosition & Layout\nPosition: relative, absolute, fixed, sticky\nInset: inset-0, top-0, bottom-0, left-0, right-0\nZ-Index: z-0, z-10, z-20, z-30, z-40, z-50\nBorders\nBorder Width: border, border-{n}, border-t, border-b, border-l, border-r\nBorder Radius: rounded, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-3xl, rounded-full\nBorder Color: border-{color}-{shade}, border-border, border-input\nEffects\nShadow: shadow-sm, shadow, shadow-md, shadow-lg, shadow-xl, shadow-2xl\nOpacity: opacity-0, opacity-25, opacity-50, opacity-75, opacity-100\nFilters\nBlur: blur-none, blur-sm, blur, blur-md, blur-lg, blur-xl\nBrightness: brightness-0, brightness-50, brightness-100, brightness-150, brightness-200\nContrast: contrast-0, contrast-50, contrast-100, contrast-150, contrast-200\nGradients\nLinear Gradients\n// Gradient direction\ntw('bg-gradient-to-r')      // left to right\ntw('bg-gradient-to-br')     // top-left to bottom-right\ntw('bg-gradient-to-t')      // bottom to top\n\n// Gradient colors\ntw('from-blue-500')         // Start color\ntw('via-purple-500')        // Middle color\ntw('to-pink-500')           // End color\n\n// Complete gradient\ntw('bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500')\n\nGradient Examples\nexport default function GradientCard({ title, tw }) {\n  return (\n    <div style={tw('w-full h-full bg-gradient-to-br from-cyan-500 to-blue-600 p-12')}>\n      <h1 style={tw('text-white text-6xl font-bold')}>\n        {title}\n      </h1>\n    </div>\n  );\n}\n\nCustom Theme Colors\n\nYou can override the default shadcn colors or add your own custom colors in .loopwind/loopwind.json:\n\n{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#3b82f6\",\n      \"primary-foreground\": \"#ffffff\",\n      \"accent\": \"#10b981\",\n      \"brand\": \"#ff6b6b\"\n    }\n  }\n}\n\n\nThen use these custom colors in your templates:\n\ntw('text-brand')    // Uses your custom brand color\ntw('bg-primary')    // Uses your custom primary color\ntw('bg-accent')     // Uses your custom accent color\n\nAuto-Detection from tailwind.config.js\n\nloopwind automatically detects and loads your project's Tailwind configuration:\n\nyour-project/\n├── tailwind.config.js  ← Automatically detected\n└── .loopwind/\n    ├── loopwind.json\n    └── templates/\n\n\nThis includes:\n\nCustom colors\nCustom spacing values\nCustom fonts\nTheme extensions\nCustom utilities\nComplete Example\nexport default function ModernCard({ \n  tw, \n  image,\n  title, \n  description, \n  category,\n  author,\n  avatar \n}) {\n  return (\n    <div style={tw('w-full h-full bg-card')}>\n      {/* Hero image */}\n      <div style={tw('relative h-2/3')}>\n        <img \n          src={image(hero)} \n          style={tw('w-full h-full object-cover')}\n        />\n        {/* Category badge */}\n        <div style={tw('absolute top-4 left-4 bg-primary/90 backdrop-blur px-4 py-2 rounded-full')}>\n          <span style={tw('text-sm font-semibold text-primary-foreground')}>\n            {category}\n          </span>\n        </div>\n      </div>\n      \n      {/* Content */}\n      <div style={tw('h-1/3 p-8 flex flex-col justify-between')}>\n        <div>\n          <h2 style={tw('text-3xl font-bold text-foreground mb-2')}>\n            {title}\n          </h2>\n          <p style={tw('text-muted-foreground line-clamp-2')}>\n            {description}\n          </p>\n        </div>\n        \n        {/* Author */}\n        <div style={tw('flex items-center gap-3')}>\n          <img \n            src={image(avatar)} \n            style={tw('w-10 h-10 rounded-full border-2 border-border')}\n          />\n          <span style={tw('text-sm text-muted-foreground')}>\n            {author}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nWhy This Approach?\nSemantic naming: text-primary instead of text-blue-600\nConsistency: All templates use the same design language\nFlexibility: Easy to customize entire theme\nAccessibility: Pre-tested color contrasts\nModern: Same system as shadcn/ui components\nFamiliar: Standard Tailwind syntax\nNext Steps\nCustom Fonts\nBuilt-in Helpers\nTemplates\nEmbedding Images\nFont Handling in loopwind\n\nThe recommended way to use fonts is through loopwind.json - configure fonts once, use everywhere.\n\nUsing Fonts from loopwind.json (Recommended)\n\nConfigure fonts in your .loopwind/loopwind.json and use Tailwind classes in templates.\n\nSimple Setup\n\nDefine font families in .loopwind/loopwind.json without loading custom fonts (uses system fonts):\n\n{\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"system-ui\", \"-apple-system\", \"sans-serif\"],\n    \"serif\": [\"Georgia\", \"serif\"],\n    \"mono\": [\"Courier New\", \"monospace\"]\n  }\n}\n\n\nTemplate usage:\n\nexport default function({ title, tw }) {\n  return (\n    <div style={tw('w-full h-full')}>\n      {/* Uses fonts.sans from loopwind.json */}\n      <h1 style={tw('font-sans text-6xl font-bold')}>\n        {title}\n      </h1>\n\n      {/* Uses fonts.mono from loopwind.json */}\n      <code style={tw('font-mono text-sm')}>\n        {code}\n      </code>\n    </div>\n  );\n}\n\n\nResult: Uses system fonts, falls back to Inter for rendering.\n\nComplete Setup (With Font Files)\n\nLoad custom font files for brand-specific typography in .loopwind/loopwind.json:\n\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"system-ui\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    },\n    \"mono\": {\n      \"family\": [\"JetBrains Mono\", \"monospace\"],\n      \"files\": [\n        { \"path\": \"./fonts/JetBrainsMono-Regular.woff\", \"weight\": 400 }\n      ]\n    }\n  }\n}\n\n\nProject structure:\n\nyour-project/\n├── .loopwind/\n│   ├── loopwind.json\n│   └── templates/\n└── fonts/\n    ├── Inter-Regular.woff\n    ├── Inter-Bold.woff\n    └── JetBrainsMono-Regular.woff\n\n\nTemplate usage (same as before):\n\n<h1 style={tw('font-sans font-bold')}>\n  {/* Uses Inter Bold from loopwind.json */}\n  {title}\n</h1>\n\n\nAvailable classes:\n\nfont-sans - Uses fonts.sans from loopwind.json\nfont-serif - Uses fonts.serif from loopwind.json\nfont-mono - Uses fonts.mono from loopwind.json\n\nSupported formats:\n\n✅ WOFF (.woff) - Recommended for best compatibility\n✅ TTF (.ttf) - Also supported\n✅ OTF (.otf) - Also supported\n❌ WOFF2 (.woff2) - Not supported by renderer\nFont Loading Priority\n\nloopwind loads fonts in this order:\n\nloopwind.json fonts (if configured with files)\nBundled Inter fonts (included with CLI)\n\nThis ensures fonts work out of the box with no configuration.\n\nDefault Fonts\n\nIf no fonts are configured, loopwind uses Inter (Regular 400, Bold 700) which is bundled with the CLI. This means fonts work offline with no configuration required.\n\nBest Practices\n✅ Use loopwind.json for project-wide fonts - Configure once, use everywhere\n✅ Use font classes - tw('font-sans') instead of fontFamily: 'Inter'\n✅ Include fallbacks - Always add system fonts: [\"Inter\", \"system-ui\", \"sans-serif\"]\n✅ Match names - First font in family array is used as the loaded font name\n✅ Relative paths - Font paths are relative to loopwind.json location\nExamples\nMinimal Setup (System Fonts)\n{\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"-apple-system\", \"sans-serif\"]\n  }\n}\n\n\nUses system Inter if available, falls back to Noto Sans for rendering.\n\nBrand Fonts Setup\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Montserrat\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Montserrat-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Montserrat-Bold.woff\", \"weight\": 700 }\n      ]\n    }\n  }\n}\n\n\nLoads and uses Montserrat for all templates.\n\nMulti-Font Setup\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    },\n    \"serif\": {\n      \"family\": [\"Playfair Display\", \"serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Playfair-Regular.woff\", \"weight\": 400 }\n      ]\n    },\n    \"mono\": {\n      \"family\": [\"Fira Code\", \"monospace\"],\n      \"files\": [\n        { \"path\": \"./fonts/FiraCode-Regular.woff\", \"weight\": 400 }\n      ]\n    }\n  }\n}\n\n\nLoads different fonts for each style class.\n\nExternal Font URLs\n\nLoad fonts directly from CDNs without downloading files:\n\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff\",\n          \"weight\": 400\n        },\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff\",\n          \"weight\": 700\n        }\n      ]\n    }\n  }\n}\n\n\nYou can also mix local and external fonts:\n\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff\",\n          \"weight\": 700\n        }\n      ]\n    }\n  }\n}\n\n\nNote: Use WOFF format (.woff) for best compatibility. WOFF2 is not supported by the underlying renderer.\n\nPerformance\n✅ Font caching - Fonts load once and are cached for all renders\n✅ Video optimization - 90-frame video loads fonts once, not 90 times\n✅ No CDN delays - Local fonts load instantly\nNext Steps\nStyling with Tailwind & shadcn/ui\nBuilt-in Helpers\nTemplates\nEmbedding Images\nloopwind.json\n\nConfigure colors and fonts for all your templates in .loopwind/loopwind.json.\n\nFile Location\nyour-project/\n├── .loopwind/\n│   ├── loopwind.json     ← Configuration file\n│   └── templates/\n\nMinimal Example\n{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#3b82f6\",\n      \"background\": \"#ffffff\"\n    }\n  }\n}\n\nTheme Colors\nDefault shadcn/ui Palette\n{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#18181b\",\n      \"primary-foreground\": \"#fafafa\",\n      \n      \"secondary\": \"#f4f4f5\",\n      \"secondary-foreground\": \"#18181b\",\n      \n      \"background\": \"#ffffff\",\n      \"foreground\": \"#09090b\",\n      \n      \"muted\": \"#f4f4f5\",\n      \"muted-foreground\": \"#71717a\",\n      \n      \"accent\": \"#f4f4f5\",\n      \"accent-foreground\": \"#18181b\",\n      \n      \"destructive\": \"#ef4444\",\n      \"destructive-foreground\": \"#fafafa\",\n      \n      \"border\": \"#e4e4e7\",\n      \"input\": \"#e4e4e7\",\n      \"ring\": \"#18181b\",\n      \n      \"card\": \"#ffffff\",\n      \"card-foreground\": \"#09090b\"\n    }\n  }\n}\n\nCustom Colors\n{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#3b82f6\",\n      \"brand\": \"#ff6b6b\",\n      \"success\": \"#22c55e\",\n      \"warning\": \"#f59e0b\"\n    }\n  }\n}\n\n\nUse in templates:\n\ntw('text-brand')      // #ff6b6b\ntw('bg-success')      // #22c55e\ntw('border-warning')  // #f59e0b\n\nFonts\nSimple (System Fonts)\n{\n  \"fonts\": {\n    \"sans\": [\"Inter\", \"system-ui\", \"sans-serif\"],\n    \"serif\": [\"Georgia\", \"serif\"],\n    \"mono\": [\"Courier New\", \"monospace\"]\n  }\n}\n\nWith Font Files\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"system-ui\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    }\n  }\n}\n\n\nPaths are relative to loopwind.json.\n\nSupported formats:\n\n✅ WOFF (.woff)\n✅ TTF (.ttf)\n✅ OTF (.otf)\n❌ WOFF2 (.woff2)\nExternal URLs\n{\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        {\n          \"path\": \"https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff\",\n          \"weight\": 400\n        }\n      ]\n    }\n  }\n}\n\nComplete Example\n{\n  \"theme\": {\n    \"colors\": {\n      \"primary\": \"#6366f1\",\n      \"primary-foreground\": \"#ffffff\",\n      \"background\": \"#ffffff\",\n      \"foreground\": \"#0f172a\",\n      \"muted\": \"#f1f5f9\",\n      \"muted-foreground\": \"#64748b\",\n      \"border\": \"#e2e8f0\",\n      \"card\": \"#ffffff\",\n      \"brand\": \"#8b5cf6\"\n    }\n  },\n  \"fonts\": {\n    \"sans\": {\n      \"family\": [\"Inter\", \"sans-serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Inter-Regular.woff\", \"weight\": 400 },\n        { \"path\": \"./fonts/Inter-Bold.woff\", \"weight\": 700 }\n      ]\n    },\n    \"serif\": {\n      \"family\": [\"Playfair Display\", \"serif\"],\n      \"files\": [\n        { \"path\": \"./fonts/Playfair-Regular.woff\", \"weight\": 400 }\n      ]\n    }\n  }\n}\n\nSchema\n{\n  \"theme\"?: {\n    \"colors\"?: {\n      [name: string]: string;  // Hex color\n    }\n  },\n  \"fonts\"?: {\n    [class: string]: string[] | {\n      family: string[];\n      files: Array<{\n        path: string;    // Local or URL\n        weight: number;  // 100-900\n      }>;\n    }\n  }\n}\n\nAuto-Detection\n\nIf no loopwind.json exists, loopwind auto-detects tailwind.config.js:\n\nyour-project/\n├── tailwind.config.js  ← Auto-detected\n└── .loopwind/\n    └── templates/\n\n\nPriority:\n\n.loopwind/loopwind.json\ntailwind.config.js\nBuilt-in defaults\nNext Steps\nStyling - Use colors in templates\nFonts - Font configuration details\nGetting Started - Setup guide"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/tomtev/loopwind",
    "publisherUrl": "https://clawhub.ai/tomtev/loopwind",
    "owner": "tomtev",
    "version": "0.25.11",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/loopwind",
    "downloadUrl": "https://openagent3.xyz/downloads/loopwind",
    "agentUrl": "https://openagent3.xyz/skills/loopwind/agent",
    "manifestUrl": "https://openagent3.xyz/skills/loopwind/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/loopwind/agent.md"
  }
}