{
  "name": "chromatic-aberration-wipe",
  "type": "registry:ui",
  "description": "Slide that pulls the colour channels apart at its fastest point and recombines them on landing",
  "dependencies": [
    "remotion",
    "@remotion/transitions"
  ],
  "registryDependencies": [
    "transition-timing"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/chromatic-aberration-wipe.tsx",
      "type": "registry:ui",
      "content": "import type { TransitionPresentation } from \"@remotion/transitions\";\nimport { useId, useMemo } from \"react\";\nimport { AbsoluteFill, interpolate } from \"remotion\";\nimport {\n  resolveTransitionTiming,\n  transitionPhase,\n  type TransitionVariant,\n} from \"@/remotion/lib/transition-timing\";\n\nexport type ChromaticAberrationAxis = \"horizontal\" | \"vertical\";\n\nexport type ChromaticAberrationWipeProps = {\n  /** Peak channel separation in px, reached in the middle of the cut. */\n  intensity?: number;\n  axis?: ChromaticAberrationAxis;\n  /** Share of the frame the scenes slide across, 0→1. */\n  slide?: number;\n};\n\n/**\n * Isolates one channel and drops the other two, so three stacked copies\n * recombine to the original image under `screen` — an actual channel\n * separation rather than a coloured drop-shadow behind the whole layer.\n */\nconst CHANNEL_MATRIX = {\n  red: \"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\",\n  green: \"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\",\n  blue: \"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\",\n} as const;\n\nconst CHANNEL_OFFSET: Record<keyof typeof CHANNEL_MATRIX, number> = {\n  red: 1,\n  green: 0,\n  blue: -1,\n};\n\nconst ChromaticPresentation: React.FC<\n  React.ComponentProps<\n    NonNullable<TransitionPresentation<ChromaticAberrationWipeProps>[\"component\"]>\n  >\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps: { intensity = 12, axis = \"horizontal\", slide = 1 },\n}) => {\n  const filterId = useId().replace(/:/g, \"\");\n  // No fade: the two scenes slide locked together and cover the frame between\n  // them, so dropping either one's opacity only lets the background through.\n  const phase = transitionPhase(presentationProgress, presentationDirection, {\n    lead: 1,\n  });\n\n  // The split is a stress artefact of the move: nothing at either end, hardest\n  // where the frame is travelling fastest.\n  const separation = interpolate(\n    phase.displace,\n    [0, 0.5, 1],\n    [0, intensity, 0],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n  const travel = phase.displace * (phase.isEntering ? 100 : -100) * slide;\n\n  const shift = useMemo(\n    () => (axis === \"horizontal\" ? `${travel}% 0%` : `0% ${travel}%`),\n    [axis, travel],\n  );\n\n  const channels = useMemo(\n    () =>\n      (Object.keys(CHANNEL_MATRIX) as (keyof typeof CHANNEL_MATRIX)[]).map(\n        (channel) => {\n          const offset = CHANNEL_OFFSET[channel] * separation;\n\n          return {\n            channel,\n            style: {\n              filter: `url(#${filterId}-${channel})`,\n              mixBlendMode: \"screen\" as const,\n              translate:\n                axis === \"horizontal\" ? `${offset}px 0px` : `0px ${offset}px`,\n            },\n          };\n        },\n      ),\n    [axis, filterId, separation],\n  );\n\n  // Under ~0.1px the three copies are indistinguishable from the original and\n  // only cost three extra composites, so the split layer is dropped entirely.\n  if (separation < 0.1) {\n    return (\n      <AbsoluteFill style={{ opacity: phase.opacity, translate: shift }}>\n        {children}\n      </AbsoluteFill>\n    );\n  }\n\n  return (\n    // `isolation` keeps the screen blend inside this scene. Without it the\n    // channel copies would blend with the other scene of the transition and\n    // wash the whole cut out.\n    <AbsoluteFill\n      style={{\n        opacity: phase.opacity,\n        translate: shift,\n        isolation: \"isolate\",\n      }}\n    >\n      <svg width={0} height={0} style={{ position: \"absolute\" }}>\n        <defs>\n          {(Object.keys(CHANNEL_MATRIX) as (keyof typeof CHANNEL_MATRIX)[]).map(\n            (channel) => (\n              <filter key={channel} id={`${filterId}-${channel}`}>\n                <feColorMatrix\n                  type=\"matrix\"\n                  values={CHANNEL_MATRIX[channel]}\n                />\n              </filter>\n            ),\n          )}\n        </defs>\n      </svg>\n      {channels.map(({ channel, style }) => (\n        <AbsoluteFill key={channel} style={style}>\n          {children}\n        </AbsoluteFill>\n      ))}\n    </AbsoluteFill>\n  );\n};\n\nexport function chromaticAberrationWipe(\n  props: ChromaticAberrationWipeProps = {},\n): TransitionPresentation<ChromaticAberrationWipeProps> {\n  return { component: ChromaticPresentation, props };\n}\n\nexport type TransitionChromaticAberrationWipeConfig = {\n  durationInFrames?: number;\n  intensity?: number;\n  axis?: ChromaticAberrationAxis;\n  slide?: number;\n  variant?: TransitionVariant;\n};\n\nexport function transitionChromaticAberrationWipe({\n  durationInFrames = 14,\n  intensity = 12,\n  axis = \"horizontal\",\n  slide = 1,\n  variant = \"editorial\",\n}: TransitionChromaticAberrationWipeConfig = {}) {\n  return {\n    presentation: chromaticAberrationWipe({ intensity, axis, slide }),\n    timing: resolveTransitionTiming({ durationInFrames, variant }),\n  };\n}\n\nexport function getTransitionChromaticAberrationWipeDuration(\n  config: TransitionChromaticAberrationWipeConfig = {},\n  fps: number,\n): number {\n  return transitionChromaticAberrationWipe(config).timing.getDurationInFrames({\n    fps,\n  });\n}\n"
    }
  ],
  "atlas": {
    "lane": "cuts",
    "drive": "time",
    "tier": "advanced"
  }
}