{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radial-burst",
  "title": "Radial Burst",
  "description": "Sculptural ribbons twist into a tilted ring and sweep outward through contour echoes for an expressive product introduction.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/radial-burst/index.tsx",
      "content": "\"use client\";\n\nimport {\n  Easing,\n  interpolateColors,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport interface RadialBurstProps {\n  /** Number of rounded segments, from 4 to 16. */\n  segments?: number;\n  /** Burst radius at a 720px reference size. */\n  radius?: number;\n  /** Segment thickness at a 720px reference size. */\n  thickness?: number;\n  /** Rotation during assembly, in degrees; negative reverses it. */\n  rotation?: number;\n  /** Sculptural stretch and tilt, from 0 (classic) to 1.5. */\n  intensity?: number;\n  /** Curvature of the flying ribbons in degrees. */\n  twist?: number;\n  /** Number of contour echoes, from 0 to 5. */\n  echoes?: number;\n  color?: string;\n  accentColor?: string;\n  backgroundColor?: string;\n  speed?: number;\n  loop?: boolean;\n  className?: string;\n}\n\n/** The ring has completely left the composition by this frame at 30 fps. */\nexport const radialBurstLength = 114;\nconst CYCLE = 120;\nconst finite = (value: number, fallback: number) =>\n  Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number) => Math.min(1, Math.max(0, value));\nconst mix = (from: number, to: number, progress: number) =>\n  from + (to - from) * progress;\nconst easeInOut = Easing.bezier(0.65, 0, 0.25, 1);\nconst easeOut = Easing.bezier(0.16, 1, 0.3, 1);\nconst progress = (frame: number, start: number, end: number) =>\n  clamp((frame - start) / (end - start));\n\n/** Includes the clean tail, in frames at 30 fps. */\nexport function getRadialBurstDuration({\n  speed = 1,\n}: Pick<RadialBurstProps, \"speed\"> = {}) {\n  const rate = finite(speed, 1);\n  return rate <= 0 ? 1 : Math.max(1, Math.ceil(CYCLE / rate));\n}\n\nexport function getRadialBurstState(\n  frame: number,\n  {\n    segments = 8,\n    radius = 210,\n    thickness = 30,\n    rotation = 135,\n    intensity = 1,\n    loop = false,\n    width = 1280,\n    height = 720,\n  }: Omit<\n    RadialBurstProps,\n    \"color\" | \"accentColor\" | \"backgroundColor\" | \"speed\" | \"className\"\n  > & {\n    width?: number;\n    height?: number;\n  } = {},\n) {\n  const w = Math.max(1, finite(width, 1280));\n  const h = Math.max(1, finite(height, 720));\n  const unit = Math.min(w, h) / 720;\n  const time = Math.max(0, finite(frame, 0));\n  const local = loop ? time % CYCLE : time;\n  const count = Math.round(Math.min(16, Math.max(4, finite(segments, 8))));\n  const burstRadius = Math.min(280, Math.max(100, finite(radius, 210))) * unit;\n  const stroke = Math.min(60, Math.max(8, finite(thickness, 30))) * unit;\n  const ringRadius = burstRadius * 0.68;\n  const assemble = easeInOut(progress(local, 44, 76));\n  const curl = easeInOut(progress(local, 48, 78));\n  const close = easeInOut(progress(local, 72, 84));\n  const anticipate = easeInOut(progress(local, 86, 96));\n  const exit = progress(local, 96, radialBurstLength) ** 2.6;\n  const launch = spring({\n    frame: Math.max(0, local - 18),\n    fps: 30,\n    durationInFrames: 26,\n    config: { damping: 13, stiffness: 170, mass: 0.8 },\n  });\n  const pop = spring({\n    frame: Math.max(0, local - 2),\n    fps: 30,\n    durationInFrames: 16,\n    config: { damping: 11, stiffness: 180, mass: 0.65 },\n  });\n  const ringStroke = mix(stroke * 0.8, stroke * 1.1, anticipate);\n  const exitStroke = mix(ringStroke, stroke * 2.2, exit);\n  // The inner edge, not just the centerline, must pass every corner.\n  const clearRadius = Math.hypot(w, h) / 2 + stroke * 2.2 + 2;\n  const orbitRadius = mix(\n    mix(28 * unit, burstRadius, launch),\n    ringRadius,\n    assemble,\n  );\n  const finalRadius = mix(ringRadius, ringRadius * 0.84, anticipate);\n  const strength = Math.min(1.5, Math.max(0, finite(intensity, 1)));\n  const energy = Math.sin(Math.PI * progress(local, 18, 84));\n  const tilt =\n    easeOut(progress(local, 18, 48)) *\n    (1 - easeInOut(progress(local, 96, 114)));\n\n  return {\n    frame: local,\n    count,\n    unit,\n    strength,\n    energy: Math.max(0, energy) * strength,\n    planeScale: 1 - tilt * 0.46 * Math.min(1.5, strength),\n    planeAngle:\n      Math.sin(progress(local, 18, 114) * Math.PI * 1.6) * 32 * strength,\n    offsetX:\n      Math.sin(Math.PI * progress(local, 18, 114)) * 65 * unit * strength,\n    offsetY:\n      -Math.sin(Math.PI * progress(local, 18, 114)) * 32 * unit * strength,\n    dotRadius: 37 * unit * pop * (1 - easeOut(progress(local, 18, 26))),\n    dotSquash: 1 - Math.sin(Math.PI * progress(local, 2, 18)) * 0.16,\n    showSegments: local >= 18 && local < 84,\n    showRing: local >= 84 && local < radialBurstLength,\n    segmentOpacity: easeOut(progress(local, 18, 23)),\n    radius: local < 84 ? orbitRadius : mix(finalRadius, clearRadius, exit),\n    strokeWidth: local < 84 ? mix(stroke, stroke * 0.8, assemble) : exitStroke,\n    segmentLength: mix(\n      12 * unit,\n      Math.min(82 * unit, burstRadius * 0.42),\n      easeOut(progress(local, 18, 36)),\n    ),\n    curl,\n    // Gaps close with a tiny overlap to prevent raster seams at the weld.\n    arcAngle: ((Math.PI * 2) / count) * mix(0.62, 1.002, close),\n    rotation: finite(rotation, 135) * easeInOut(progress(local, 34, 84)),\n  };\n}\n\n/** A straight rounded spoke curls continuously into a circular arc. */\nfunction getSegmentPoints({\n  radius,\n  segmentLength,\n  arcAngle,\n  curl,\n}: Pick<\n  ReturnType<typeof getRadialBurstState>,\n  \"radius\" | \"segmentLength\" | \"arcAngle\" | \"curl\"\n>) {\n  const half = arcAngle / 2;\n  const x = radius * Math.cos(half);\n  const y = radius * Math.sin(half);\n  const tangent = (4 / 3) * Math.tan(arcAngle / 4) * radius;\n  const arc = [\n    [x, -y],\n    [x + tangent * Math.sin(half), -y + tangent * Math.cos(half)],\n    [x + tangent * Math.sin(half), y - tangent * Math.cos(half)],\n    [x, y],\n  ];\n  const angle = (curl * Math.PI) / 2;\n  return [-0.5, -1 / 6, 1 / 6, 0.5].map((offset, index) => {\n    const px = radius + segmentLength * offset * Math.cos(angle);\n    const py = segmentLength * offset * Math.sin(angle);\n    return [mix(px, arc[index][0], curl), mix(py, arc[index][1], curl)];\n  });\n}\n\nexport function getRadialBurstSegmentPath(\n  state: Parameters<typeof getSegmentPoints>[0],\n) {\n  const points = getSegmentPoints(state).map((point) =>\n    point.map((value) => value.toFixed(3)).join(\" \"),\n  );\n  return `M ${points[0]} C ${points[1]} ${points[2]} ${points[3]}`;\n}\n\n/** Sample a bent centerline and its normals to make a tapered, filled ribbon. */\nexport function getRadialBurstRibbonPath(\n  state: ReturnType<typeof getRadialBurstState>,\n  index: number,\n  twist = 110,\n) {\n  const energy = state.energy;\n  const points = getSegmentPoints({\n    ...state,\n    radius: state.radius * (1 + Math.sin(index * 2.4) * energy * 0.15),\n    segmentLength: state.segmentLength * (1 + energy * 2.8),\n  });\n  const bend =\n    (Math.min(180, Math.max(-180, finite(twist, 110))) / 110) *\n    energy *\n    145 *\n    state.unit;\n  points[1][1] += bend * 0.55;\n  points[2][1] += bend;\n  // Carry the tip with the bend so the centerline does not double back into\n  // a hairpin while the ribbon transitions from a spoke to an arc.\n  points[3][1] += bend * 1.25;\n  points[2][0] += energy * 38 * state.unit;\n  const upper: string[] = [];\n  const lower: string[] = [];\n  const format = (x: number, y: number) => `${x.toFixed(3)} ${y.toFixed(3)}`;\n  let firstWidth = 0;\n  let lastWidth = 0;\n  for (let sample = 0; sample <= 36; sample++) {\n    const t = sample / 36;\n    const q = 1 - t;\n    const x =\n      q ** 3 * points[0][0] +\n      3 * q * q * t * points[1][0] +\n      3 * q * t * t * points[2][0] +\n      t ** 3 * points[3][0];\n    const y =\n      q ** 3 * points[0][1] +\n      3 * q * q * t * points[1][1] +\n      3 * q * t * t * points[2][1] +\n      t ** 3 * points[3][1];\n    const dx =\n      3 * q * q * (points[1][0] - points[0][0]) +\n      6 * q * t * (points[2][0] - points[1][0]) +\n      3 * t * t * (points[3][0] - points[2][0]);\n    const dy =\n      3 * q * q * (points[1][1] - points[0][1]) +\n      6 * q * t * (points[2][1] - points[1][1]) +\n      3 * t * t * (points[3][1] - points[2][1]);\n    const length = Math.max(0.001, Math.hypot(dx, dy));\n    const ddx =\n      6 * q * (points[2][0] - 2 * points[1][0] + points[0][0]) +\n      6 * t * (points[3][0] - 2 * points[2][0] + points[1][0]);\n    const ddy =\n      6 * q * (points[2][1] - 2 * points[1][1] + points[0][1]) +\n      6 * t * (points[3][1] - 2 * points[2][1] + points[1][1]);\n    const curvature = Math.abs(dx * ddy - dy * ddx) / length ** 3;\n    const swell = Math.sin(Math.PI * t) ** 1.4;\n    // Offset curves fold back on themselves when their width exceeds the\n    // bend radius. Limit the width locally to keep tight curls smooth.\n    const halfWidth = Math.min(\n      (state.strokeWidth / 2) * (1 + energy * (2.4 * swell - 0.65 * t)),\n      0.72 / Math.max(curvature, 0.00001),\n    );\n    if (sample === 0) firstWidth = halfWidth;\n    if (sample === 36) lastWidth = halfWidth;\n    upper.push(\n      format(x - (dy / length) * halfWidth, y + (dx / length) * halfWidth),\n    );\n    lower.push(\n      format(x + (dy / length) * halfWidth, y - (dx / length) * halfWidth),\n    );\n  }\n  return `M ${upper.join(\" L \")} A ${lastWidth} ${lastWidth} 0 0 0 ${lower[36]} L ${lower.slice(0, -1).reverse().join(\" L \")} A ${firstWidth} ${firstWidth} 0 0 0 ${upper[0]} Z`;\n}\n\nexport function RadialBurst({\n  segments = 8,\n  radius = 210,\n  thickness = 30,\n  rotation = 135,\n  intensity = 1,\n  twist = 110,\n  echoes = 3,\n  color = \"#ffffff\",\n  accentColor = \"#f4e4a7\",\n  backgroundColor = \"#a800b7\",\n  speed = 1,\n  loop = false,\n  className,\n}: RadialBurstProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const time = frame * (30 / fps) * Math.max(0, finite(speed, 1));\n  const options = {\n    segments,\n    radius,\n    thickness,\n    rotation,\n    intensity,\n    loop,\n    width,\n    height,\n  };\n  const state = getRadialBurstState(time, options);\n  const path = getRadialBurstSegmentPath(state);\n  const echoCount = Math.round(Math.min(5, Math.max(0, finite(echoes, 3))));\n  const plane = (pose: typeof state) =>\n    `translate(${pose.offsetX} ${pose.offsetY}) rotate(${pose.planeAngle}) scale(1 ${pose.planeScale})`;\n  const ribbons = (pose: typeof state, outline: boolean) =>\n    Array.from({ length: pose.count }, (_, index) => (\n      <path\n        key={index}\n        d={getRadialBurstRibbonPath(pose, index, twist)}\n        transform={`rotate(${(index * 360) / pose.count + pose.rotation - 90 + Math.sin(index * 1.7) * pose.energy * 16})`}\n        fill={\n          outline\n            ? \"none\"\n            : index % 3 === 0\n              ? interpolateColors(\n                  clamp(pose.energy),\n                  [0, 1],\n                  [color, accentColor],\n                )\n              : color\n        }\n        stroke={outline ? color : \"none\"}\n        strokeWidth={1.5 * pose.unit}\n      />\n    ));\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        backgroundColor,\n      }}\n    >\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        role=\"img\"\n        aria-label=\"Geometric radial burst\"\n        style={{ display: \"block\" }}\n      >\n        <g transform={`translate(${width / 2} ${height / 2})`}>\n          {state.strength > 0 &&\n          state.frame >= 18 &&\n          state.frame < radialBurstLength\n            ? Array.from({ length: echoCount }, (_, index) => {\n                const lag = (echoCount - index) * 2.2;\n                const echo = getRadialBurstState(\n                  Math.max(18, state.frame - lag),\n                  { ...options, loop: false },\n                );\n                const opacity =\n                  0.36 *\n                  (1 - index / (echoCount + 1)) *\n                  Math.min(1, state.strength) *\n                  (1 - easeOut(progress(state.frame, 107, 114)));\n                return (\n                  <g key={index} transform={plane(echo)} opacity={opacity}>\n                    {echo.showSegments ? (\n                      ribbons(echo, true)\n                    ) : echo.showRing ? (\n                      <circle\n                        r={echo.radius}\n                        fill=\"none\"\n                        stroke={index % 2 ? color : accentColor}\n                        strokeWidth={echo.strokeWidth * (0.22 + index * 0.07)}\n                      />\n                    ) : null}\n                  </g>\n                );\n              })\n            : null}\n          <g transform={plane(state)}>\n            {state.dotRadius > 0.001 ? (\n              <circle\n                r={state.dotRadius}\n                fill={color}\n                transform={`scale(${1 / state.dotSquash} ${state.dotSquash})`}\n              />\n            ) : null}\n            {state.showSegments && state.strength > 0 ? (\n              <g opacity={state.segmentOpacity}>{ribbons(state, false)}</g>\n            ) : state.showSegments ? (\n              <g\n                fill=\"none\"\n                stroke={color}\n                strokeWidth={state.strokeWidth}\n                strokeLinecap=\"round\"\n                opacity={state.segmentOpacity}\n                transform={`rotate(${state.rotation - 90})`}\n              >\n                {Array.from({ length: state.count }, (_, index) => (\n                  <path\n                    key={index}\n                    d={path}\n                    transform={`rotate(${(index * 360) / state.count})`}\n                  />\n                ))}\n              </g>\n            ) : null}\n            {state.showRing ? (\n              <circle\n                r={state.radius}\n                fill=\"none\"\n                stroke={color}\n                strokeWidth={state.strokeWidth}\n              />\n            ) : null}\n          </g>\n        </g>\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/radial-burst.tsx"
    }
  ],
  "type": "registry:component"
}
