{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lens-zoom",
  "title": "Lens Zoom",
  "description": "A hard cut disguised as a lens punch-in: barrel distortion, zoom blur, chromatic split and camera shake around the splice.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/lens-zoom/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport type { ReactNode } from \"react\";\nimport { useId } from \"react\";\nimport { Easing, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport const LENS_ZOOM_DURATION_IN_FRAMES = 28;\n\nexport const SHAKE_STOPS = [-9 / 11, -3 / 11, 4 / 11, 8 / 11] as const;\n\nexport const LENS_ZOOM_EASE_IN = Easing.bezier(0.54, 0, 0.82, 0.8);\nexport const LENS_ZOOM_EASE_OUT = Easing.bezier(0.18, 0.2, 0.46, 1);\n\nexport type LensZoomProps = {\n  inScaleFrom?: number;\n  inScaleTo?: number;\n  inFovFrom?: number;\n  inFovTo?: number;\n  inBlurFrom?: number;\n  inBlurTo?: number;\n  inRotationFrom?: number;\n  inRotationTo?: number;\n  outScaleFrom?: number;\n  outScaleTo?: number;\n  outFovFrom?: number;\n  outFovTo?: number;\n  outBlurFrom?: number;\n  outBlurTo?: number;\n  outRotationFrom?: number;\n  outRotationTo?: number;\n  anticipateScale?: number;\n  anticipatePortion?: number;\n  lensSteps?: number;\n  blurSamples?: number;\n  blurScaleGain?: number;\n  blurFade?: number;\n  shakeAmount?: number;\n  shakeFrequency?: number;\n  shakeCyclesPerUnit?: number;\n  shakeTranslatePx?: number;\n  shakeRotateDeg?: number;\n  redScale?: number;\n  greenScale?: number;\n  blueScale?: number;\n  letterboxAspect?: number;\n  width?: number;\n  height?: number;\n  distortionCenterX?: number;\n  distortionCenterY?: number;\n};\n\nfunction hashed(seed: number, i: number): number {\n  let t = Math.imul(i ^ seed, 0x27d4eb2d);\n  t ^= t >>> 15;\n  t = Math.imul(t, 0x85ebca6b);\n  t ^= t >>> 13;\n  return ((t >>> 0) / 4294967296) * 2 - 1;\n}\n\nfunction valueNoise(seed: number, t: number): number {\n  const i = Math.floor(t);\n  const f = t - i;\n  const u = f * f * (3 - 2 * f);\n  return hashed(seed, i) * (1 - u) + hashed(seed, i + 1) * u;\n}\n\nfunction fovHalf(fov: number): number {\n  return (Math.min(fov, 178) / 2) * (Math.PI / 180);\n}\n\nfunction lensMagnification(r: number, fov: number): number {\n  if (fov < 0.01) return 1;\n  const theta = fovHalf(fov);\n  const peak = Math.tan(theta) / theta;\n  if (r <= 0) return peak;\n  const a = r * theta;\n  if (a >= Math.PI / 2 - 1e-3) return 1;\n  const rSource = Math.tan(a) / Math.tan(theta);\n  if (rSource <= 1e-6) return peak;\n  return Math.max(1, r / rSource);\n}\n\nfunction radiusForMagnification(target: number, fov: number): number {\n  let lo = 0;\n  let hi = 1;\n  for (let i = 0; i < 24; i++) {\n    const mid = (lo + hi) / 2;\n    if (lensMagnification(mid, fov) > target) lo = mid;\n    else hi = mid;\n  }\n  return (lo + hi) / 2;\n}\n\nfunction ringClip(\n  rInner: number,\n  rOuter: number | null,\n  w: number,\n  h: number,\n  cx: number,\n  cy: number,\n): string {\n  const circle = (r: number, clockwise: boolean) => {\n    const s = clockwise ? 1 : 0;\n    return `M ${cx + r} ${cy} A ${r} ${r} 0 1 ${s} ${cx - r} ${cy} A ${r} ${r} 0 1 ${s} ${cx + r} ${cy} Z`;\n  };\n  const outer =\n    rOuter === null\n      ? `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`\n      : circle(rOuter, true);\n  return rInner > 0 ? `${outer} ${circle(rInner, false)}` : outer;\n}\n\ntype HalfProps = {\n  scene: ReactNode;\n  scale: number;\n  fov: number;\n  blur: number;\n  rotation: number;\n  centerX: number;\n  centerY: number;\n  width: number;\n  height: number;\n  lensSteps: number;\n  blurSamples: number;\n  blurScaleGain: number;\n  blurFade: number;\n};\n\nfunction TransitionHalf({\n  scene,\n  scale,\n  fov,\n  blur,\n  rotation,\n  centerX,\n  centerY,\n  width,\n  height,\n  lensSteps,\n  blurSamples,\n  blurScaleGain,\n  blurFade,\n}: HalfProps) {\n  const origin = `${centerX}px ${centerY}px`;\n  const base = scale / 100;\n\n  const peak = lensMagnification(0, fov);\n  const steps = peak <= 1.001 ? 0 : lensSteps;\n  const rings = Array.from({ length: steps }, (_, i) => {\n    const s = 1 + ((peak - 1) * (i + 0.5)) / steps;\n    return { s, r: radiusForMagnification(s, fov) };\n  });\n\n  const samples =\n    blur <= 0.05 ? 1 : Math.max(2, Math.round((blurSamples * blur) / 60));\n  const weights = Array.from(\n    { length: samples },\n    (_, j) => (1 - j / samples) ** blurFade,\n  );\n\n  const groups: {\n    key: number;\n    alpha: number;\n    copies: { key: string; clip: string; k: number }[];\n  }[] = [];\n  let running = 0;\n  for (let j = samples - 1; j >= 0; j--) {\n    running += weights[j];\n    const g =\n      samples === 1\n        ? 1\n        : 1 + (blur / 100) * blurScaleGain * (j / (samples - 1));\n    const halfWidth = (width / 2) * g;\n    const copies: { key: string; clip: string; k: number }[] = [];\n\n    copies.push({\n      key: \"base\",\n      clip: ringClip(\n        rings.length ? rings[0].r * halfWidth : 0,\n        null,\n        width,\n        height,\n        centerX,\n        centerY,\n      ),\n      k: g * base,\n    });\n\n    for (let i = 0; i < rings.length; i++) {\n      const inner = i + 1 < rings.length ? rings[i + 1].r * halfWidth : 0;\n      copies.push({\n        key: `${i}`,\n        clip: ringClip(\n          inner,\n          rings[i].r * halfWidth,\n          width,\n          height,\n          centerX,\n          centerY,\n        ),\n        k: g * rings[i].s * base,\n      });\n    }\n\n    groups.push({ key: j, alpha: weights[j] / running, copies });\n  }\n\n  return (\n    <div style={{ position: \"absolute\", inset: 0 }}>\n      {groups.map(({ key, alpha, copies }) => (\n        <div\n          key={key}\n          style={{ position: \"absolute\", inset: 0, opacity: alpha }}\n        >\n          {copies.map(({ key: ck, clip, k }) => (\n            <div\n              key={ck}\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                clipPath: `path(\"${clip}\")`,\n                rotate: `${rotation}deg`,\n                scale: `${k}`,\n                transformOrigin: origin,\n              }}\n            >\n              {scene}\n            </div>\n          ))}\n        </div>\n      ))}\n    </div>\n  );\n}\n\ninterface LensZoomStageProps extends HalfProps {\n  shakeEnvelope: number;\n  shakeFrequency: number;\n  shakeCyclesPerUnit: number;\n  shakeTranslatePx: number;\n  shakeRotateDeg: number;\n  redScale: number;\n  greenScale: number;\n  blueScale: number;\n  letterboxAspect: number;\n}\n\nfunction LensZoomStage({\n  shakeEnvelope,\n  shakeFrequency,\n  shakeCyclesPerUnit,\n  shakeTranslatePx,\n  shakeRotateDeg,\n  redScale,\n  greenScale,\n  blueScale,\n  letterboxAspect,\n  ...half\n}: LensZoomStageProps) {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const filterId = useId();\n  const { width, height, centerX, centerY } = half;\n\n  const core = <TransitionHalf {...half} />;\n\n  const split = redScale !== greenScale || blueScale !== greenScale;\n  const channels: { id: string; scale: number; matrix: string }[] = [\n    {\n      id: \"r\",\n      scale: redScale,\n      matrix: \"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\",\n    },\n    {\n      id: \"g\",\n      scale: greenScale,\n      matrix: \"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\",\n    },\n    {\n      id: \"b\",\n      scale: blueScale,\n      matrix: \"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\",\n    },\n  ];\n\n  const composited = split ? (\n    <div style={{ position: \"absolute\", inset: 0, backgroundColor: \"#000000\" }}>\n      {channels.map((c) => (\n        <div\n          key={c.id}\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            mixBlendMode: \"screen\",\n            filter: `url(#${filterId}-${c.id})`,\n            scale: `${c.scale}`,\n            transformOrigin: `${centerX}px ${centerY}px`,\n          }}\n        >\n          {core}\n        </div>\n      ))}\n    </div>\n  ) : (\n    core\n  );\n\n  const t = (frame / fps) * shakeFrequency * shakeCyclesPerUnit;\n  const octave = (seed: number) =>\n    valueNoise(seed, t) + 0.45 * valueNoise(seed + 977, t * 2.6);\n  const shakeX = octave(11) * shakeTranslatePx * shakeEnvelope;\n  const shakeY = octave(23) * shakeTranslatePx * shakeEnvelope;\n  const shakeR = octave(41) * shakeRotateDeg * shakeEnvelope;\n\n  const barHeight =\n    letterboxAspect > 0\n      ? Math.max(0, (height - width / letterboxAspect) / 2)\n      : 0;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        width,\n        height,\n        overflow: \"hidden\",\n        backgroundColor: \"#000000\",\n      }}\n    >\n      {split ? (\n        <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n          <defs>\n            {channels.map((c) => (\n              <filter\n                key={c.id}\n                id={`${filterId}-${c.id}`}\n                colorInterpolationFilters=\"sRGB\"\n              >\n                <feColorMatrix type=\"matrix\" values={c.matrix} />\n              </filter>\n            ))}\n          </defs>\n        </svg>\n      ) : null}\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          translate: `${shakeX}px ${shakeY}px`,\n          rotate: `${shakeR}deg`,\n          transformOrigin: `${centerX}px ${centerY}px`,\n        }}\n      >\n        {composited}\n      </div>\n\n      {barHeight > 0 ? (\n        <>\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: 0,\n              height: barHeight,\n              backgroundColor: \"#000000\",\n            }}\n          />\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              bottom: 0,\n              height: barHeight,\n              backgroundColor: \"#000000\",\n            }}\n          />\n        </>\n      ) : null}\n    </div>\n  );\n}\n\nconst LensZoomPresentation: React.FC<\n  TransitionPresentationComponentProps<LensZoomProps>\n> = ({\n  children,\n  presentationDirection,\n  presentationProgress,\n  passedProps,\n}) => {\n  const config = useVideoConfig();\n  const {\n    inScaleFrom = 100,\n    inScaleTo = 150,\n    inFovFrom = 0,\n    inFovTo = 160,\n    inBlurFrom = 0,\n    inBlurTo = 60,\n    inRotationFrom = 0,\n    inRotationTo = -15,\n\n    outScaleFrom = 135,\n    outScaleTo = 100,\n    outFovFrom = 135,\n    outFovTo = 0,\n    outBlurFrom = 45,\n    outBlurTo = 0,\n    outRotationFrom = 345,\n    outRotationTo = 360,\n\n    anticipateScale = 100,\n    anticipatePortion = 0.35,\n\n    lensSteps = 9,\n    blurSamples = 7,\n    blurScaleGain = 1,\n    blurFade = 1.5,\n\n    shakeAmount = 50,\n    shakeFrequency = 1.4,\n    shakeCyclesPerUnit = 8,\n    shakeTranslatePx = 73,\n    shakeRotateDeg = 1.1,\n\n    redScale = 1.01,\n    greenScale = 1.0,\n    blueScale = 0.99,\n\n    letterboxAspect = 0,\n\n    width = config.width,\n    height = config.height,\n    distortionCenterX = width / 2,\n    distortionCenterY = height / 2,\n  } = passedProps;\n\n  const exiting = presentationDirection === \"exiting\";\n\n  if (exiting ? presentationProgress >= 0.5 : presentationProgress < 0.5)\n    return null;\n\n  const p = exiting\n    ? interpolate(presentationProgress, [0, 0.5], [0, 1], {\n        easing: LENS_ZOOM_EASE_IN,\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      })\n    : interpolate(presentationProgress, [0.5, 1], [0, 1], {\n        easing: LENS_ZOOM_EASE_OUT,\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      });\n\n  const anticipating =\n    exiting && anticipatePortion > 0 && anticipateScale < inScaleFrom;\n\n  const pFx =\n    anticipating && exiting\n      ? interpolate(p, [anticipatePortion, 1], [0, 1], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        })\n      : p;\n\n  const lerp = (from: number, to: number) => from + (to - from) * pFx;\n\n  const exitScale = anticipating\n    ? interpolate(\n        p,\n        [0, anticipatePortion, 1],\n        [inScaleFrom, anticipateScale, inScaleTo],\n        {\n          easing: Easing.inOut(Easing.ease),\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        },\n      )\n    : lerp(inScaleFrom, inScaleTo);\n\n  const shakeEnvelope =\n    interpolate(\n      presentationProgress,\n      SHAKE_STOPS.map((s) => 0.5 + s / 2),\n      [0, shakeAmount, shakeAmount, 0],\n      { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n    ) / 100;\n\n  return (\n    <LensZoomStage\n      scene={children}\n      scale={exiting ? exitScale : lerp(outScaleFrom, outScaleTo)}\n      fov={exiting ? lerp(inFovFrom, inFovTo) : lerp(outFovFrom, outFovTo)}\n      blur={exiting ? lerp(inBlurFrom, inBlurTo) : lerp(outBlurFrom, outBlurTo)}\n      rotation={\n        exiting\n          ? lerp(inRotationFrom, inRotationTo)\n          : lerp(outRotationFrom, outRotationTo)\n      }\n      centerX={distortionCenterX}\n      centerY={distortionCenterY}\n      width={width}\n      height={height}\n      lensSteps={lensSteps}\n      blurSamples={blurSamples}\n      blurScaleGain={blurScaleGain}\n      blurFade={blurFade}\n      shakeEnvelope={shakeEnvelope}\n      shakeFrequency={shakeFrequency}\n      shakeCyclesPerUnit={shakeCyclesPerUnit}\n      shakeTranslatePx={shakeTranslatePx}\n      shakeRotateDeg={shakeRotateDeg}\n      redScale={redScale}\n      greenScale={greenScale}\n      blueScale={blueScale}\n      letterboxAspect={letterboxAspect}\n    />\n  );\n};\n\nexport function lensZoom(\n  props: LensZoomProps = {},\n): TransitionPresentation<LensZoomProps> {\n  return {\n    component: LensZoomPresentation,\n    props,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/lens-zoom.tsx"
    }
  ],
  "type": "registry:component"
}
