{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ring-burst",
  "title": "Ring Burst",
  "description": "A glowing ring bursts outward from a point and reveals the next scene.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/ring-burst/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/Fraunces\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: serif } = loadSerif(\"italic\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** Full logo as on remocn.dev: R mark (0.76em, on the baseline) + \"emocn\" in Manrope 600. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nconst isLogo = (s: string) => /^remocn\\.?$/i.test(s.trim());\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ntype Tone = \"dark\" | \"light\";\n\nconst TONES: Record<Tone, { bg: string; fg: string }> = {\n  dark: { bg: \"#0A0A0B\", fg: \"#F3F2EF\" },\n  light: { bg: \"#F5F4F0\", fg: \"#121214\" },\n};\n\n/* gradients.sh \"halo\" palette: a thick soft ring around the landed logo. */\nconst HALO = {\n  dark: [\"#fff4ec\", \"#ffb36b\", \"#e2457a\", \"#3d4dff\"],\n  light: [\"#ff9a4d\", \"#e2457a\", \"#3d4dff\", \"#3d4dff\"],\n} as const;\n\nconst mixA = (c: string, a: number) =>\n  `color-mix(in oklab, ${c} ${Math.round(a * 100)}%, transparent)`;\n\nfunction haloGround(tone: Tone, r: number) {\n  const [c0, c1, c2, c3] = HALO[tone];\n  if (tone === \"light\") {\n    return `radial-gradient(circle ${r.toFixed(1)}px at 50% 50%, transparent 0%, transparent 75.5%, ${mixA(c0, 0.92)} 77%, ${mixA(c1, 0.6)} 82%, ${mixA(c2, 0.22)} 90%, transparent 100%)`;\n  }\n  return `radial-gradient(circle ${r.toFixed(1)}px at 50% 50%, transparent 0%, transparent 74%, ${mixA(c0, 0.85)} 76.5%, ${mixA(c1, 0.62)} 81%, ${mixA(c2, 0.36)} 88%, ${mixA(c3, 0.16)} 94%, transparent 100%)`;\n}\n\nfunction Headline({\n  text,\n  tone,\n  accent,\n  mark,\n}: {\n  text: string;\n  tone: Tone;\n  accent: string;\n  mark: boolean;\n}) {\n  const { width: W, height: H } = useVideoConfig();\n  const u = H / 720;\n  const c = TONES[tone];\n  const logo = mark && isLogo(text);\n  const dot = mark && !logo && text.endsWith(\".\");\n  const body = dot ? text.slice(0, -1) : text;\n  const size = Math.min(\n    196 * u,\n    (0.72 * W) / (Math.max(4, text.length) * 0.48),\n  );\n  return (\n    <AbsoluteFill\n      style={{\n        color: c.fg,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      {logo ? (\n        <RemocnLogo\n          fontSize={Math.min(116 * u, (0.36 * W) / 3.9)}\n          color={c.fg}\n        />\n      ) : (\n        <div\n          style={{\n            fontFamily: serif,\n            fontStyle: \"italic\",\n            fontWeight: 400,\n            fontSize: size,\n            letterSpacing: \"-0.03em\",\n            lineHeight: 1,\n            whiteSpace: \"nowrap\",\n            paddingBottom: size * 0.1,\n            paddingRight: size * 0.06,\n          }}\n        >\n          {body}\n          {dot ? <span style={{ color: accent }}>.</span> : null}\n        </div>\n      )}\n    </AbsoluteFill>\n  );\n}\n\nfunction Scene({\n  content,\n  tone,\n  accent,\n  mark,\n  ground,\n}: {\n  content: React.ReactNode;\n  tone: Tone;\n  accent: string;\n  mark: boolean;\n  ground?: string;\n}) {\n  const text = typeof content === \"string\" || typeof content === \"number\";\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: TONES[tone].bg,\n        backgroundImage: ground,\n      }}\n    >\n      {text ? (\n        <Headline\n          text={String(content)}\n          tone={tone}\n          accent={accent}\n          mark={mark}\n        />\n      ) : (\n        content\n      )}\n    </AbsoluteFill>\n  );\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={11}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nexport interface RingBurstProps {\n  /** Outgoing scene: a short headline string or any React node. */\n  from?: React.ReactNode;\n  /** Incoming scene, carried in by the last ring. `\"remocn\"` sets the remocn.dev logo. */\n  to?: React.ReactNode;\n  /** Burst point as fractions of the frame, `[x, y]`. */\n  origin?: [number, number];\n  /** Ring colours, outermost first (2–3). Derived from `accent` and `theme` by default. */\n  colors?: string[];\n  /** Frames the outgoing scene holds before the burst. */\n  delay?: number;\n  accent?: string;\n  /** Playback multiplier. */\n  speed?: number;\n  /** `dark` lands on a dark scene, `light` on a light one. */\n  theme?: Tone;\n}\n\nconst GROW = 32;\nconst GAPS = [0, 4, 8, 13];\nconst EASE_BURST = Easing.bezier(0.25, 0.45, 0.12, 1);\nconst EASE_PUSH = Easing.bezier(0.45, 0, 0.55, 1);\nconst EASE_LAND = Easing.bezier(0.22, 1, 0.36, 1);\n\nfunction ringColors(theme: Tone, accent: string) {\n  return theme === \"dark\"\n    ? [accent, \"#141416\", \"#E2DFD8\"]\n    : [accent, \"#E2DFD8\", \"#141416\"];\n}\n\nexport function RingBurst({\n  from = \"Before.\",\n  to = \"remocn\",\n  origin = [0.5, 0.5],\n  colors,\n  delay = 20,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: RingBurstProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = H / 720;\n  const toneB: Tone = theme;\n  const toneA: Tone = theme === \"dark\" ? \"light\" : \"dark\";\n  const fills = (colors?.length ? colors : ringColors(theme, accent)).slice(\n    0,\n    3,\n  );\n  const gaps = [...GAPS.slice(0, fills.length), GAPS[3]];\n\n  const ox = origin[0] * W;\n  const oy = origin[1] * H;\n  const reach =\n    Math.max(\n      Math.hypot(ox, oy),\n      Math.hypot(W - ox, oy),\n      Math.hypot(ox, H - oy),\n      Math.hypot(W - ox, H - oy),\n    ) +\n    6 * u;\n\n  const progress = (i: number) =>\n    interpolate(t, [delay + gaps[i], delay + gaps[i] + GROW], [0, 1], CLAMP);\n  const radius = (i: number) => reach * EASE_BURST(progress(i));\n\n  const last = gaps.length - 1;\n  const done = progress(last) >= 1;\n  const push =\n    1 +\n    0.06 *\n      EASE_PUSH(interpolate(t, [delay, delay + GROW * 0.8], [0, 1], CLAMP));\n  const land = EASE_LAND(progress(last));\n  const settle = 1.08 - 0.08 * land;\n  // The halo keeps breathing outward a touch after the landing.\n  const drift = interpolate(\n    t,\n    [delay + GROW, delay + GROW + 90],\n    [0, 1],\n    CLAMP,\n  );\n  const halo = haloGround(\n    toneB,\n    Math.min(W, H) * (0.45 + 0.035 * land + 0.015 * drift),\n  );\n\n  const disc = (r: number, key: string, children: React.ReactNode) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left: ox - r,\n        top: oy - r,\n        width: 2 * r,\n        height: 2 * r,\n        borderRadius: \"50%\",\n        overflow: \"hidden\",\n      }}\n    >\n      {children}\n    </div>\n  );\n\n  const coveredBy = (i: number) => radius(i) >= reach - 0.5;\n  const hideA = fills.some((_, i) => coveredBy(i)) || done;\n\n  return (\n    <AbsoluteFill style={{ background: TONES[toneB].bg, overflow: \"hidden\" }}>\n      {hideA ? null : (\n        <AbsoluteFill\n          style={{\n            transform: `scale(${push})`,\n            transformOrigin: `${ox}px ${oy}px`,\n          }}\n        >\n          <Scene content={from} tone={toneA} accent={accent} mark={false} />\n        </AbsoluteFill>\n      )}\n      {fills.map((fill, i) => {\n        const r = radius(i);\n        const buried = done || coveredBy(i + 1 <= last ? i + 1 : last);\n        if (r <= 0 || buried) return null;\n        return disc(\n          r,\n          `${fill}-${i}`,\n          <div style={{ width: \"100%\", height: \"100%\", background: fill }} />,\n        );\n      })}\n      {radius(last) > 0\n        ? disc(\n            done ? reach : radius(last),\n            \"scene-b\",\n            <div\n              style={{\n                position: \"absolute\",\n                left: (done ? reach : radius(last)) - ox,\n                top: (done ? reach : radius(last)) - oy,\n                width: W,\n                height: H,\n                transform: `scale(${settle})`,\n                transformOrigin: `${ox}px ${oy}px`,\n              }}\n            >\n              <Scene\n                content={to}\n                tone={toneB}\n                accent={accent}\n                mark\n                ground={halo}\n              />\n            </div>,\n          )\n        : null}\n      <Grain opacity={theme === \"light\" ? 0.035 : 0.05} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/ring-burst.tsx"
    }
  ],
  "type": "registry:component"
}
