{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion",
  "type": "registry:lib",
  "title": "Motion",
  "description": "One deterministic progress-driven API, from raw curves to composed effects.",
  "dependencies": [
    "react"
  ],
  "files": [
    {
      "path": "src/lib/motion/index.ts",
      "type": "registry:lib",
      "target": "vanillasky/motion/index.ts",
      "content": "/**\n * Unified motion surface — ONE import for both motion vocabularies:\n *\n *  - Core curves (src/lib/react-animations/animation-utils.ts): `interpolate`,\n *    `spring`, `SPRING_SMOOTH/SNAPPY/BOUNCY/CRISP`, `Easing`, `stagger`,\n *    `cubicBezier`. Used by every built-in template.\n *  - Motion stdlib (src/lib/vibecode/motion-stdlib.ts): the export-verified\n *    high-level helpers — `phase`, `staggerWindow`, `cascade`, `typewriter`,\n *    `countUp`, `punch`, `glow`, `meshGradient`, `particles`, `burst`,\n *    `sweep`, `drift`, `orbit`, `EASE`, and friends. Parity matrix in\n *    docs/vibecode-stdlib-verification.md.\n *\n * This is the stable entrypoint for templates, custom scenes, the skill, and\n * the registry. Keep implementation files private behind this facade.\n */\n\nexport * from \"../react-animations/animation-utils\";\nexport * from \"../vibecode/motion-stdlib\";\n"
    },
    {
      "path": "src/lib/vibecode/motion-stdlib.ts",
      "type": "registry:lib",
      "target": "vanillasky/vibecode/motion-stdlib.ts",
      "content": "/**\n * Vibecode motion stdlib — export-verified, high-level motion vocabulary\n * injected into the vibecode sandbox scope (see scope.ts).\n *\n * Why this exists: handing a code-writing model raw CSS makes it reinvent\n * basics, badly. These helpers encode the craft moves of Remotion-grade\n * pieces (springy staggers, eased counters, layered glows, seeded particle\n * fields, path draws) as single calls the model composes.\n *\n * Hard rules — every helper in this file:\n *   - is PURE and DETERMINISTIC: same args → same output, always.\n *   - touches NO DOM, NO globals, NO Date/Math.random/timers.\n *   - is progress-driven: animation state comes in as `progress` (0..1).\n *   - emits only export-safe CSS: no `filter`, no transitions, no animations.\n *     (The export pipeline rasterizes SVG-as-image — see CLAUDE.md.)\n *\n * Naming is deliberately collision-resistant: multi-word identifiers a model\n * won't reach for as a loop variable (round-2 eval found `s`/`dim` shadowed\n * by `.map((s, si) => …)` params, producing NaN geometry).\n *\n * Every helper has a verified preview/export parity entry in\n * docs/vibecode-stdlib-verification.md. Don't add helpers without running\n * scripts/qa/vibecode-stdlib-demos.mjs.\n */\n\nimport type { CSSProperties } from \"react\";\nimport { cubicBezier } from \"../react-animations/animation-utils\";\n\nexport type EasingFn = (t: number) => number;\n\n/**\n * Re-export of the canonical cubic-bezier solver (also `Easing.bezier`).\n * cubicBezier(0.16, 1, 0.3, 1) === EASE.crispEnter\n */\nexport { cubicBezier };\n\n// ─── internals ───────────────────────────────────────────────────\n\nfunction clamp01(t: number): number {\n  return t < 0 ? 0 : t > 1 ? 1 : t;\n}\n\n/** Hex → rgba with alpha. Non-hex colors pass through unchanged. */\nfunction alpha(color: string, a: number): string {\n  const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(color);\n  if (!m) return color;\n  const h = m[1];\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const r = parseInt(full.slice(0, 2), 16);\n  const g = parseInt(full.slice(2, 4), 16);\n  const b = parseInt(full.slice(4, 6), 16);\n  return `rgba(${r},${g},${b},${Math.round(clamp01(a) * 1000) / 1000})`;\n}\n\n// ─── timing / physics ────────────────────────────────────────────\n\n/**\n * Named easing presets. Enter on the ease-OUT family, exit on ease-IN.\n * crispEnter / editorial / pop are the canonical VanillaSky curves from\n * docs/motion-library.md §1.\n */\nexport const EASE: Record<string, EasingFn> = {\n  linear: (t) => t,\n  outExpo: (t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t)),\n  outQuint: (t) => 1 - Math.pow(1 - t, 5),\n  outCubic: (t) => 1 - Math.pow(1 - t, 3),\n  /** Decelerating overshoot — lands from beyond 1. Great for card arrivals. */\n  outBack: (t) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    const c1 = 1.70158;\n    const c3 = c1 + 1;\n    return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);\n  },\n  inQuad: (t) => t * t,\n  inCubic: (t) => t * t * t,\n  inOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),\n  /** Pulls back below 0 before launching. Use for wind-up exits/entrances. */\n  anticipate: (t) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    const c = 2.0;\n    return t * t * ((c + 1) * t - c);\n  },\n  /** CRISP_ENTER — UI slide-ins, tight purposeful reveals. */\n  crispEnter: cubicBezier(0.16, 1, 0.3, 1),\n  /** EDITORIAL — calm holds, photo reveals, chart draws. */\n  editorial: cubicBezier(0.45, 0, 0.55, 1),\n  /** POP — stat pops, single-element arrivals with small overshoot. */\n  pop: cubicBezier(0.34, 1.56, 0.64, 1),\n};\n\n/**\n * Remap scene progress to a local 0..1 inside a [start, end] window,\n * clamped, optionally eased. The backbone of multi-beat scenes.\n *\n * const t = phase(progress, [0.2, 0.6], EASE.crispEnter);\n */\nexport function phase(\n  progress: number,\n  window: readonly [number, number],\n  easing?: EasingFn,\n): number {\n  const [start, end] = window;\n  if (end <= start) return progress >= end ? 1 : 0;\n  const t = clamp01((progress - start) / (end - start));\n  return easing ? easing(t) : t;\n}\n\nexport interface StaggerWindowOptions {\n  /** 0 = strictly sequential, 1 = all items animate together. Default 0.6. */\n  overlap?: number;\n  /** Animation order: \"start\" (default), \"end\" (reverse), \"center\" (outward), \"random\" (seeded shuffle). */\n  from?: \"start\" | \"end\" | \"center\" | \"random\";\n  /** Seed for from: \"random\" — same seed → same order every frame. Default \"stagger\". */\n  seed?: string | number;\n  easing?: EasingFn;\n}\n\n/**\n * Per-item progress for staggered groups with OVERLAPPING windows — unlike\n * `stagger()`, which is delay-based. Item 0 starts at progress 0; the last\n * item ends exactly at 1, so the stagger always fills the scene.\n *\n * items.map((item, i) => {\n *   const t = staggerWindow(progress, i, items.length, { overlap: 0.6, easing: EASE.pop });\n *   return <div style={{ opacity: Math.min(1, t), transform: `translateY(${(1 - t) * 40}px)` }}>…</div>;\n * })\n */\nexport function staggerWindow(\n  progress: number,\n  index: number,\n  count: number,\n  options?: StaggerWindowOptions,\n): number {\n  const { overlap = 0.6, from = \"start\", seed = \"stagger\", easing } = options ?? {};\n  const n = Math.max(1, Math.floor(count));\n  const ov = clamp01(overlap);\n  let order = Math.min(Math.max(index, 0), n - 1);\n  if (from === \"end\") order = n - 1 - order;\n  else if (from === \"center\") order = Math.abs(order - (n - 1) / 2) * 2;\n  else if (from === \"random\") order = rand01(seed, order) * (n - 1);\n  const widthFrac = 1 / (1 + (n - 1) * (1 - ov));\n  const step = n > 1 ? (1 - widthFrac) / (n - 1) : 0;\n  const t = clamp01((progress - order * step) / widthFrac);\n  return easing ? easing(t) : t;\n}\n\nexport interface PunchOptions {\n  /** Progress at which the punch peaks. Default 0.5. */\n  at?: number;\n  /** Window width of the punch. Default 0.25. */\n  width?: number;\n  /** Peak extra scale, e.g. 0.12 → 1 → 1.12 → 1. Default 0.12. */\n  amount?: number;\n}\n\n/**\n * A scale multiplier that bumps 1 → 1+amount → 1 around a moment.\n * Use for beat hits and emphasis: `transform: \\`scale(${punch(progress, { at: 0.8 })})\\``.\n */\nexport function punch(progress: number, options?: PunchOptions): number {\n  const { at = 0.5, width = 0.25, amount = 0.12 } = options ?? {};\n  if (width <= 0) return 1;\n  const t = (progress - (at - width / 2)) / width;\n  if (t <= 0 || t >= 1) return 1;\n  return 1 + amount * Math.sin(t * Math.PI);\n}\n\n// ─── typography ──────────────────────────────────────────────────\n\nexport interface CascadeOptions {\n  /** Split unit. Default \"word\". */\n  by?: \"word\" | \"char\";\n  /** Window overlap between items (see staggerWindow). Default 0.7. */\n  overlap?: number;\n  easing?: EasingFn;\n  /** Entrance travel in px. Default 28. */\n  distance?: number;\n  /** Travel direction. Default \"up\" (rises into place). */\n  direction?: \"up\" | \"down\";\n  from?: \"start\" | \"end\" | \"center\";\n}\n\nexport interface CascadeItem {\n  /** The word or character. */\n  item: string;\n  /** Eased local progress 0..1 (may overshoot 1 with springy easings). */\n  t: number;\n  /** Ready-to-spread inline style: opacity + translate + scale punch-in. */\n  style: CSSProperties;\n}\n\n/**\n * Per-word / per-char cascade — the word-cascade archetype as one call.\n * Returns ready-to-render spans:\n *\n * cascade(title, progress).map(({ item, style }, i) => (\n *   <span key={i} style={style}>{item}</span>\n * ))\n */\nexport function cascade(text: string, progress: number, options?: CascadeOptions): CascadeItem[] {\n  const { by = \"word\", overlap = 0.7, easing = EASE.pop, distance = 28, direction = \"up\", from = \"start\" } =\n    options ?? {};\n  const items =\n    by === \"char\" ? Array.from(String(text)) : String(text).split(/\\s+/).filter((w) => w.length > 0);\n  const n = items.length;\n  const dir = direction === \"down\" ? -1 : 1;\n  return items.map((item, i) => {\n    const t = staggerWindow(progress, i, n, { overlap, from, easing });\n    const style: CSSProperties = {\n      display: \"inline-block\",\n      whiteSpace: \"pre\",\n      opacity: clamp01(t * 1.4),\n      transform: `translateY(${((1 - t) * distance * dir).toFixed(2)}px) scale(${(0.88 + 0.12 * t).toFixed(3)})`,\n      ...(by === \"word\" && i < n - 1 ? { marginRight: \"0.26em\" } : null),\n    };\n    return { item, t, style };\n  });\n}\n\nexport interface TypewriterOptions {\n  /**\n   * Progress range over which typing happens (held after). Default [0, 0.7].\n   * (Named `range`, not `window` — the validator bans the `window` token.)\n   */\n  range?: readonly [number, number];\n  easing?: EasingFn;\n  /** Cursor blinks per scene. Default 6. Set 0 to hide. */\n  blinks?: number;\n}\n\nexport interface TypewriterResult {\n  /** The visible slice — render this directly. */\n  text: string;\n  /** True when the block cursor should be visible this frame. */\n  cursorOn: boolean;\n  done: boolean;\n}\n\n/**\n * Typewriter via string slicing (never per-char opacity — it reads as fade,\n * not typing). Deterministic blink derived from progress.\n *\n * const tw = typewriter(cmd, progress);\n * <span>{tw.text}{tw.cursorOn ? \"▋\" : \" \"}</span>\n */\nexport function typewriter(text: string, progress: number, options?: TypewriterOptions): TypewriterResult {\n  const { range: win = [0, 0.7], easing = EASE.linear, blinks = 6 } = options ?? {};\n  const chars = Array.from(String(text));\n  const t = phase(progress, win, easing);\n  const visible = t >= 1 ? chars.length : Math.floor(t * chars.length);\n  const done = visible >= chars.length;\n  const cursorOn = blinks > 0 && Math.floor(clamp01(progress) * blinks * 2) % 2 === 0;\n  return { text: chars.slice(0, visible).join(\"\"), cursorOn, done };\n}\n\nexport interface CountUpOptions {\n  start?: number;\n  /** Decimal places. Defaults to 1 if target is non-integer, else 0. */\n  decimals?: number;\n  easing?: EasingFn;\n  /** \"plain\" → 12,847 · \"compact\" → 12.8K. Default \"plain\". */\n  format?: \"plain\" | \"compact\";\n  prefix?: string;\n  suffix?: string;\n}\n\nexport interface CountUpResult {\n  value: number;\n  /** Formatted display string including prefix/suffix. */\n  text: string;\n  /** Scale multiplier with a landing punch near the end — apply via transform. */\n  scale: number;\n}\n\n/**\n * Eased counter with deterministic formatting (no locale dependence) and a\n * scale punch as the number lands.\n *\n * const { text, scale } = countUp(progress, 12847, { format: \"compact\", suffix: \" users\" });\n * <div style={{ transform: `scale(${scale})` }}>{text}</div>\n */\nexport function countUp(progress: number, target: number, options?: CountUpOptions): CountUpResult {\n  const {\n    start = 0,\n    easing = EASE.outExpo,\n    format = \"plain\",\n    prefix = \"\",\n    suffix = \"\",\n  } = options ?? {};\n  const decimals = options?.decimals ?? (Number.isInteger(target) ? 0 : 1);\n  const e = easing(clamp01(progress));\n  const value = start + (target - start) * e;\n  let body: string;\n  if (format === \"compact\" && Math.abs(value) >= 1000) {\n    const units: Array<[number, string]> = [\n      [1e9, \"B\"],\n      [1e6, \"M\"],\n      [1e3, \"K\"],\n    ];\n    const [div, unit] = units.find(([d]) => Math.abs(value) >= d) as [number, string];\n    const scaled = value / div;\n    body = `${(Math.round(scaled * 10) / 10).toFixed(Math.abs(scaled) >= 100 ? 0 : 1)}${unit}`;\n  } else {\n    const fixed = value.toFixed(decimals);\n    const [int, frac] = fixed.split(\".\");\n    const sign = int.startsWith(\"-\") ? \"-\" : \"\";\n    const digits = sign ? int.slice(1) : int;\n    let grouped = \"\";\n    for (let i = 0; i < digits.length; i++) {\n      if (i > 0 && (digits.length - i) % 3 === 0) grouped += \",\";\n      grouped += digits[i];\n    }\n    body = `${sign}${grouped}${frac ? `.${frac}` : \"\"}`;\n  }\n  const scale = punch(progress, { at: 0.88, width: 0.24, amount: 0.08 });\n  return { value, text: `${prefix}${body}${suffix}`, scale };\n}\n\n// ─── layout ──────────────────────────────────────────────────────\n\n/**\n * Absolute-center an element at (xPct%, yPct%) of its container — the\n * translate(-50%,-50%) idiom done right. Extra transforms compose AFTER\n * the centering translate so scale/rotate don't break the anchor.\n *\n * <div style={{ ...center(50, 42, `scale(${pop})`), width: 600 }}>…</div>\n */\nexport function center(xPct = 50, yPct = 50, extraTransform = \"\"): CSSProperties {\n  return {\n    position: \"absolute\",\n    left: `${xPct}%`,\n    top: `${yPct}%`,\n    transform: `translate(-50%, -50%)${extraTransform ? ` ${extraTransform}` : \"\"}`,\n  };\n}\n\n// ─── light / depth (no CSS filter — export-safe) ─────────────────\n\nexport interface GlowOptions {\n  /** Diameter in px — scale with sceneScale, e.g. 520 * sceneScale. Default 480. */\n  size?: number;\n  /** CSS position of the glow center. Defaults \"50%\" / \"50%\". */\n  x?: string;\n  y?: string;\n  /** Core opacity 0..1. Default 0.4. */\n  intensity?: number;\n}\n\n/**\n * A layered radial-gradient glow blob (absolutely positioned, pointer-inert).\n * The export-safe replacement for `filter: blur()` light.\n *\n * <div style={glow(accent, { size: 600 * sceneScale, y: \"42%\" })} />\n */\nexport function glow(color: string, options?: GlowOptions): CSSProperties {\n  const { size = 480, x = \"50%\", y = \"50%\", intensity = 0.4 } = options ?? {};\n  return {\n    position: \"absolute\",\n    left: x,\n    top: y,\n    width: size,\n    height: size,\n    transform: \"translate(-50%, -50%)\",\n    borderRadius: \"50%\",\n    pointerEvents: \"none\",\n    // `closest-side` — the default (farthest-corner) only reaches transparent at\n    // the box edge, so a large bloom can show a faint rectangular seam.\n    background: `radial-gradient(circle closest-side, ${alpha(color, intensity)} 0%, ${alpha(\n      color,\n      intensity * 0.45,\n    )} 32%, transparent 72%)`,\n  };\n}\n\n/**\n * Layered box-shadow stack for believable depth (single shadows look flat).\n * elevation 1 (card resting) … 5 (floating hero). Returns the boxShadow string.\n */\nexport function softShadow(elevation = 3, color = \"#000\"): string {\n  const e = Math.min(5, Math.max(1, elevation));\n  return [\n    `0 ${2 * e}px ${4 * e}px ${alpha(color, 0.16)}`,\n    `0 ${6 * e}px ${16 * e}px ${alpha(color, 0.2)}`,\n    `0 ${12 * e}px ${40 * e}px ${alpha(color, 0.24)}`,\n  ].join(\", \");\n}\n\n/**\n * Full-bleed darkened-edges overlay (focus pull). strength 0..1.\n * Render LAST inside the body so it sits above content, or depth \"foreground\".\n */\nexport function vignette(strength = 0.5): CSSProperties {\n  return {\n    position: \"absolute\",\n    inset: 0,\n    pointerEvents: \"none\",\n    background: `radial-gradient(ellipse at 50% 45%, rgba(0,0,0,0) 52%, rgba(0,0,0,${\n      Math.round(clamp01(strength) * 100) / 100\n    }) 100%)`,\n  };\n}\n\n/**\n * Deterministic mesh-gradient background string — 2-4 colors as soft radial\n * blobs at seeded positions over a base layer. Use as `background`.\n *\n * background: meshGradient([accent, secondary, \"#1a1040\"], \"hero\")\n */\nexport function meshGradient(colors: string[], seed: string | number = 1): string {\n  const list = colors.length > 0 ? colors.slice(0, 4) : [\"#222\"];\n  const layers = list.map((c, i) => {\n    const cx = Math.round(12 + rand01(seed, i * 3) * 76);\n    const cy = Math.round(10 + rand01(seed, i * 3 + 1) * 80);\n    const r = Math.round(40 + rand01(seed, i * 3 + 2) * 35);\n    return `radial-gradient(circle at ${cx}% ${cy}%, ${alpha(c, 0.85)} 0%, transparent ${r}%)`;\n  });\n  layers.push(`linear-gradient(180deg, ${alpha(list[0], 0.5)} 0%, rgba(0,0,0,0.9) 100%)`);\n  return layers.join(\", \");\n}\n\n/**\n * Film-grain texture overlay — a pre-rasterized 96×96 seeded-noise PNG tile\n * (data URI). Why PNG and not SVG feTurbulence: SVG <filter> elements do NOT\n * execute when a data-URI SVG is rasterized inside the export pipeline's\n * SVG-as-image pass (verified by scripts/qa/vibecode-stdlib-demos.mjs — the\n * turbulence probe exports flat). PNG data URIs render identically in\n * preview and export (same mechanism as the emoji-PNG pipeline).\n *\n * The tile was generated once from rand01(\"vanillasky-grain\", i) — fully\n * deterministic. `seed` shifts the tile offset so layered grains never align.\n */\nconst GRAIN_TILE_PX = 96;\nconst GRAIN_TILE_URI =\n  \"data:image/png;base64,\" +\n  \"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\";\n\nexport function grain(opacity = 0.08, seed: string | number = 2): CSSProperties {\n  const ox = Math.floor(rand01(seed, 0) * GRAIN_TILE_PX);\n  const oy = Math.floor(rand01(seed, 1) * GRAIN_TILE_PX);\n  return {\n    position: \"absolute\",\n    inset: 0,\n    pointerEvents: \"none\",\n    opacity: clamp01(opacity),\n    backgroundImage: `url(\"${GRAIN_TILE_URI}\")`,\n    backgroundRepeat: \"repeat\",\n    backgroundPosition: `${ox}px ${oy}px`,\n    backgroundSize: `${GRAIN_TILE_PX}px ${GRAIN_TILE_PX}px`,\n  };\n}\n\nexport interface DriftOptions {\n  /** Total x travel in px over the scene. Default 0. */\n  x?: number;\n  /** Total y travel in px over the scene. Default 0. */\n  y?: number;\n  easing?: EasingFn;\n  /** Extra transform appended after the drift translate. */\n  extraTransform?: string;\n}\n\n/**\n * Slow continuous travel — returns a transform string. Layer different\n * x/y per element for parallax depth (background drifts less than focal).\n *\n * transform: drift(progress, { y: -40 * sceneScale })\n */\nexport function drift(progress: number, options?: DriftOptions): string {\n  const { x = 0, y = 0, easing = EASE.editorial, extraTransform = \"\" } = options ?? {};\n  const e = easing(clamp01(progress));\n  return `translate(${(x * e).toFixed(2)}px, ${(y * e).toFixed(2)}px)${\n    extraTransform ? ` ${extraTransform}` : \"\"\n  }`;\n}\n\n// ─── shape / SVG ─────────────────────────────────────────────────\n\n/**\n * Stroke draw-on for SVG paths/circles/polylines. Pass the path length\n * (circle: 2πr). Spread onto the SVG element's style or attributes.\n *\n * <path d=\"…\" style={{ ...pathDraw(progress, 1200), stroke: accent, fill: \"none\" }} />\n */\nexport function pathDraw(\n  progress: number,\n  pathLength: number,\n  easing: EasingFn = EASE.editorial,\n): { strokeDasharray: number; strokeDashoffset: number } {\n  const e = easing(clamp01(progress));\n  return { strokeDasharray: pathLength, strokeDashoffset: pathLength * (1 - e) };\n}\n\nexport interface OrbitOptions {\n  /** Orbit center. */\n  cx: number;\n  cy: number;\n  radius: number;\n  /** Full revolutions over the scene. Default 1. */\n  turns?: number;\n  /** Start angle in degrees, -90 = 12 o'clock. Default -90. */\n  startDeg?: number;\n  clockwise?: boolean;\n}\n\n/**\n * Position on a circular orbit at the given progress.\n * Returns { x, y, deg } — deg is the current angle (use for rotate()).\n */\nexport function orbit(progress: number, options: OrbitOptions): { x: number; y: number; deg: number } {\n  const { cx, cy, radius, turns = 1, startDeg = -90, clockwise = true } = options;\n  const deg = startDeg + (clockwise ? 1 : -1) * clamp01(progress) * turns * 360;\n  const rad = (deg * Math.PI) / 180;\n  return { x: cx + Math.cos(rad) * radius, y: cy + Math.sin(rad) * radius, deg };\n}\n\n/**\n * Interpolate between two same-length point lists → SVG `points` string\n * for <polygon>/<polyline> morphs.\n *\n * <polygon points={morph(progress, triangle, star, EASE.crispEnter)} fill={accent} />\n */\nexport function morph(\n  progress: number,\n  fromPoints: ReadonlyArray<readonly [number, number]>,\n  toPoints: ReadonlyArray<readonly [number, number]>,\n  easing: EasingFn = EASE.inOutCubic,\n): string {\n  const e = easing(clamp01(progress));\n  const n = Math.min(fromPoints.length, toPoints.length);\n  const pts: string[] = [];\n  for (let i = 0; i < n; i++) {\n    const x = fromPoints[i][0] + (toPoints[i][0] - fromPoints[i][0]) * e;\n    const y = fromPoints[i][1] + (toPoints[i][1] - fromPoints[i][1]) * e;\n    pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);\n  }\n  return pts.join(\" \");\n}\n\nexport type SweepDirection = \"left\" | \"right\" | \"up\" | \"down\" | \"open\";\n\n/**\n * Masked wipe reveal via clip-path inset (verified export-safe).\n * \"left\" reveals left→right, \"open\" reveals center-out.\n *\n * <div style={{ ...sweep(progress, \"left\"), position: \"relative\" }}>…</div>\n */\nexport function sweep(\n  progress: number,\n  direction: SweepDirection = \"left\",\n  easing: EasingFn = EASE.crispEnter,\n): { clipPath: string } {\n  const e = clamp01(easing(clamp01(progress)));\n  const rest = ((1 - e) * 100).toFixed(2);\n  switch (direction) {\n    case \"right\":\n      return { clipPath: `inset(0% 0% 0% ${rest}%)` };\n    case \"up\":\n      return { clipPath: `inset(${rest}% 0% 0% 0%)` };\n    case \"down\":\n      return { clipPath: `inset(0% 0% ${rest}% 0%)` };\n    case \"open\":\n      return { clipPath: `inset(0% ${(parseFloat(rest) / 2).toFixed(2)}% 0% ${(parseFloat(rest) / 2).toFixed(2)}%)` };\n    case \"left\":\n    default:\n      return { clipPath: `inset(0% ${rest}% 0% 0%)` };\n  }\n}\n\n// ─── particles ───────────────────────────────────────────────────\n\n/**\n * Deterministic pseudo-random 0..1 from a seed + stream index.\n * FNV-1a + avalanche — same (seed, index) always yields the same value.\n * THE replacement for Math.random in vibecoded scenes.\n */\nexport function rand01(seed: string | number, index = 0): number {\n  let h = 2166136261 >>> 0;\n  const str = String(seed);\n  for (let i = 0; i < str.length; i++) {\n    h ^= str.charCodeAt(i);\n    h = Math.imul(h, 16777619) >>> 0;\n  }\n  h = (h + Math.imul(index + 1, 0x9e3779b9)) >>> 0;\n  h ^= h >>> 16;\n  h = Math.imul(h, 0x85ebca6b) >>> 0;\n  h ^= h >>> 13;\n  h = Math.imul(h, 0xc2b2ae35) >>> 0;\n  h ^= h >>> 16;\n  return (h >>> 0) / 4294967296;\n}\n\nexport interface ParticlesOptions {\n  /** \"float\" ambient bobbing · \"rise\" upward stream · \"rain\" downward. Default \"float\". */\n  mode?: \"float\" | \"rise\" | \"rain\";\n  /** Travel speed multiplier. Default 1. */\n  speed?: number;\n  /** Particle size range in px (scale with sceneScale at call site). Default [4, 10]. */\n  sizeRange?: readonly [number, number];\n}\n\nexport interface Particle {\n  /** Position as 0..1 fractions of the field — multiply by width/height. */\n  x: number;\n  y: number;\n  /** Size in px (from sizeRange). */\n  size: number;\n  /** 0..1 — already fades near field edges, no popping. */\n  opacity: number;\n  /** Degrees. */\n  rotation: number;\n}\n\n/**\n * Seeded ambient particle field. Deterministic: same (count, seed, progress)\n * → identical field. Map to absolutely-positioned divs:\n *\n * particles(progress, 24, \"stars\").map((p, i) => (\n *   <div key={i} style={{ position: \"absolute\", left: p.x * width, top: p.y * height,\n *     width: p.size, height: p.size, borderRadius: \"50%\", background: accent, opacity: p.opacity }} />\n * ))\n */\nexport function particles(\n  progress: number,\n  count: number,\n  seed: string | number,\n  options?: ParticlesOptions,\n): Particle[] {\n  const { mode = \"float\", speed = 1, sizeRange = [4, 10] } = options ?? {};\n  const p = clamp01(progress);\n  const out: Particle[] = [];\n  for (let i = 0; i < count; i++) {\n    const baseX = rand01(seed, i * 7);\n    const baseY = rand01(seed, i * 7 + 1);\n    const sizeT = rand01(seed, i * 7 + 2);\n    const phase0 = rand01(seed, i * 7 + 3);\n    const wobble = 0.015 + rand01(seed, i * 7 + 4) * 0.025;\n    const x = baseX + Math.sin((p * speed + phase0) * Math.PI * 3) * wobble;\n    let y: number;\n    if (mode === \"rise\") y = (((baseY - p * speed * 0.35) % 1) + 1) % 1;\n    else if (mode === \"rain\") y = (baseY + p * speed * 0.35) % 1;\n    else y = baseY + Math.sin((p * speed + phase0) * Math.PI * 2) * wobble * 1.5;\n    const edgeFade = clamp01(Math.sin(Math.PI * clamp01(y)) * 2);\n    out.push({\n      x,\n      y,\n      size: sizeRange[0] + (sizeRange[1] - sizeRange[0]) * sizeT,\n      opacity: edgeFade * (0.45 + 0.55 * rand01(seed, i * 7 + 5)),\n      rotation: (phase0 * 360 + p * speed * 240) % 360,\n    });\n  }\n  return out;\n}\n\nexport interface BurstOptions {\n  /** Cone width in degrees. Default 360 (full radial burst). */\n  spreadDeg?: number;\n  /** Cone center direction in degrees, -90 = up. Default -90. */\n  originDeg?: number;\n  /** Downward pull applied over time (0 = none). Default 0.35. */\n  gravity?: number;\n  easing?: EasingFn;\n}\n\nexport interface BurstParticle {\n  /** Offset from origin in UNIT radius — multiply by px radius at call site. */\n  dx: number;\n  dy: number;\n  rotation: number;\n  scale: number;\n  opacity: number;\n}\n\n/**\n * Seeded radial burst (confetti / celebration). Particles fly outward from a\n * shared origin as progress runs 0→1, with gravity and a fade tail.\n *\n * burst(phase(progress, [0.55, 1]), 32, \"confetti\").map((b, i) => (\n *   <div key={i} style={{ position: \"absolute\",\n *     left: width / 2 + b.dx * minDim * 0.42, top: height * 0.45 + b.dy * minDim * 0.42,\n *     width: 12, height: 12, background: i % 2 ? accent : secondary,\n *     opacity: b.opacity, transform: `rotate(${b.rotation}deg) scale(${b.scale})` }} />\n * ))\n */\nexport function burst(\n  progress: number,\n  count: number,\n  seed: string | number,\n  options?: BurstOptions,\n): BurstParticle[] {\n  const { spreadDeg = 360, originDeg = -90, gravity = 0.35, easing = EASE.outCubic } = options ?? {};\n  const e = easing(clamp01(progress));\n  const out: BurstParticle[] = [];\n  for (let i = 0; i < count; i++) {\n    const jitter = rand01(seed, i * 5) * 0.8;\n    const angleDeg = originDeg - spreadDeg / 2 + (spreadDeg * (i + jitter)) / Math.max(1, count);\n    const rad = (angleDeg * Math.PI) / 180;\n    const dist = e * (0.5 + 0.5 * rand01(seed, i * 5 + 1));\n    out.push({\n      dx: Math.cos(rad) * dist,\n      dy: Math.sin(rad) * dist + gravity * e * e,\n      rotation: (rand01(seed, i * 5 + 2) * 720 * e) % 360,\n      scale: 1 - 0.45 * e * rand01(seed, i * 5 + 3),\n      opacity: e < 0.7 ? 1 : clamp01((1 - e) / 0.3),\n    });\n  }\n  return out;\n}\n"
    },
    {
      "path": "src/lib/react-animations/animation-utils.ts",
      "type": "registry:lib",
      "target": "vanillasky/react-animations/animation-utils.ts",
      "content": "/**\n * Animation utilities — interpolate, spring, easing.\n * Pure math functions, no external dependencies.\n * Inspired by Remotion's API but fully independent.\n */\n\n// ─── Easing functions ────────────────────────────────────────────\n\n/**\n * CSS-style cubic-bezier curve → easing function (t: 0..1 → eased value).\n * Deterministic Newton-Raphson with bisection fallback — same approach as\n * the browser's cubic-bezier(). y values outside [0, 1] produce overshoot.\n *\n * docs/motion-library.md §1 references curves as `Easing.bezier(...)` —\n * this is that function (also exposed as `Easing.bezier`).\n */\nexport function cubicBezier(\n  x1: number,\n  y1: number,\n  x2: number,\n  y2: number,\n): (t: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;\n  const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;\n  const sampleDX = (t: number) => (3 * ax * t + 2 * bx) * t + cx;\n  return (x: number): number => {\n    if (x <= 0) return 0;\n    if (x >= 1) return 1;\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(t) - x;\n      if (Math.abs(err) < 1e-6) return sampleY(t);\n      const d = sampleDX(t);\n      if (Math.abs(d) < 1e-6) break;\n      t -= err / d;\n    }\n    // Bisection fallback — sampleX is monotonic on [0, 1] for valid curves.\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 24; i++) {\n      const v = sampleX(t);\n      if (Math.abs(v - x) < 1e-6) break;\n      if (v < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return sampleY(t);\n  };\n}\n\nexport const Easing = {\n  /** Easing.bezier(0.16, 1, 0.3, 1) — the canonical curve syntax from docs/motion-library.md. */\n  bezier: cubicBezier,\n  // Curves\n  linear: (t: number) => t,\n  quad: (t: number) => t * t,\n  cubic: (t: number) => t * t * t,\n  sin: (t: number) => 1 - Math.cos((t * Math.PI) / 2),\n  exp: (t: number) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))),\n  circle: (t: number) => 1 - Math.sqrt(1 - t * t),\n\n  // Directional modifiers\n  in: (fn: (t: number) => number) => fn,\n  out:\n    (fn: (t: number) => number) =>\n    (t: number) =>\n      1 - fn(1 - t),\n  inOut:\n    (fn: (t: number) => number) =>\n    (t: number) =>\n      t < 0.5 ? fn(t * 2) / 2 : 1 - fn((1 - t) * 2) / 2,\n} as const;\n\n// ─── Interpolate ─────────────────────────────────────────────────\n\ninterface InterpolateOptions {\n  easing?: (t: number) => number;\n  extrapolateLeft?: \"clamp\" | \"extend\";\n  extrapolateRight?: \"clamp\" | \"extend\";\n}\n\n/**\n * Maps a value from one range to another with optional easing and clamping.\n *\n * @example\n * interpolate(0.5, [0, 1], [0, 100]) // 50\n * interpolate(progress, [0, 0.7], [0, 10000], { extrapolateRight: \"clamp\" })\n */\nexport function interpolate(\n  value: number,\n  inputRange: readonly [number, ...number[]],\n  outputRange: readonly [number, ...number[]],\n  options?: InterpolateOptions,\n): number {\n  const { easing, extrapolateLeft = \"extend\", extrapolateRight = \"extend\" } = options ?? {};\n\n  // Find the segment\n  let i = 0;\n  for (; i < inputRange.length - 2; i++) {\n    if (value < inputRange[i + 1]) break;\n  }\n\n  const inputMin = inputRange[i];\n  const inputMax = inputRange[i + 1];\n  const outputMin = outputRange[i];\n  const outputMax = outputRange[i + 1];\n\n  // Normalize to 0-1\n  let t = inputMax === inputMin ? 0 : (value - inputMin) / (inputMax - inputMin);\n\n  // Clamp\n  if (t < 0 && extrapolateLeft === \"clamp\") t = 0;\n  if (t > 1 && extrapolateRight === \"clamp\") t = 1;\n\n  // Apply easing\n  if (easing && t >= 0 && t <= 1) {\n    t = easing(t);\n  }\n\n  return outputMin + t * (outputMax - outputMin);\n}\n\n// ─── Spring ──────────────────────────────────────────────────────\n\nexport interface SpringConfig {\n  damping?: number;\n  stiffness?: number;\n  mass?: number;\n}\n\n// Presets\nexport const SPRING_SMOOTH: SpringConfig = { damping: 26, stiffness: 170 };\nexport const SPRING_SNAPPY: SpringConfig = { damping: 20, stiffness: 300 };\nexport const SPRING_BOUNCY: SpringConfig = { damping: 10, stiffness: 180 };\n// Lower damping than SNAPPY + higher stiffness → subtle overshoot, quick settle.\n// Sits between SNAPPY (no overshoot) and BOUNCY (lots) for a \"punchy but not silly\" feel.\nexport const SPRING_CRISP: SpringConfig = { damping: 14, stiffness: 320 };\n\n/**\n * Spring physics simulation. Converts linear progress (0→1) to\n * spring-eased progress with overshoot and settle.\n *\n * @param progress - Linear progress 0→1\n * @param config - Spring physical properties\n * @returns Spring-eased value (may overshoot 1 with low damping)\n *\n * @example\n * const scale = spring(progress, SPRING_BOUNCY); // bouncy entrance\n * const opacity = spring(progress, SPRING_SMOOTH); // smooth fade\n */\nexport function spring(progress: number, config?: SpringConfig): number {\n  if (progress <= 0) return 0;\n  if (progress >= 1) {\n    // For high-damping (no overshoot) configs, settle at 1\n    const { damping = 26 } = config ?? {};\n    if (damping >= 20) return 1;\n  }\n\n  const { damping = 26, stiffness = 170, mass = 1 } = config ?? {};\n\n  // Simulate spring physics at the given progress point\n  // We run a fixed number of iterations to find the spring value\n  const omega = Math.sqrt(stiffness / mass);\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n\n  // Scale progress to a time value (spring needs ~1-3s to settle)\n  const t = progress * 3.5;\n\n  let value: number;\n\n  if (zeta < 1) {\n    // Underdamped (bouncy)\n    const omegaD = omega * Math.sqrt(1 - zeta * zeta);\n    value = 1 - Math.exp(-zeta * omega * t) * (Math.cos(omegaD * t) + (zeta * omega / omegaD) * Math.sin(omegaD * t));\n  } else if (zeta === 1) {\n    // Critically damped\n    value = 1 - Math.exp(-omega * t) * (1 + omega * t);\n  } else {\n    // Overdamped\n    const s1 = -omega * (zeta + Math.sqrt(zeta * zeta - 1));\n    const s2 = -omega * (zeta - Math.sqrt(zeta * zeta - 1));\n    value = 1 + (s1 * Math.exp(s2 * t) - s2 * Math.exp(s1 * t)) / (s2 - s1);\n  }\n\n  return value;\n}\n\n// ─── Stagger helper ──────────────────────────────────────────────\n\n/**\n * Returns a progress value for a staggered animation item.\n * Maps overall progress to per-item progress with delay.\n *\n * @param progress - Overall scene progress (0→1)\n * @param index - Item index (0-based)\n * @param total - Total number of items\n * @param staggerDelay - Delay between items (0→1 scale, default 0.08)\n * @param startAt - When the first item should start appearing (default 0.1)\n *\n * @example\n * items.map((item, i) => {\n *   const itemProgress = stagger(progress, i, items.length);\n *   const opacity = spring(itemProgress, SPRING_SMOOTH);\n *   return <div style={{ opacity }}>{item}</div>;\n * })\n */\nexport function stagger(\n  progress: number,\n  index: number,\n  total: number,\n  staggerDelay = 0.08,\n  startAt = 0.1,\n): number {\n  const itemStart = startAt + index * staggerDelay;\n  const available = 1 - startAt - (total - 1) * staggerDelay;\n  const itemDuration = Math.max(0.1, available);\n  return Math.max(0, Math.min(1, (progress - itemStart) / itemDuration));\n}\n"
    }
  ],
  "meta": {
    "vanillasky": {
      "layer": "lib",
      "tier": "free",
      "domain": "motion",
      "level": "foundation",
      "audiences": [
        "custom-scenes",
        "react-developers"
      ],
      "dependencies": [],
      "useWhen": "Use it whenever a value changes with scene progress; start with a composed helper, then drop to interpolation or springs when the scene needs custom choreography.",
      "avoidWhen": "Do not use browser timers, CSS transitions, requestAnimationFrame, or Math.random inside a renderable scene."
    }
  }
}
