{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "release-teaser",
  "title": "Release Teaser",
  "description": "A 16-second cinematic release teaser with center-out typography, a procedural dimensional mark, moving edge light, and an independent fictional identity.",
  "dependencies": ["remotion", "@fontsource/inter"],
  "files": [
    {
      "path": "registry/remocn-templates/release-teaser/index.tsx",
      "content": "\"use client\";\n\nimport \"@fontsource/inter/500.css\";\nimport \"@fontsource/inter/600.css\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Audio,\n  Sequence,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { TeaserBackground } from \"./background\";\nimport { type ReleaseTeaserProps, resolveReleaseTeaserProps } from \"./content\";\nimport {\n  atFps,\n  RELEASE_TEASER_FPS,\n  RELEASE_TEASER_FRAMES,\n  RELEASE_TEASER_HEIGHT,\n  RELEASE_TEASER_WIDTH,\n} from \"./motion\";\nimport { ReleaseLockup } from \"./scenes/closing\";\nimport { Statement } from \"./scenes/statement\";\n\nexport type { ReleaseTeaserProps, ReleaseTeaserTheme } from \"./content\";\nexport { releaseTeaserStatements, releaseTeaserTheme } from \"./content\";\nexport {\n  RELEASE_TEASER_FPS,\n  RELEASE_TEASER_FRAMES,\n  RELEASE_TEASER_HEIGHT,\n  RELEASE_TEASER_WIDTH,\n  releaseTeaserTimeline,\n} from \"./motion\";\n\nexport function ReleaseTeaser(props: ReleaseTeaserProps) {\n  const currentFrame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const frame = (currentFrame / fps) * RELEASE_TEASER_FPS;\n  const {\n    brandName,\n    release,\n    tagline,\n    statements,\n    accentColor,\n    theme,\n    logoSrc,\n    lightIntensity,\n    reducedMotion,\n  } = props;\n  const scene = useMemo(\n    () =>\n      resolveReleaseTeaserProps({\n        brandName,\n        release,\n        tagline,\n        statements,\n        accentColor,\n        theme,\n        logoSrc,\n        lightIntensity,\n        reducedMotion,\n      }),\n    [\n      brandName,\n      release,\n      tagline,\n      statements,\n      accentColor,\n      theme,\n      logoSrc,\n      lightIntensity,\n      reducedMotion,\n    ],\n  );\n  const scale = Math.min(width / 960, height / 540);\n  return (\n    <AbsoluteFill\n      style={{\n        background: scene.theme.background,\n        overflow: \"hidden\",\n        fontFamily: '\"Inter\", sans-serif',\n        WebkitFontSmoothing: \"antialiased\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          left: (width - 960 * scale) / 2,\n          top: (height - 540 * scale) / 2,\n          width: 960,\n          height: 540,\n          scale,\n          transformOrigin: \"0 0\",\n          overflow: \"hidden\",\n        }}\n      >\n        <TeaserBackground scene={scene} frame={frame} />\n        <Sequence\n          name=\"An idea\"\n          from={0}\n          durationInFrames={atFps(104, fps)}\n          layout=\"none\"\n        >\n          <Statement\n            scene={scene}\n            text={scene.statements[0]}\n            frame={frame}\n            duration={104}\n          />\n        </Sequence>\n        <Sequence\n          name=\"Better work\"\n          from={atFps(104, fps)}\n          durationInFrames={atFps(208, fps) - atFps(104, fps)}\n          layout=\"none\"\n        >\n          <Statement\n            scene={scene}\n            text={scene.statements[1]}\n            frame={frame - 104}\n            duration={104}\n          />\n        </Sequence>\n        <Sequence\n          name=\"Every detail\"\n          from={atFps(208, fps)}\n          durationInFrames={atFps(326, fps) - atFps(208, fps)}\n          layout=\"none\"\n        >\n          <Statement\n            scene={scene}\n            text={scene.statements[2]}\n            frame={frame - 208}\n            duration={118}\n          />\n        </Sequence>\n        <Sequence\n          name=\"Together\"\n          from={atFps(326, fps)}\n          durationInFrames={atFps(446, fps) - atFps(326, fps)}\n          layout=\"none\"\n        >\n          <Statement\n            scene={scene}\n            text={scene.statements[3]}\n            frame={frame - 326}\n            duration={120}\n          />\n        </Sequence>\n        <Sequence\n          name=\"A fresh chapter\"\n          from={atFps(446, fps)}\n          durationInFrames={atFps(610, fps) - atFps(446, fps)}\n          layout=\"none\"\n        >\n          <Statement\n            scene={scene}\n            text={scene.statements[4]}\n            frame={frame - 446}\n            duration={164}\n          />\n        </Sequence>\n        <Sequence\n          name=\"Release reveal\"\n          from={atFps(610, fps)}\n          durationInFrames={atFps(960, fps) - atFps(610, fps)}\n          layout=\"none\"\n        >\n          <ReleaseLockup scene={scene} frame={frame - 610} />\n        </Sequence>\n      </div>\n      {props.audioSrc?.trim() ? (\n        <Audio\n          src={props.audioSrc.trim()}\n          volume={\n            Number.isFinite(props.volume)\n              ? Math.max(0, Math.min(1, props.volume ?? 1))\n              : 1\n          }\n        />\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n\nexport const releaseTeaserConfig = {\n  componentName: \"ReleaseTeaser\",\n  importPath: \"@/components/remocn/templates/release-teaser\",\n  controls: {\n    brandName: {\n      type: \"text-content\" as const,\n      default: \"Orvio\",\n      description: \"Name in the final release lockup\",\n    },\n    release: {\n      type: \"text-content\" as const,\n      default: \"2\",\n      description: \"Version suffix; leave empty to hide\",\n    },\n    tagline: {\n      type: \"text-content\" as const,\n      default: \"Your next chapter starts here.\",\n      description: \"Final supporting line\",\n    },\n    accentColor: {\n      type: \"color\" as const,\n      default: \"#9BC8DD\",\n      description: \"Mark and dimensional edge lighting\",\n    },\n    reducedMotion: {\n      type: \"boolean\" as const,\n      default: false,\n      description:\n        \"Static mark and complete statements, without camera motion or blur\",\n    },\n  },\n  durationInFrames: RELEASE_TEASER_FRAMES,\n  fps: RELEASE_TEASER_FPS,\n  compositionWidth: RELEASE_TEASER_WIDTH,\n  compositionHeight: RELEASE_TEASER_HEIGHT,\n};\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/index.tsx"
    },
    {
      "path": "registry/remocn-templates/release-teaser/content.ts",
      "content": "export interface ReleaseTeaserTheme {\n  background: string;\n  surface: string;\n  foreground: string;\n  muted: string;\n  accent: string;\n}\n\nexport interface ReleaseTeaserProps {\n  brandName?: string;\n  release?: string;\n  tagline?: string;\n  statements?: string[];\n  accentColor?: string;\n  theme?: Partial<ReleaseTeaserTheme>;\n  logoSrc?: string;\n  lightIntensity?: number;\n  reducedMotion?: boolean;\n  audioSrc?: string;\n  volume?: number;\n}\n\nexport const releaseTeaserTheme: ReleaseTeaserTheme = {\n  background: \"#050709\",\n  surface: \"#151C22\",\n  foreground: \"#F2F5F7\",\n  muted: \"#A4ADB5\",\n  accent: \"#9BC8DD\",\n};\n\nexport const releaseTeaserStatements = [\n  \"Start with an idea\",\n  \"Make room\\nfor better work\",\n  \"Bring every detail to life\",\n  \"Move together.\\nGo further.\",\n  \"A fresh chapter.\\nNo starting over.\",\n] as const;\n\nconst nonempty = (value: string | undefined, fallback: string) =>\n  value?.trim() || fallback;\n\nexport function resolveReleaseTeaserProps(props: ReleaseTeaserProps = {}) {\n  return {\n    brandName: nonempty(props.brandName, \"Orvio\"),\n    // An explicit empty release intentionally hides the version suffix.\n    release: props.release === undefined ? \"2\" : props.release.trim(),\n    tagline: nonempty(props.tagline, \"Your next chapter starts here.\"),\n    statements: releaseTeaserStatements.map((fallback, i) =>\n      nonempty(props.statements?.[i], fallback),\n    ),\n    theme: {\n      background: nonempty(\n        props.theme?.background,\n        releaseTeaserTheme.background,\n      ),\n      surface: nonempty(props.theme?.surface, releaseTeaserTheme.surface),\n      foreground: nonempty(\n        props.theme?.foreground,\n        releaseTeaserTheme.foreground,\n      ),\n      muted: nonempty(props.theme?.muted, releaseTeaserTheme.muted),\n      accent: nonempty(\n        props.accentColor,\n        nonempty(props.theme?.accent, releaseTeaserTheme.accent),\n      ),\n    },\n    logoSrc: props.logoSrc?.trim() || undefined,\n    lightIntensity: Number.isFinite(props.lightIntensity)\n      ? Math.max(0, Math.min(2, props.lightIntensity ?? 1))\n      : 1,\n    reducedMotion: props.reducedMotion ?? false,\n  };\n}\n\nexport type ReleaseTeaserScene = ReturnType<typeof resolveReleaseTeaserProps>;\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/content.ts"
    },
    {
      "path": "registry/remocn-templates/release-teaser/motion.ts",
      "content": "import { Easing, interpolate } from \"remotion\";\n\nexport const RELEASE_TEASER_FPS = 60000 / 1001;\nexport const RELEASE_TEASER_FRAMES = 960;\nexport const RELEASE_TEASER_WIDTH = 1920;\nexport const RELEASE_TEASER_HEIGHT = 1080;\n\n// Half-open intervals on a doubled NTSC picture clock (16.016 seconds).\nexport const releaseTeaserTimeline = [\n  { name: \"An idea\", from: 0, to: 104 },\n  { name: \"Better work\", from: 104, to: 208 },\n  { name: \"Every detail\", from: 208, to: 326 },\n  { name: \"Together\", from: 326, to: 446 },\n  { name: \"A fresh chapter\", from: 446, to: 610 },\n  { name: \"Release reveal\", from: 610, to: 960 },\n] as const;\n\nexport const atFps = (frame: number, fps: number) =>\n  Math.round((frame / RELEASE_TEASER_FPS) * fps);\nexport const clamp = (value: number) => Math.max(0, Math.min(1, value));\nexport const progress = (frame: number, from: number, to: number) =>\n  clamp((frame - from) / (to - from));\nexport const ease = (frame: number, from: number, to: number) =>\n  interpolate(frame, [from, to], [0, 1], {\n    easing: Easing.bezier(0.5, 0, 0.2, 1),\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\nconst segmenter = new Intl.Segmenter(\"en\", { granularity: \"grapheme\" });\nexport const graphemes = (text: string) =>\n  Array.from(segmenter.segment(text), (part) => part.segment);\n\nexport function textUnits(text: string) {\n  return graphemes(text).reduce((sum, char) => {\n    if (/\\s/.test(char)) return sum + 0.3;\n    if (/[ilI.,'!|]/.test(char)) return sum + 0.29;\n    if (/[MW@%]/.test(char)) return sum + 0.98;\n    return sum + ((char.codePointAt(0) ?? 0) > 0x024f ? 1.02 : 0.63);\n  }, 0);\n}\n\n/** Honor explicit lines; keep arbitrary overrides within the safe area. */\nexport function fitText(text: string, maxSize = 48, width = 808) {\n  const lines = text.split(\"\\n\");\n  return Math.min(\n    maxSize,\n    width / Math.max(1, ...lines.map(textUnits)),\n    220 / (Math.max(1, lines.length) * 1.15),\n  );\n}\n\n/** Center-out focus, with a bounded stagger independent of copy length. */\nexport function characterState(\n  frame: number,\n  end: number,\n  index: number,\n  count: number,\n) {\n  const distance =\n    count <= 1 ? 0 : Math.abs(index - (count - 1) / 2) / ((count - 1) / 2);\n  const delay = distance * 7;\n  const enter = ease(frame, delay, delay + 18);\n  const exit = ease(frame, end - 16 - distance * 5, end - 1 - distance * 5);\n  return {\n    opacity: enter * (1 - exit),\n    blur: (1 - enter) * 5 + exit * 3,\n    y: (1 - enter) * 2 - exit * 1.5,\n  };\n}\n\nexport function ringPose(frame: number, reducedMotion = false) {\n  const f = reducedMotion ? 760 : Math.max(0, Math.min(959, frame));\n  const travel = progress(f, 0, 610);\n  const reveal = ease(f, 602, 702);\n  return {\n    x: 480 + (1 - reveal) * (18 - travel * 26),\n    y: (-200 + travel * 100) * (1 - reveal) + 270 * reveal,\n    scale: (2.78 - travel * 0.32) * (1 - reveal) + 1.08 * reveal,\n    rx: (0.4 - travel * 0.1) * (1 - reveal) + 0.14 * reveal,\n    ry: (-0.55 + travel * 0.12) * (1 - reveal) - 0.16 * reveal,\n    rz: (-0.42 + travel * 0.22) * (1 - reveal) - 0.13 * reveal,\n    lightAngle: -0.8 + (f / 960) * 1.1,\n    opacity: reducedMotion ? 0.72 : ease(f, 0, 48) * (1 - ease(f, 858, 946)),\n  };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/motion.ts"
    },
    {
      "path": "registry/remocn-templates/release-teaser/geometry.ts",
      "content": "export type Vec3 = [number, number, number];\nexport interface RingFace {\n  vertices: Vec3[];\n  normal: Vec3;\n  center: Vec3;\n  bevel: boolean;\n  edgeNormals?: [Vec3, Vec3];\n}\n\nconst normalize = (v: Vec3): Vec3 => {\n  const length = Math.hypot(...v) || 1;\n  return [v[0] / length, v[1] / length, v[2] / length];\n};\nconst subtract = (a: Vec3, b: Vec3): Vec3 => [\n  a[0] - b[0],\n  a[1] - b[1],\n  a[2] - b[2],\n];\nconst cross = (a: Vec3, b: Vec3): Vec3 => [\n  a[1] * b[2] - a[2] * b[1],\n  a[2] * b[0] - a[0] * b[2],\n  a[0] * b[1] - a[1] * b[0],\n];\nexport const dot = (a: Vec3, b: Vec3) =>\n  a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n\nfunction face(vertices: Vec3[], bevel: boolean): RingFace {\n  return {\n    vertices,\n    normal: normalize(\n      cross(\n        subtract(vertices[1], vertices[0]),\n        subtract(vertices[2], vertices[0]),\n      ),\n    ),\n    center: vertices.reduce<Vec3>(\n      (s, p) => [\n        s[0] + p[0] / vertices.length,\n        s[1] + p[1] / vertices.length,\n        s[2] + p[2] / vertices.length,\n      ],\n      [0, 0, 0],\n    ),\n    bevel,\n  };\n}\n\n/** A solid 320-degree annulus with a 40-degree opening and eight chamfered edges. */\nexport function createRing(segments = 96): RingFace[] {\n  const count = Math.max(12, Math.round(segments));\n  // Counter-clockwise cross-section: radius, depth. Front faces point toward +Z.\n  const profile = [\n    [144, 32],\n    [210, 32],\n    [220, 22],\n    [220, -22],\n    [210, -32],\n    [144, -32],\n    [134, -22],\n    [134, 22],\n  ];\n  const section = (i: number): Vec3[] => {\n    const angle = ((-18 + (i / count) * 320) * Math.PI) / 180;\n    return profile.map(([r, z]) => [\n      r * Math.cos(angle),\n      r * Math.sin(angle),\n      z,\n    ]);\n  };\n  const faces: RingFace[] = [];\n  for (let i = 0; i < count; i++) {\n    const a = section(i);\n    const b = section(i + 1);\n    for (let j = 0; j < profile.length; j++) {\n      const next = (j + 1) % profile.length;\n      const quad = face([a[j], a[next], b[next], b[j]], j % 2 === 1);\n      const halfStep = ((160 / count) * Math.PI) / 180;\n      // Continuous circumferential normals preserve the flat chamfers while\n      // allowing the renderer to shade each strip without visible tessellation.\n      quad.edgeNormals = [\n        rotate(quad.normal, 0, 0, -halfStep),\n        rotate(quad.normal, 0, 0, halfStep),\n      ];\n      faces.push(quad);\n    }\n  }\n  faces.push(face(section(0).reverse(), false), face(section(count), false));\n  return faces;\n}\n\nexport function rotate(v: Vec3, rx: number, ry: number, rz: number): Vec3 {\n  const y = v[1] * Math.cos(rx) - v[2] * Math.sin(rx);\n  const z = v[1] * Math.sin(rx) + v[2] * Math.cos(rx);\n  const x = v[0] * Math.cos(ry) + z * Math.sin(ry);\n  const depth = -v[0] * Math.sin(ry) + z * Math.cos(ry);\n  return [\n    x * Math.cos(rz) - y * Math.sin(rz),\n    x * Math.sin(rz) + y * Math.cos(rz),\n    depth,\n  ];\n}\n\n/** Compute trigonometry once for a whole frame, not for every mesh vertex. */\nexport function createRotator(rx: number, ry: number, rz: number) {\n  const x = rotate([1, 0, 0], rx, ry, rz);\n  const y = rotate([0, 1, 0], rx, ry, rz);\n  const z = rotate([0, 0, 1], rx, ry, rz);\n  return (v: Vec3): Vec3 => [\n    x[0] * v[0] + y[0] * v[1] + z[0] * v[2],\n    x[1] * v[0] + y[1] * v[1] + z[1] * v[2],\n    x[2] * v[0] + y[2] * v[1] + z[2] * v[2],\n  ];\n}\n\nexport function project(\n  v: Vec3,\n  scale: number,\n  x: number,\n  y: number,\n): [number, number] {\n  const perspective = 1100 / Math.max(300, 1100 - v[2] * scale);\n  return [x + v[0] * scale * perspective, y + v[1] * scale * perspective];\n}\n\nexport { normalize };\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/geometry.ts"
    },
    {
      "path": "registry/remocn-templates/release-teaser/background.tsx",
      "content": "import { useLayoutEffect, useRef } from \"react\";\nimport type { ReleaseTeaserScene } from \"./content\";\nimport {\n  createRing,\n  createRotator,\n  dot,\n  normalize,\n  project,\n  type Vec3,\n} from \"./geometry\";\nimport { ringPose } from \"./motion\";\nimport { BrandImage } from \"./ui\";\n\nconst ring = createRing();\n\n/** Resolve any browser CSS color to channels without bundling a color library. */\nfunction channels(ctx: CanvasRenderingContext2D, color: string): number[] {\n  ctx.clearRect(0, 0, 1, 1);\n  ctx.fillStyle = \"#000000\";\n  ctx.fillStyle = color;\n  ctx.fillRect(0, 0, 1, 1);\n  return Array.from(ctx.getImageData(0, 0, 1, 1).data).slice(0, 3);\n}\n\nfunction DimensionalRing({\n  scene,\n  frame,\n}: {\n  scene: ReleaseTeaserScene;\n  frame: number;\n}) {\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const colorCache = useRef(new Map<string, number[]>());\n  // Synchronous layout paint: a seek paints only its requested frame, with no RAF or history.\n  useLayoutEffect(() => {\n    const ctx = canvas.current?.getContext(\"2d\", { willReadFrequently: true });\n    if (!ctx) return;\n    ctx.setTransform(1, 0, 0, 1, 0, 0);\n    const colorChannels = (color: string) => {\n      const cached = colorCache.current.get(color);\n      if (cached) return cached;\n      const value = channels(ctx, color);\n      colorCache.current.set(color, value);\n      return value;\n    };\n    const accent = colorChannels(scene.theme.accent);\n    const surface = colorChannels(scene.theme.surface);\n    ctx.clearRect(0, 0, 1920, 1080);\n    ctx.scale(2, 2);\n    const pose = ringPose(frame, scene.reducedMotion);\n    const rotatePoint = createRotator(pose.rx, pose.ry, pose.rz);\n    const key = normalize([Math.cos(pose.lightAngle) * 0.95, 0.6, 0.3]);\n    const rim = normalize([-0.8, -0.5, 0.18]);\n    const half = normalize([key[0], key[1], key[2] + 1]);\n    const faces = ring\n      .map((face) => ({\n        ...face,\n        normal: rotatePoint(face.normal),\n        edgeNormals: face.edgeNormals?.map(rotatePoint),\n        center: rotatePoint(face.center),\n        vertices: face.vertices.map(rotatePoint),\n      }))\n      .sort((a, b) => a.center[2] - b.center[2]);\n    for (const face of faces) {\n      if (\n        dot(\n          face.normal,\n          normalize([\n            -face.center[0],\n            -face.center[1],\n            1100 / pose.scale - face.center[2],\n          ]),\n        ) <= 0\n      )\n        continue;\n      const shade = (normal: Vec3) => {\n        const diffuse = Math.max(0, dot(normal, key));\n        const rimLight = Math.max(0, dot(normal, rim));\n        const specular =\n          Math.max(0, dot(normal, half)) ** (face.bevel ? 26 : 48);\n        const intensity = scene.lightIntensity * pose.opacity;\n        const rgb = surface.map((c, i) =>\n          Math.min(\n            255,\n            c * 0.19 +\n              intensity *\n                (accent[i] * (diffuse ** 3 * 0.36 + rimLight ** 6 * 0.16) +\n                  180 * specular * 0.68),\n          ),\n        );\n        return `rgb(${rgb.join(\",\")})`;\n      };\n      const points = face.vertices.map((v) =>\n        project(v, pose.scale, pose.x, pose.y),\n      );\n      ctx.beginPath();\n      ctx.moveTo(...points[0]);\n      for (const p of points.slice(1)) ctx.lineTo(...p);\n      ctx.closePath();\n      if (face.edgeNormals) {\n        const gradient = ctx.createLinearGradient(\n          (points[0][0] + points[1][0]) / 2,\n          (points[0][1] + points[1][1]) / 2,\n          (points[2][0] + points[3][0]) / 2,\n          (points[2][1] + points[3][1]) / 2,\n        );\n        gradient.addColorStop(0, shade(face.edgeNormals[0]));\n        gradient.addColorStop(1, shade(face.edgeNormals[1]));\n        ctx.fillStyle = gradient;\n      } else {\n        ctx.fillStyle = shade(face.normal);\n      }\n      ctx.fill();\n      // A sub-pixel seam overlap prevents hairlines between adjoining mesh quads.\n      ctx.strokeStyle = ctx.fillStyle;\n      ctx.lineWidth = 0.45;\n      ctx.stroke();\n    }\n  }, [\n    frame,\n    scene.reducedMotion,\n    scene.lightIntensity,\n    scene.theme.accent,\n    scene.theme.surface,\n  ]);\n\n  return (\n    <canvas\n      ref={canvas}\n      width={1920}\n      height={1080}\n      tabIndex={-1}\n      aria-hidden=\"true\"\n      style={{ width: 960, height: 540, position: \"absolute\", inset: 0 }}\n    />\n  );\n}\n\nexport function TeaserBackground({\n  scene,\n  frame,\n}: {\n  scene: ReleaseTeaserScene;\n  frame: number;\n}) {\n  const pose = ringPose(frame, scene.reducedMotion);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        opacity: pose.opacity,\n      }}\n    >\n      {scene.logoSrc ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: pose.x - 220,\n            top: pose.y - 220,\n            width: 440,\n            height: 440,\n            scale: pose.scale,\n            rotate: `${(pose.rz * 180) / Math.PI}deg`,\n            opacity: 0.18 * scene.lightIntensity,\n          }}\n        >\n          <BrandImage\n            src={scene.logoSrc}\n            style={{ width: \"100%\", height: \"100%\", objectFit: \"contain\" }}\n          />\n        </div>\n      ) : (\n        <DimensionalRing scene={scene} frame={frame} />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/background.tsx"
    },
    {
      "path": "registry/remocn-templates/release-teaser/ui.tsx",
      "content": "import type { CSSProperties } from \"react\";\nimport { Img, useRemotionEnvironment } from \"remotion\";\n\nexport function BrandImage({\n  src,\n  style,\n}: {\n  src: string;\n  style: CSSProperties;\n}) {\n  const { isStudio, isRendering } = useRemotionEnvironment();\n  if (isStudio && !isRendering) {\n    // biome-ignore lint/performance/noImgElement: Studio-only media registration workaround; exports and embedded players retain Img's loading gate.\n    return <img src={src} alt=\"\" style={style} />;\n  }\n  return <Img src={src} style={style} />;\n}\n\n/** Original open-orbit mark; the opening matches the dimensional background. */\nexport function OrbitMark({\n  color,\n  size = 30,\n}: {\n  color: string;\n  size?: number;\n}) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"-240 -240 480 480\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M209.23 -67.98 A220 220 0 1 1 116.58 -186.57 L71.01 -113.64 A134 134 0 1 0 127.44 -41.41 Z\"\n        fill={color}\n      />\n    </svg>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/ui.tsx"
    },
    {
      "path": "registry/remocn-templates/release-teaser/scenes/statement.tsx",
      "content": "import { Interactive, interpolate } from \"remotion\";\nimport type { ReleaseTeaserScene } from \"../content\";\nimport { characterState, fitText, graphemes } from \"../motion\";\n\nexport function Statement({\n  scene,\n  text,\n  frame,\n  duration,\n}: {\n  scene: ReleaseTeaserScene;\n  text: string;\n  frame: number;\n  duration: number;\n}) {\n  return (\n    <Interactive.Div\n      name=\"Statement\"\n      role=\"img\"\n      aria-label={text}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"center\",\n        alignItems: \"center\",\n        fontSize: fitText(text),\n        fontWeight: 500,\n        lineHeight: 1.13,\n        color: scene.theme.foreground,\n        scale: scene.reducedMotion\n          ? 1\n          : interpolate(frame, [0, duration], [0.99, 1.025], {\n              extrapolateLeft: \"clamp\",\n              extrapolateRight: \"clamp\",\n            }),\n      }}\n    >\n      {text.split(\"\\n\").map((line, lineIndex) => {\n        const chars = graphemes(line);\n        return (\n          <div\n            key={`${lineIndex}-${line}`}\n            style={{\n              whiteSpace: \"pre\",\n              textAlign: \"center\",\n              minHeight: \"1.13em\",\n            }}\n          >\n            {scene.reducedMotion\n              ? line\n              : chars.map((char, index) => {\n                  const state = characterState(\n                    frame,\n                    duration,\n                    index,\n                    chars.length,\n                  );\n                  return (\n                    <span\n                      key={`${index}-${char}`}\n                      style={{\n                        display: \"inline-block\",\n                        opacity: state.opacity,\n                        filter:\n                          state.blur > 0.08\n                            ? `blur(${state.blur}px)`\n                            : undefined,\n                        translate: `0 ${state.y}px`,\n                      }}\n                    >\n                      {char}\n                    </span>\n                  );\n                })}\n          </div>\n        );\n      })}\n    </Interactive.Div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/scenes/statement.tsx"
    },
    {
      "path": "registry/remocn-templates/release-teaser/scenes/closing.tsx",
      "content": "import { Interactive, interpolate } from \"remotion\";\nimport type { ReleaseTeaserScene } from \"../content\";\nimport { ease, fitText } from \"../motion\";\nimport { BrandImage, OrbitMark } from \"../ui\";\n\nexport function ReleaseLockup({\n  scene,\n  frame,\n}: {\n  scene: ReleaseTeaserScene;\n  frame: number;\n}) {\n  const title = `${scene.brandName}${scene.release ? ` ${scene.release}` : \"\"}`;\n  const reveal = scene.reducedMotion ? 1 : ease(frame, 0, 32);\n  return (\n    <Interactive.Div\n      name=\"Release lockup\"\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        opacity: reveal,\n        filter:\n          !scene.reducedMotion && reveal < 0.99\n            ? `blur(${(1 - reveal) * 6}px)`\n            : undefined,\n        scale: scene.reducedMotion\n          ? 1\n          : interpolate(frame, [0, 350], [0.97, 1.03], {\n              extrapolateLeft: \"clamp\",\n              extrapolateRight: \"clamp\",\n            }),\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 16,\n          marginBottom: 16,\n        }}\n      >\n        <div style={{ width: 34, height: 34, flexShrink: 0 }}>\n          {scene.logoSrc ? (\n            <BrandImage\n              src={scene.logoSrc}\n              style={{ width: 34, height: 34, objectFit: \"contain\" }}\n            />\n          ) : (\n            <OrbitMark size={34} color={scene.theme.accent} />\n          )}\n        </div>\n        <Interactive.Div\n          name=\"Brand and release\"\n          style={{\n            fontSize: fitText(title, 70, 750),\n            fontWeight: 600,\n            lineHeight: 1,\n            color: scene.theme.foreground,\n            whiteSpace: \"pre\",\n            textAlign: \"center\",\n          }}\n        >\n          {title}\n        </Interactive.Div>\n      </div>\n      <Interactive.Div\n        name=\"Closing line\"\n        style={{\n          fontSize: fitText(scene.tagline, 20, 740),\n          lineHeight: 1.3,\n          color: scene.theme.muted,\n          whiteSpace: \"pre\",\n          textAlign: \"center\",\n          opacity: scene.reducedMotion ? 1 : ease(frame, 8, 40),\n        }}\n      >\n        {scene.tagline}\n      </Interactive.Div>\n    </Interactive.Div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/templates/release-teaser/scenes/closing.tsx"
    }
  ],
  "type": "registry:component"
}
