{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "comic-panels",
  "title": "Comic Panels",
  "description": "The frame cracks into a three-panel comic page with speed lines, red halftone and a starburst, then the action panel bursts into a full-frame POW.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/comic-panels/index.tsx",
      "content": "\"use client\";\n\nimport { type ReactNode, useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type ComicPanelsColors = {\n  ink: string;\n  paper: string;\n  accent: string;\n};\n\nexport type ComicPanelsProps = {\n  /** Hero content, framed in the top panel once the page cracks. Falls back to a placeholder \"remocn\" lockup. */\n  children?: ReactNode;\n  /** Optional word in the starburst, e.g. \"POW\". */\n  word?: string;\n  colors?: Partial<ComicPanelsColors>;\n  /** Ink color. Overrides `colors.ink` when set. */\n  ink?: string;\n  /** Paper color. Overrides `colors.paper` when set. */\n  paper?: string;\n  /** Accent color. Overrides `colors.accent` when set. */\n  accent?: string;\n  /** Scale of `children` inside the top panel. */\n  heroScale?: number;\n  speed?: number;\n};\n\nexport const comicPanelsDefaults = {\n  colors: {\n    ink: \"#111111\",\n    paper: \"#FFFFFF\",\n    accent: \"#E5322D\",\n  } as ComicPanelsColors,\n  heroScale: 0.62,\n  speed: 1,\n};\n\nexport const comicPanelsLength = 36;\n\nconst FONT =\n  \"'Arial Black', Impact, Inter, -apple-system, 'Segoe UI', sans-serif\";\nconst SANS =\n  \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Helvetica, Arial, sans-serif\";\n\n// Placeholder hero shown when no children are passed.\nfunction PlaceholderLockup({ ink, accent }: { ink: string; accent: string }) {\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 28 }}>\n      <div\n        style={{\n          width: 120,\n          height: 120,\n          borderRadius: 32,\n          backgroundColor: accent,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div\n          style={{\n            width: 36,\n            height: 36,\n            borderRadius: \"50%\",\n            backgroundColor: ink,\n          }}\n        />\n      </div>\n      <span\n        style={{\n          fontFamily: SANS,\n          fontSize: 104,\n          fontWeight: 800,\n          letterSpacing: \"-0.04em\",\n          color: ink,\n        }}\n      >\n        remocn\n      </span>\n    </div>\n  );\n}\nconst clamp = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\nconst ease = (\n  t: number,\n  from: number,\n  to: number,\n  easing: (n: number) => number = Easing.inOut(Easing.cubic),\n) => interpolate(t, [from, to], [0, 1], { ...clamp, easing });\n\ntype Pt = [number, number];\nconst poly = (points: Pt[]) => points.map((p) => p.join(\",\")).join(\" \");\n\nfunction starburst(spikes: number, outer: number, inner: number, seed: string) {\n  const points: Pt[] = [];\n  for (let i = 0; i < spikes * 2; i++) {\n    const a =\n      (i / (spikes * 2)) * Math.PI * 2 +\n      (random(`${seed}-a-${i}`) - 0.5) * 0.12;\n    const r =\n      i % 2 === 0\n        ? outer * (0.82 + 0.18 * random(`${seed}-r-${i}`))\n        : inner * (0.9 + 0.1 * random(`${seed}-r-${i}`));\n    points.push([Math.cos(a) * r, Math.sin(a) * r]);\n  }\n  return poly(points);\n}\n\nexport function ComicPanels({\n  children,\n  word,\n  colors,\n  ink,\n  paper,\n  accent,\n  heroScale = comicPanelsDefaults.heroScale,\n  speed = comicPanelsDefaults.speed,\n}: ComicPanelsProps) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, fps } = useVideoConfig();\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const merged = { ...comicPanelsDefaults.colors, ...colors };\n  const c: ComicPanelsColors = {\n    ink: ink ?? merged.ink,\n    paper: paper ?? merged.paper,\n    accent: accent ?? merged.accent,\n  };\n  const t = (frame / fps) * 30 * speed;\n  const u = Math.min(W, H) / 720;\n\n  // Page geometry: one slanted horizontal gutter, one slanted vertical one.\n  const a0 = 0.6 * H;\n  const a1 = 0.5 * H;\n  const yA = (x: number) => a0 + ((a1 - a0) * x) / W;\n  const b0: Pt = [0.53 * W, yA(0.53 * W)];\n  const b1: Pt = [0.46 * W, H];\n  const gutter = 16 * u;\n  const border = 5 * u;\n\n  const margin = ease(t, 0, 8) * 22 * u;\n  const crackA = ease(t, 2, 10, Easing.out(Easing.cubic));\n  const crackB = ease(t, 8, 14, Easing.out(Easing.cubic));\n  const slide2 = ease(t, 10, 18, Easing.out(Easing.exp));\n  const slide3 = ease(t, 13, 21, Easing.out(Easing.exp));\n  const burst = ease(t, 23, 34, Easing.in(Easing.cubic));\n  const settle = ease(t, 30, 36, Easing.out(Easing.cubic));\n  const shaking = t >= 22 && t < 31;\n  const step = Math.floor(t / 2);\n  const shake = shaking\n    ? [\n        (random(`comic-sx-${step}`) - 0.5) * 14 * u,\n        (random(`comic-sy-${step}`) - 0.5) * 14 * u,\n      ]\n    : [0, 0];\n  const jolt = `translate(${shake[0]} ${shake[1]})`;\n\n  const left: Pt[] = [[0, a0], b0, b1, [0, H]];\n  const right: Pt[] = [b0, [W, a1], [W, H], b1];\n  const heroFocus: Pt = [W / 2, interpolate(crackA, [0, 1], [H / 2, 0.27 * H])];\n  const heroS = interpolate(crackA, [0, 1], [1, heroScale]);\n\n  // Speed lines: wedges from the hero focus, redrawn every 2 frames.\n  const wedges = Array.from({ length: 56 }, (_, i) => {\n    const a =\n      (i / 56) * Math.PI * 2 + (random(`comic-w-${step}-${i}`) - 0.5) * 0.09;\n    const spread = 0.008 + 0.014 * random(`comic-ws-${step}-${i}`);\n    const r0 = (0.3 + 0.12 * random(`comic-wr-${step}-${i}`)) * W * heroS;\n    const r1 = W * 1.2;\n    const [fx, fy] = heroFocus;\n    const at = (r: number, d: number): Pt => [\n      fx + Math.cos(a + d) * r,\n      fy + Math.sin(a + d) * r * 0.62,\n    ];\n    return poly([at(r0, 0), at(r1, -spread), at(r1, spread)]);\n  });\n\n  // Halftone: dots swell toward the panel's lower-left corner.\n  const pitch = 20 * u;\n  const dots: { x: number; y: number; r: number }[] = [];\n  for (let y = a1 - pitch; y < H + pitch; y += pitch) {\n    for (let x = 0; x < b0[0] + pitch; x += pitch) {\n      const ox = (Math.round(y / pitch) % 2) * (pitch / 2);\n      const d = Math.hypot(x + ox, H - y) / Math.hypot(b0[0], H - a0);\n      const r = pitch * 0.5 * Math.max(0, Math.min(1, 1.15 - d));\n      if (r > 0.6 * u) dots.push({ x: x + ox, y, r });\n    }\n  }\n\n  const c3: Pt = [(b0[0] + W + W + b1[0]) / 4, (b0[1] + a1 + H + H) / 4];\n  const outer = 0.4 * (H - a1);\n  const pulse = 1 + 0.04 * Math.sin(t * 1.4);\n  const center: Pt = [\n    interpolate(burst, [0, 1], [c3[0], W / 2]),\n    interpolate(burst, [0, 1], [c3[1], H / 2]),\n  ];\n  const redScale = interpolate(burst, [0, 1], [1, 9]) * pulse;\n  const whiteScale =\n    interpolate(burst, [0, 1], [1, (0.4 * H) / (outer * 0.62)]) *\n    pulse *\n    interpolate(settle, [0, 1], [1.08, 1]);\n  const spin = interpolate(burst, [0, 1], [0, 14]);\n  const burstLayer = (\n    <g transform={`translate(${center[0]} ${center[1]}) rotate(${spin})`}>\n      <polygon\n        transform={`scale(${redScale})`}\n        points={starburst(16, outer, outer * 0.68, \"comic-red\")}\n        fill={c.accent}\n        stroke={c.ink}\n        strokeWidth={(border * 0.8) / redScale}\n        strokeLinejoin=\"round\"\n      />\n      <polygon\n        transform={`scale(${whiteScale})`}\n        points={starburst(13, outer * 0.62, outer * 0.42, \"comic-white\")}\n        fill={c.paper}\n        stroke={c.ink}\n        strokeWidth={(border * 0.8) / whiteScale}\n        strokeLinejoin=\"round\"\n      />\n      {word ? (\n        <g\n          transform={`scale(${whiteScale}) rotate(-8)`}\n          textAnchor=\"middle\"\n          dominantBaseline=\"central\"\n          fontFamily={FONT}\n          fontWeight={900}\n          fontSize={outer * 0.4}\n        >\n          <text x={outer * 0.03} y={outer * 0.03} fill={c.accent}>\n            {word}\n          </text>\n          <text fill={c.ink}>{word}</text>\n        </g>\n      ) : null}\n    </g>\n  );\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: c.paper, overflow: \"hidden\" }}>\n      <AbsoluteFill>\n        <svg width={W} height={H} style={{ position: \"absolute\" }} aria-hidden>\n          <defs>\n            <clipPath id={`${id}-left`}>\n              <polygon points={poly(left)} />\n            </clipPath>\n            <clipPath id={`${id}-right`}>\n              <polygon points={poly(right)} />\n            </clipPath>\n          </defs>\n          <g fill={c.ink} transform={jolt}>\n            {wedges.map((points, i) => (\n              <polygon key={i} points={points} />\n            ))}\n          </g>\n        </svg>\n        <AbsoluteFill\n          style={{\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            transform: `translate(${heroFocus[0] - W / 2 + shake[0]}px, ${heroFocus[1] - H / 2 + shake[1]}px) scale(${heroS})`,\n          }}\n        >\n          {children ?? <PlaceholderLockup ink={c.ink} accent={c.accent} />}\n        </AbsoluteFill>\n        <svg width={W} height={H} style={{ position: \"absolute\" }} aria-hidden>\n          <g\n            clipPath={`url(#${id}-left)`}\n            transform={`translate(${(slide2 - 1) * b0[0]} 0)`}\n            opacity={slide2 > 0 ? 1 : 0}\n          >\n            <polygon points={poly(left)} fill={c.paper} />\n            <g fill={c.accent} transform={jolt}>\n              {dots.map((d) => (\n                <circle key={`${d.x}-${d.y}`} cx={d.x} cy={d.y} r={d.r} />\n              ))}\n            </g>\n          </g>\n          <g clipPath={`url(#${id}-right)`}>\n            <g\n              transform={`translate(${(1 - slide3) * (W - b1[0])} 0)`}\n              opacity={slide3 > 0 ? 1 : 0}\n            >\n              <polygon points={poly(right)} fill={c.ink} />\n              <g transform={jolt}>{burst === 0 ? burstLayer : null}</g>\n            </g>\n          </g>\n          {/* Gutters: ink edges first, then the white channel on top. */}\n          {[c.ink, c.paper].map((stroke, pass) => (\n            <g\n              key={stroke + pass}\n              stroke={stroke}\n              strokeWidth={pass === 0 ? gutter + border * 2 : gutter}\n              strokeLinecap=\"butt\"\n            >\n              <line\n                x1={-gutter}\n                y1={yA(-gutter)}\n                x2={-gutter + (W + 2 * gutter) * crackA}\n                y2={yA(-gutter + (W + 2 * gutter) * crackA)}\n              />\n              <line\n                x1={b0[0]}\n                y1={b0[1]}\n                x2={b0[0] + (b1[0] + (b1[0] - b0[0]) * 0.05 - b0[0]) * crackB}\n                y2={b0[1] + (H * 1.05 - b0[1]) * crackB}\n              />\n            </g>\n          ))}\n          <path\n            d={`M ${-W} ${-H} H ${2 * W} V ${2 * H} H ${-W} Z M ${margin} ${margin} V ${H - margin} H ${W - margin} V ${margin} Z`}\n            fill={c.paper}\n          />\n          {margin > 0 ? (\n            <rect\n              x={margin}\n              y={margin}\n              width={W - margin * 2}\n              height={H - margin * 2}\n              fill=\"none\"\n              stroke={c.ink}\n              strokeWidth={border}\n            />\n          ) : null}\n        </svg>\n      </AbsoluteFill>\n      {burst > 0 ? (\n        <svg width={W} height={H} style={{ position: \"absolute\" }} aria-hidden>\n          <g transform={jolt}>{burstLayer}</g>\n        </svg>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/comic-panels.tsx"
    }
  ],
  "type": "registry:component"
}
