{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-spiral-pass",
  "title": "Shader Spiral Pass",
  "description": "Accelerate into the hero spiral and emerge through its soft center into the next scene.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "registryDependencies": ["@remocn/shader-light-tunnel"],
  "files": [
    {
      "path": "registry/remocn/shader-spiral-pass/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport { useLayoutEffect, useRef } from \"react\";\nimport { AbsoluteFill, useDelayRender, useVideoConfig } from \"remotion\";\nimport { createTunnelRenderer } from \"@/components/remocn/shader-light-tunnel\";\n\nexport type ShaderSpiralPassProps = {\n  /** Shader motion multiplier; transition duration is controlled by timing. */\n  speed?: number;\n  spirals?: number;\n  twist?: number;\n  /** Magnification reached while passing through the center, from 2 to 24. */\n  zoom?: number;\n  /** Feather width as a fraction of the shorter composition dimension. */\n  softness?: number;\n  timeOffset?: number;\n};\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst finite = (value: number, fallback: number, min: number, max: number) =>\n  clamp(Number.isFinite(value) ? value : fallback, min, max);\nconst smooth = (start: number, end: number, value: number) => {\n  const t = clamp((value - start) / (end - start), 0, 1);\n  return t * t * (3 - 2 * t);\n};\n\n/** Pure phase calculation also used to verify endpoint and coverage behavior. */\nexport function getSpiralPassPhase(progress: number, zoom = 14) {\n  const p = finite(progress, 0, 0, 1);\n  const dive = clamp((p - 0.08) / 0.84, 0, 1);\n  return {\n    cover: smooth(0, 0.18, p),\n    showNext: p >= 0.2,\n    zoom: Math.exp(Math.log(finite(zoom, 14, 2, 24)) * dive * dive),\n    aperture: smooth(0.64, 1, p) ** 2,\n    outgoingZoom: 1 + 0.5 * smooth(0, 0.22, p),\n    incomingZoom: 1 + 0.12 * (1 - smooth(0.55, 1, p)),\n  };\n}\n\nfunction ShaderSpiralPassPresentation({\n  children,\n  presentationProgress,\n  presentationDirection,\n  presentationDurationInFrames,\n  passedProps,\n}: TransitionPresentationComponentProps<ShaderSpiralPassProps>) {\n  const { width, height, fps } = useVideoConfig();\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const renderer = useRef<ReturnType<typeof createTunnelRenderer> | null>(null);\n  const {\n    speed = 1,\n    spirals = 3,\n    twist = 0.7,\n    zoom = 14,\n    softness = 0.12,\n    timeOffset = 0,\n  } = passedProps;\n  const p = finite(presentationProgress, 0, 0, 1);\n  const entering = presentationDirection === \"entering\";\n  const active = entering && p > 0 && p < 1;\n  const phase = getSpiralPassPhase(p, zoom);\n  const feather = Math.min(width, height) * finite(softness, 0.12, 0.02, 0.3);\n  const radius = (Math.hypot(width, height) / 2 + feather) * phase.aperture;\n  // Clip the incoming scene itself, in screen space. Its content must never\n  // extend through the shader outside the exit, even when the scene is scaled.\n  const edge = Math.min(feather, radius * 0.12);\n  const sceneMask =\n    entering && p < 1\n      ? `radial-gradient(circle at center, black ${Math.max(0, radius - edge)}px, transparent ${radius}px)`\n      : undefined;\n\n  useLayoutEffect(() => {\n    if (!active || !canvas.current) return;\n    const handle = delayRender(\"Initialize Shader Spiral Pass\");\n    try {\n      renderer.current = createTunnelRenderer(canvas.current);\n      continueRender(handle);\n    } catch (error) {\n      cancelRender(error instanceof Error ? error : new Error(String(error)));\n    }\n    return () => {\n      renderer.current?.dispose();\n      renderer.current = null;\n    };\n  }, [active, delayRender, continueRender, cancelRender]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: canvas resizing clears its pixels, including while paused.\n  useLayoutEffect(() => {\n    if (!active) return;\n    try {\n      // Integrate acceleration so shader motion speeds up without a time jump.\n      const seconds = (p * presentationDurationInFrames) / fps;\n      const time =\n        finite(timeOffset, 0, -10000, 10000) +\n        seconds * (1 + 0.8 * p * p) * finite(speed, 1, 0, 5);\n      renderer.current?.draw(time, twist, 1, 1, spirals, phase.zoom);\n    } catch (error) {\n      cancelRender(error instanceof Error ? error : new Error(String(error)));\n    }\n  }, [\n    active,\n    p,\n    presentationDurationInFrames,\n    fps,\n    speed,\n    timeOffset,\n    twist,\n    spirals,\n    phase.zoom,\n    width,\n    height,\n    cancelRender,\n  ]);\n\n  return (\n    <AbsoluteFill style={{ overflow: \"hidden\" }}>\n      {active && (\n        <AbsoluteFill\n          style={{\n            opacity: phase.cover,\n            pointerEvents: \"none\",\n          }}\n        >\n          <canvas\n            ref={canvas}\n            width={width}\n            height={height}\n            aria-hidden=\"true\"\n            tabIndex={-1}\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              width: \"100%\",\n              height: \"100%\",\n            }}\n          />\n        </AbsoluteFill>\n      )}\n      <AbsoluteFill\n        style={{\n          visibility: entering !== phase.showNext ? \"hidden\" : \"visible\",\n          opacity: entering && p < 1 ? smooth(0.64, 0.72, p) : 1,\n          maskImage: sceneMask,\n          WebkitMaskImage: sceneMask,\n        }}\n      >\n        <AbsoluteFill\n          style={{\n            transform: `scale(${entering ? phase.incomingZoom : phase.outgoingZoom})`,\n            transformOrigin: \"center\",\n          }}\n        >\n          {children}\n        </AbsoluteFill>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n\nexport function shaderSpiralPass(\n  props: ShaderSpiralPassProps = {},\n): TransitionPresentation<ShaderSpiralPassProps> {\n  return { component: ShaderSpiralPassPresentation, props };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/shader-spiral-pass.tsx"
    }
  ],
  "type": "registry:component"
}
