{
  "name": "grid-pixelate-wipe",
  "type": "registry:ui",
  "description": "Reveal where the next scene arrives one grid cell at a time",
  "dependencies": [
    "remotion",
    "@remotion/transitions"
  ],
  "registryDependencies": [
    "transition-timing",
    "timing"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/grid-pixelate-wipe.tsx",
      "type": "registry:ui",
      "content": "import type { TransitionPresentation } from \"@remotion/transitions\";\nimport { useMemo } from \"react\";\nimport { AbsoluteFill, Easing, interpolate } from \"remotion\";\nimport {\n  resolveTransitionTiming,\n  transitionPhase,\n  type TransitionVariant,\n} from \"@/remotion/lib/transition-timing\";\n\nexport type GridPixelateOrder =\n  | \"from-left\"\n  | \"from-top\"\n  | \"diagonal\"\n  | \"center\";\n\nexport type GridPixelateShape = \"square\" | \"dot\";\n\nexport type GridPixelateWipeProps = {\n  cols?: number;\n  rows?: number;\n  /** Which corner or axis the cells light up from. */\n  order?: GridPixelateOrder;\n  shape?: GridPixelateShape;\n  /** 0 pops every cell together; 1 spreads them across the whole window. */\n  stagger?: number;\n};\n\nfunction cellRank(\n  order: GridPixelateOrder,\n  col: number,\n  row: number,\n  cols: number,\n  rows: number,\n): number {\n  const u = cols <= 1 ? 0 : col / (cols - 1);\n  const v = rows <= 1 ? 0 : row / (rows - 1);\n\n  switch (order) {\n    case \"from-left\":\n      return u;\n    case \"from-top\":\n      return v;\n    case \"diagonal\":\n      return (u + v) / 2;\n    case \"center\":\n      return Math.min(1, Math.hypot(u - 0.5, v - 0.5) / 0.7071);\n  }\n}\n\nfunction cellOpacity(\n  progress: number,\n  rank: number,\n  stagger: number,\n): number {\n  const revealAt = rank * stagger;\n  const span = Math.max(1 - stagger, 0.001);\n  const raw = Math.min(1, Math.max(0, (progress - revealAt) / span));\n\n  return interpolate(raw, [0, 1], [0, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n}\n\nfunction buildGridMask(\n  progress: number,\n  cols: number,\n  rows: number,\n  order: GridPixelateOrder,\n  shape: GridPixelateShape,\n  stagger: number,\n): string {\n  const cellW = 100 / cols;\n  const cellH = 100 / rows;\n  const cells: string[] = [];\n\n  for (let row = 0; row < rows; row++) {\n    for (let col = 0; col < cols; col++) {\n      const opacity = cellOpacity(\n        progress,\n        cellRank(order, col, row, cols, rows),\n        stagger,\n      );\n\n      if (opacity < 0.004) {\n        continue;\n      }\n\n      const alpha = opacity.toFixed(3);\n\n      if (shape === \"dot\") {\n        // The dot grows with its own reveal, so the frame fills in as a field\n        // of expanding points rather than a grid of fading squares.\n        const r = (Math.min(cellW, cellH) / 2) * opacity;\n        cells.push(\n          `<circle cx=\"${(col + 0.5) * cellW}\" cy=\"${(row + 0.5) * cellH}\" r=\"${r.toFixed(3)}\" fill=\"white\" fill-opacity=\"${alpha}\"/>`,\n        );\n        continue;\n      }\n\n      cells.push(\n        `<rect x=\"${col * cellW}\" y=\"${row * cellH}\" width=\"${cellW}\" height=\"${cellH}\" fill=\"white\" fill-opacity=\"${alpha}\"/>`,\n      );\n    }\n  }\n\n  const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\">${cells.join(\"\")}</svg>`;\n  return `url(\"data:image/svg+xml,${encodeURIComponent(svg)}\")`;\n}\n\nconst GridPixelateWipePresentation: React.FC<\n  React.ComponentProps<\n    NonNullable<TransitionPresentation<GridPixelateWipeProps>[\"component\"]>\n  >\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps: {\n    cols = 12,\n    rows = 8,\n    order = \"from-left\",\n    shape = \"square\",\n    stagger = 0.82,\n  },\n}) => {\n  const phase = transitionPhase(presentationProgress, presentationDirection, {\n    lead: 1,\n  });\n  const filled = 1 - phase.displace;\n\n  const style = useMemo(() => {\n    // Only the arriving scene is masked. Masking the outgoing one as well\n    // would leave both partly transparent mid-cut and show the background\n    // through the gaps between cells.\n    if (!phase.isEntering) {\n      return undefined;\n    }\n\n    // Built once and handed to both mask properties — the previous version\n    // called this twice per frame for identical output.\n    const mask = buildGridMask(filled, cols, rows, order, shape, stagger);\n\n    return {\n      WebkitMaskImage: mask,\n      maskImage: mask,\n      WebkitMaskSize: \"100% 100%\",\n      maskSize: \"100% 100%\",\n      WebkitMaskRepeat: \"no-repeat\",\n      maskRepeat: \"no-repeat\",\n    };\n  }, [cols, filled, order, phase.isEntering, rows, shape, stagger]);\n\n  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;\n};\n\n/** Grid cell stagger wipe presentation for TransitionSeries */\nexport function gridPixelateWipe(\n  props: GridPixelateWipeProps = {},\n): TransitionPresentation<GridPixelateWipeProps> {\n  return {\n    component: GridPixelateWipePresentation,\n    props,\n  };\n}\n\nexport type TransitionGridPixelateWipeConfig = {\n  durationInFrames?: number;\n  cols?: number;\n  rows?: number;\n  order?: GridPixelateOrder;\n  shape?: GridPixelateShape;\n  stagger?: number;\n  variant?: TransitionVariant;\n};\n\n/** Grid pixelate wipe transition config for use with TransitionSeries.Transition */\nexport function transitionGridPixelateWipe({\n  durationInFrames = 26,\n  cols = 12,\n  rows = 8,\n  order = \"from-left\",\n  shape = \"square\",\n  stagger = 0.82,\n  variant = \"editorial\",\n}: TransitionGridPixelateWipeConfig = {}) {\n  return {\n    presentation: gridPixelateWipe({ cols, rows, order, shape, stagger }),\n    timing: resolveTransitionTiming({ durationInFrames, variant }),\n  };\n}\n\nexport function getTransitionGridPixelateWipeDuration(\n  config: TransitionGridPixelateWipeConfig = {},\n  fps: number,\n): number {\n  return transitionGridPixelateWipe(config).timing.getDurationInFrames({ fps });\n}\n"
    }
  ],
  "atlas": {
    "lane": "cuts",
    "drive": "time",
    "tier": "advanced"
  }
}