{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "backgrounds",
  "type": "registry:lib",
  "title": "Backgrounds",
  "description": "Paint deterministic brand backdrops and keep their visual layer moving.",
  "dependencies": [
    "react"
  ],
  "registryDependencies": [
    "@vanillasky/motion",
    "@vanillasky/theme"
  ],
  "files": [
    {
      "path": "src/lib/backgrounds/index.ts",
      "type": "registry:lib",
      "target": "vanillasky/backgrounds/index.ts",
      "content": "/** Brand-safe scene backgrounds and continuous background motion. */\nexport {\n  TOP_TEXT_AREA_RATIO,\n  gradientBackground,\n  BrandGradientOverlay,\n} from \"../scene-templates/color-utils\";\nexport * from \"../scene-templates/background-effect\";\n"
    },
    {
      "path": "src/lib/scene-templates/background-effect.ts",
      "type": "registry:lib",
      "target": "vanillasky/scene-templates/background-effect.ts",
      "content": "/**\n * Background motion effects for templates.\n *\n * Applied to the background layer (media, gradient) throughout the entire scene.\n * Continuous motion — not tied to text timing.\n *\n * Templates with usesGlobalBackgroundEffect: true consume the video-level default.\n *\n * 9 effects, each with a distinct mood:\n * - static:        No motion — intentional stillness\n * - slow-zoom-in:  Draw attention inward, build focus\n * - slow-zoom-out: Reveal, establish, pull back\n * - ken-burns:     Classic cinematic photo motion (zoom + pan, direction alternates per scene)\n * - drift:         Gentle lateral pan (direction alternates per scene)\n * - pulse:         Beat-synced energy\n * - breathe:       Ambient dreamy float\n * - slow-tilt:     Tension, Dutch angle drift\n * - camera-shake:  Intensity, handheld drama\n */\n\nimport { interpolate } from \"../motion\";\n\nexport interface BackgroundTransform {\n  transform: string;\n  transformOrigin: string;\n}\n\nexport const BACKGROUND_EFFECTS = [\n  \"static\",\n  \"slow-zoom-in\",\n  \"slow-zoom-out\",\n  \"ken-burns\",\n  \"drift\",\n  \"pulse\",\n  \"breathe\",\n  \"slow-tilt\",\n  \"camera-shake\",\n] as const;\n\ntype BackgroundEffectName = (typeof BACKGROUND_EFFECTS)[number];\n\n// ─── Directional variant helpers ────────────────────────────────\n// Ken Burns and Drift have internal direction variants that alternate\n// by scene index to create visual variety across a video.\n\ntype Direction = \"right\" | \"left\" | \"up\" | \"down\";\nconst DIRECTIONS: Direction[] = [\"right\", \"left\", \"up\", \"down\"];\n\nfunction getKenBurnsTransform(progress: number, direction: Direction): BackgroundTransform {\n  const scale = interpolate(progress, [0, 1], [1.05, 1.15]);\n  const mainAxis = direction === \"right\" || direction === \"left\";\n  const sign = direction === \"right\" || direction === \"down\" ? 1 : -1;\n  const primary = interpolate(progress, [0, 1], [-2 * sign, 2 * sign]);\n  const secondary = interpolate(progress, [0, 1], [-0.5, 0.5]);\n\n  const x = mainAxis ? primary : secondary;\n  const y = mainAxis ? secondary : primary;\n\n  return {\n    transform: `scale(${scale}) translate(${x}%, ${y}%)`,\n    transformOrigin: \"center\",\n  };\n}\n\nfunction getDriftTransform(progress: number, direction: Direction): BackgroundTransform {\n  const horizontal = direction === \"right\" || direction === \"left\";\n  const sign = direction === \"right\" || direction === \"down\" ? 1 : -1;\n  const offset = interpolate(progress, [0, 1], [-2 * sign, 2 * sign]);\n\n  return {\n    transform: horizontal\n      ? `scale(1.1) translateX(${offset}%)`\n      : `scale(1.1) translateY(${offset}%)`,\n    transformOrigin: \"center\",\n  };\n}\n\n// ─── Public API ─────────────────────────────────────────────────\n\n/**\n * Get background transform for the current progress.\n *\n * @param effect — background effect name\n * @param progress — scene progress 0→1\n * @param beatIntensity — 0→1 for beat-reactive effects\n * @param sceneIndex — used to alternate direction for ken-burns/drift\n */\nexport function getBackgroundTransform(\n  effect: string | undefined,\n  progress: number,\n  beatIntensity = 0,\n  sceneIndex = 0,\n): BackgroundTransform {\n  const dir = DIRECTIONS[sceneIndex % DIRECTIONS.length];\n\n  // Undefined falls through to slow-zoom-in. Previously the missing\n  // case landed in the `default:` branch alongside `\"static\"`, returning\n  // `transform: \"none\"` — so every video where the AI didn't set\n  // backgroundEffect (i.e. nearly all of them) rendered with zero\n  // motion while the editor UI displayed \"Slow zoom in\" as default.\n  // Now renderer and UI agree: `undefined` means motion, `\"static\"`\n  // is the explicit opt-out. Slow zoom in (scale 1.00 → 1.12, no\n  // translate) is the safest default for 2-4 s scenes — visible\n  // enough to register, gentle enough not to pull focus from text.\n  // Ken Burns adds translate that's too fast on short clips; reserve\n  // for longer photo scenes via an explicit opt-in.\n  const resolved = effect ?? \"slow-zoom-in\";\n\n  switch (resolved) {\n    case \"slow-zoom-in\":\n      return {\n        transform: `scale(${interpolate(progress, [0, 1], [1, 1.12])})`,\n        transformOrigin: \"center\",\n      };\n\n    case \"slow-zoom-out\":\n      return {\n        transform: `scale(${interpolate(progress, [0, 1], [1.12, 1])})`,\n        transformOrigin: \"center\",\n      };\n\n    case \"ken-burns\":\n      return getKenBurnsTransform(progress, dir);\n\n    case \"drift\":\n      return getDriftTransform(progress, dir);\n\n    case \"pulse\": {\n      // Subtle scale pulse synced to beat\n      const base = 1 + beatIntensity * 0.04;\n      const breathe = 1 + Math.sin(progress * Math.PI * 2) * 0.02;\n      return {\n        transform: `scale(${base * breathe})`,\n        transformOrigin: \"center\",\n      };\n    }\n\n    case \"breathe\": {\n      // Slow ambient float — gentle scale + vertical drift, not beat-synced\n      const s = 1 + Math.sin(progress * Math.PI) * 0.03;\n      const y = Math.sin(progress * Math.PI) * 0.5;\n      return {\n        transform: `scale(${s}) translateY(${y}%)`,\n        transformOrigin: \"center\",\n      };\n    }\n\n    case \"slow-tilt\": {\n      // Subtle Dutch angle drift\n      const angle = interpolate(progress, [0, 1], [0, 2.5]);\n      return {\n        transform: `scale(1.08) rotate(${angle}deg)`,\n        transformOrigin: \"center\",\n      };\n    }\n\n    case \"camera-shake\": {\n      // Rapid small offsets for handheld/dramatic feel\n      // Deterministic noise based on progress for reproducibility\n      const t = progress * 40;\n      const sx = Math.sin(t * 2.3) * 0.4 + Math.sin(t * 5.7) * 0.2;\n      const sy = Math.cos(t * 3.1) * 0.3 + Math.cos(t * 4.9) * 0.15;\n      const sr = Math.sin(t * 1.7) * 0.3;\n      return {\n        transform: `scale(1.05) translate(${sx}%, ${sy}%) rotate(${sr}deg)`,\n        transformOrigin: \"center\",\n      };\n    }\n\n    case \"static\":\n    default:\n      return {\n        transform: \"none\",\n        transformOrigin: \"center\",\n      };\n  }\n}\n"
    },
    {
      "path": "src/lib/scene-templates/color-utils.ts",
      "type": "registry:lib",
      "target": "vanillasky/scene-templates/color-utils.ts",
      "content": "/**\n * Color utilities for scene templates.\n *\n * Provides safe color manipulation that works with hex colors\n * and graceful fallbacks for non-hex inputs.\n */\nimport React from \"react\";\nimport { resolveTokens, darken, orderDarkToLight, type BackgroundFamily } from \"../theme\";\n\n// ─── Layout constants ───────────────────────────────────────\n//\n// Shared vertical layout for templates that place a TextOverlay at the top of\n// the scene (showcase-*, chart-*, infographic-*). Using a single constant\n// guarantees the headline sits in the same place across every template, so\n// scene-to-scene cuts don't visually jump.\n//\n// TextOverlay vertically centers itself inside its container, so the visual\n// midline of the headline = TOP_TEXT_AREA_RATIO / 2 of the scene height.\nexport const TOP_TEXT_AREA_RATIO = 0.32;\n\n/**\n * Gradient background — clean 2-color linear gradient with a subtle\n * animated breathing overlay. Matches the look of a plain CSS\n * `linear-gradient(90deg, colorA, colorB)` so palette chips preview\n * exactly what templates render.\n *\n * The animation is deliberately minimal: two low-opacity radial blobs\n * of the same two colors drift slowly, adding just enough motion for\n * video without introducing a third \"midtone\" color that muddies\n * the gradient (a common source of unwanted purple/brown tints).\n */\nexport function gradientBackground({\n  colorA,\n  colorB,\n  solidBg,\n  progress,\n  sceneDuration,\n  seed,\n  family,\n}: {\n  colorA: string;\n  colorB: string;\n  /**\n   * When set, the function returns this color as a solid background\n   * (no gradient, no animated radial overlays). Used when the user has\n   * a scraped brand whose darkest color reads as a real surface — the\n   * accent stays on text/CTAs, but the bg goes flat instead of pairing\n   * with an unrelated brand color and producing a noisy gradient.\n   */\n  solidBg?: string;\n  progress: number;\n  sceneDuration?: number;\n  seed: number;\n  /**\n   * Background family from the style preset. Omitted = \"mesh\", the original\n   * look — so existing configs are untouched. All families are pure CSS\n   * background strings (no `filter`, which the SVG export can't rasterize)\n   * and stay deterministic in `progress`.\n   */\n  family?: BackgroundFamily;\n}): string {\n  if (solidBg) return solidBg;\n  const elapsed = progress * (sceneDuration || 3);\n  const t = elapsed * 0.6;\n\n  const seededRandom = (s: number): number => {\n    const x = Math.sin(s * 12.9898 + s * 78.233) * 43758.5453;\n    return x - Math.floor(x);\n  };\n  const phase = seededRandom(seed) * Math.PI * 2;\n\n  // Darker of the two colors anchors the top of the frame so text and logos\n  // sit over the stronger value. 180deg = top→bottom.\n  const [topColor, bottomColor] = orderDarkToLight(colorA, colorB);\n\n  if (family === \"wash\") {\n    // No radials: one calm vertical ramp that drifts a few percent over the\n    // scene. Reads considered rather than energetic.\n    const shift = Math.sin(t * 0.5 + phase) * 6;\n    return `linear-gradient(${175 + shift}deg, ${topColor} 0%, ${bottomColor} 100%)`;\n  }\n\n  if (family === \"spotlight\") {\n    // A single hard pool of accent high in the frame over a near-black\n    // surround — high contrast, very little colour area.\n    const x = 50 + Math.sin(t * 0.4 + phase) * 8;\n    const y = 28 + Math.cos(t * 0.3 + phase) * 5;\n    return `\n      radial-gradient(ellipse 70% 55% at ${x}% ${y}%, ${colorA}66 0%, ${colorA}1a 45%, transparent 72%),\n      linear-gradient(180deg, ${darken(topColor, 0.55)} 0%, ${darken(bottomColor, 0.7)} 100%)\n    `;\n  }\n\n  const x1 = 25 + Math.sin(t + phase) * 25;\n  const y1 = 30 + Math.cos(t * 0.7 + phase + 1.0) * 25;\n  const x2 = 75 + Math.sin(t * 0.6 + phase + 2.0) * 25;\n  const y2 = 70 + Math.cos(t * 0.4 + phase + 3.5) * 25;\n\n  return `\n    radial-gradient(ellipse 60% 60% at ${x1}% ${y1}%, ${colorA}4d 0%, transparent 65%),\n    radial-gradient(ellipse 60% 60% at ${x2}% ${y2}%, ${colorB}4d 0%, transparent 65%),\n    linear-gradient(180deg, ${topColor} 0%, ${bottomColor} 100%)\n  `;\n}\n\n/**\n * Animated brand-gradient overlay. Drop as the first child inside a template's\n * outer container. Uses accent + secondary (or hue-shifted accent) as gradient\n * colors. When the user is in solid mode (accent = secondary = bg), the\n * gradient collapses to a flat layer — no visual effect beyond the solid bg.\n */\nexport const BrandGradientOverlay: React.FC<{\n  style: Parameters<typeof resolveTokens>[0];\n  progress: number;\n  sceneDuration?: number;\n  seed: number;\n}> = ({ style: globalStyle, progress, sceneDuration, seed }) => {\n  const tokens = resolveTokens(globalStyle);\n  const accent = tokens.accent;\n  // tokens.secondary carries the \"auto\" guard: an explicit kit secondary\n  // passes through verbatim, but the literal string \"auto\" (the kit's\n  // derive-for-me sentinel) resolves to shiftHue(accent, 50) instead of\n  // leaking into the CSS gradient as an invalid color.\n  const secondary = tokens.secondary;\n  const solidBg = tokens.bg;\n\n  return React.createElement(\"div\", {\n    style: {\n      position: \"absolute\" as const,\n      inset: 0,\n      background: gradientBackground({ colorA: accent, colorB: secondary, solidBg, progress, sceneDuration, seed, family: tokens.preset.background }),\n      pointerEvents: \"none\" as const,\n    },\n  });\n};\n"
    }
  ],
  "meta": {
    "vanillasky": {
      "layer": "lib",
      "tier": "free",
      "domain": "appearance",
      "level": "composed",
      "audiences": [
        "custom-scenes",
        "react-developers"
      ],
      "dependencies": [
        "theme",
        "motion"
      ],
      "useWhen": "Use gradientBackground to paint the scene layer and getBackgroundTransform to add deterministic camera movement across the full scene duration.",
      "avoidWhen": "Do not apply these transforms to text or interactive chrome, and do not use CSS filter effects in the export path."
    }
  }
}
