{
  "name": "code-diff-wipe",
  "type": "registry:block",
  "description": "Diff patch landing line by line: an apply front travels the file, removals collapse, additions open and write in",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "ai-composer-utils",
    "code-syntax",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/code-diff-wipe/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/JetBrainsMono\";\nimport {\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { stageScale } from \"@/remotion/lib/ai-composer-utils\";\nimport { CODE_THEMES, CodeLine, tokenizeCode } from \"@/remotion/lib/code-syntax\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\", \"700\"],\n  subsets: [\"latin\"],\n});\n\n/** Reference stage the window is laid out against, then uniformly scaled. */\nconst REF_WIDTH = 1280;\nconst REF_HEIGHT = 720;\n\nconst WINDOW_WIDTH = 940;\nconst HEADER_HEIGHT = 46;\nconst BODY_PADDING = 22;\nconst ROW_HEIGHT = 34;\nconst FONT_SIZE = 20;\nconst GUTTER_WIDTH = 46;\nconst CODE_INSET = 18;\n\nexport type CodeDiffWipeProps = {\n  /** Source before the patch. */\n  before?: string;\n  /** Source after the patch. */\n  after?: string;\n  /** Filename on the window header. */\n  title?: string;\n  /** Seconds the patch front takes to travel the file. */\n  wipeSeconds?: number;\n  accentColor?: string;\n  /** Overrides the page background behind the window. */\n  backgroundColor?: string;\n  theme?: \"dark\" | \"light\";\n  /** Animation speed multiplier. */\n  speed?: number;\n};\n\nconst DEFAULT_BEFORE = `export function render(scene) {\n  const frames = scene.frames;\n  return frames.map(draw);\n}`;\n\nconst DEFAULT_AFTER = `export function render(scene, options) {\n  const frames = scene.frames;\n  const scale = options.scale ?? 1;\n  return frames.map((frame) => draw(frame, scale));\n}`;\n\ntype DiffKind = \"context\" | \"remove\" | \"add\";\n\ntype DiffRow = {\n  kind: DiffKind;\n  text: string;\n};\n\n/**\n * Line-level LCS diff. Snippets in a video are a handful of lines, so the\n * quadratic table is free and the result is the one a reader expects.\n */\nfunction diffLines(before: string[], after: string[]): DiffRow[] {\n  const table: number[][] = Array.from({ length: before.length + 1 }, () =>\n    new Array(after.length + 1).fill(0),\n  );\n\n  for (let i = before.length - 1; i >= 0; i -= 1) {\n    for (let j = after.length - 1; j >= 0; j -= 1) {\n      table[i][j] =\n        before[i] === after[j]\n          ? table[i + 1][j + 1] + 1\n          : Math.max(table[i + 1][j], table[i][j + 1]);\n    }\n  }\n\n  const rows: DiffRow[] = [];\n  let i = 0;\n  let j = 0;\n\n  while (i < before.length && j < after.length) {\n    if (before[i] === after[j]) {\n      rows.push({ kind: \"context\", text: before[i] });\n      i += 1;\n      j += 1;\n    } else if (table[i + 1][j] >= table[i][j + 1]) {\n      rows.push({ kind: \"remove\", text: before[i] });\n      i += 1;\n    } else {\n      rows.push({ kind: \"add\", text: after[j] });\n      j += 1;\n    }\n  }\n\n  while (i < before.length) {\n    rows.push({ kind: \"remove\", text: before[i] });\n    i += 1;\n  }\n  while (j < after.length) {\n    rows.push({ kind: \"add\", text: after[j] });\n    j += 1;\n  }\n\n  return rows;\n}\n\nconst CHROME_LIGHTS = [\"#FF5F57\", \"#FEBC2E\", \"#28C840\"] as const;\n\n/** Drawn markers — a bare \"+\"/\"−\" in the gutter is a font gamble at this size. */\nconst Plus: React.FC<{ size: number; color: string }> = ({ size, color }) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n    <path\n      d=\"M12 5.5v13M5.5 12h13\"\n      stroke={color}\n      strokeWidth={2.6}\n      strokeLinecap=\"round\"\n    />\n  </svg>\n);\n\nconst Minus: React.FC<{ size: number; color: string }> = ({ size, color }) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n    <path\n      d=\"M5.5 12h13\"\n      stroke={color}\n      strokeWidth={2.6}\n      strokeLinecap=\"round\"\n    />\n  </svg>\n);\n\n/**\n * Diff wipe staged as the patch actually landing: the before-file sits whole,\n * then an apply front travels down it — removed lines redden and collapse as it\n * passes, added lines open under it and write themselves in — and the header\n * tallies the change as it goes.\n */\nexport const CodeDiffWipe: React.FC<CodeDiffWipeProps> = ({\n  before = DEFAULT_BEFORE,\n  after = DEFAULT_AFTER,\n  title = \"render.ts\",\n  wipeSeconds = 1.7,\n  accentColor = \"#E8B86D\",\n  backgroundColor,\n  theme = \"dark\",\n  speed = 1,\n}) => {\n  const rawFrame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const palette = CODE_THEMES[theme];\n  const page = backgroundColor ?? palette.page;\n  const scale = stageScale(width, height, REF_WIDTH, REF_HEIGHT);\n  const frame = rawFrame * speed;\n\n  const seconds = (value: number) => value * fps;\n  const clamp = {\n    extrapolateLeft: \"clamp\" as const,\n    extrapolateRight: \"clamp\" as const,\n  };\n\n  const rows = diffLines(\n    before.replace(/\\s+$/, \"\").replace(/^\\n+/, \"\").split(\"\\n\"),\n    after.replace(/\\s+$/, \"\").replace(/^\\n+/, \"\").split(\"\\n\"),\n  );\n  const tokenRows = tokenizeCode(rows.map((row) => row.text));\n\n  const added = rows.filter((row) => row.kind === \"add\").length;\n  const removed = rows.filter((row) => row.kind === \"remove\").length;\n\n  const addColor = palette.token.string;\n  const removeColor = theme === \"dark\" ? \"#F87171\" : \"#DC2626\";\n\n  // --- Timeline -----------------------------------------------------------\n  const wipeStart = seconds(0.7);\n  const wipeFrames = seconds(wipeSeconds);\n  const rowTrigger = (index: number) =>\n    wipeStart + (rows.length <= 1 ? 0 : (index / rows.length) * wipeFrames);\n  const wipeEnd = wipeStart + wipeFrames;\n\n  /** 0 → 1 as the apply front passes a given row. */\n  const rowProgress = (index: number) =>\n    interpolate(\n      frame,\n      [rowTrigger(index), rowTrigger(index) + seconds(0.3)],\n      [0, 1],\n      { easing: EASING.enter, ...clamp },\n    );\n\n  const enter = spring({\n    fps,\n    frame,\n    config: { damping: 18, stiffness: 120, mass: 0.8 },\n  });\n  const chromeIn = interpolate(frame, [seconds(0.1), seconds(0.4)], [0, 1], {\n    easing: EASING.enter,\n    ...clamp,\n  });\n  // Tints hold while the change is fresh, then cool off.\n  const settle = interpolate(\n    frame,\n    [wipeEnd + seconds(0.7), wipeEnd + seconds(1.4)],\n    [0, 1],\n    { easing: EASING.enter, ...clamp },\n  );\n\n  // Removed lines hold their place, struck through, while the front passes —\n  // both sides of the change are readable together, the way a diff is meant to\n  // be read. Only once the patch has fully landed do they drop away, leaving\n  // the new file behind.\n  const dropOld = interpolate(\n    frame,\n    [wipeEnd + seconds(0.3), wipeEnd + seconds(0.75)],\n    [0, 1],\n    { easing: EASING.enter, ...clamp },\n  );\n\n  const heights = rows.map((row, index) => {\n    const progress = rowProgress(index);\n    if (row.kind === \"remove\") return ROW_HEIGHT * (1 - dropOld);\n    if (row.kind === \"add\") return ROW_HEIGHT * progress;\n    return ROW_HEIGHT;\n  });\n\n  // The front is drawn where the rows it has already passed end, so it always\n  // sits on the seam between changed and untouched code.\n  const frontIndex = interpolate(frame, [wipeStart, wipeEnd], [0, rows.length], {\n    ...clamp,\n  });\n  const frontY = heights\n    .slice(0, Math.floor(frontIndex))\n    .reduce((total, value) => total + value, 0);\n  const frontVisible = interpolate(\n    frame,\n    [wipeStart - seconds(0.15), wipeStart, wipeEnd, wipeEnd + seconds(0.3)],\n    [0, 1, 1, 0],\n    clamp,\n  );\n\n  // The window breathes with its contents rather than reserving dead space for\n  // the widest moment of the diff.\n  const bodyHeight =\n    heights.reduce((total, value) => total + value, 0) + BODY_PADDING * 2;\n  const glyphSize = FONT_SIZE * 0.8;\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: page,\n        fontFamily,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        overflow: \"hidden\",\n        position: \"relative\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(ellipse 70% 50% at 50% 38%, ${accentColor}1C, transparent 70%)`,\n          opacity: chromeIn,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background:\n            theme === \"dark\"\n              ? \"radial-gradient(ellipse 120% 90% at 50% 50%, transparent 45%, rgba(0,0,0,0.55) 100%)\"\n              : \"radial-gradient(ellipse 120% 90% at 50% 50%, transparent 55%, rgba(15,18,25,0.07) 100%)\",\n        }}\n      />\n\n      <div\n        style={{\n          width: WINDOW_WIDTH,\n          transform: `scale(${scale * interpolate(enter, [0, 1], [0.965, 1])}) translateY(${interpolate(enter, [0, 1], [26, 0])}px)`,\n          opacity: enter,\n          borderRadius: 16,\n          background: palette.window,\n          border: `1px solid ${palette.border}`,\n          boxShadow: `inset 0 1px 0 ${palette.highlight}, 0 34px 90px ${palette.shadow}`,\n          overflow: \"hidden\",\n        }}\n      >\n        {/* Header chrome, with the tally filling in as the patch lands */}\n        <div\n          style={{\n            height: HEADER_HEIGHT,\n            display: \"flex\",\n            alignItems: \"center\",\n            padding: \"0 18px\",\n            borderBottom: `1px solid ${palette.border}`,\n            background: palette.header,\n            opacity: chromeIn,\n            position: \"relative\",\n          }}\n        >\n          <div style={{ display: \"flex\", gap: 8 }}>\n            {CHROME_LIGHTS.map((color) => (\n              <div\n                key={color}\n                style={{\n                  width: 11,\n                  height: 11,\n                  borderRadius: \"50%\",\n                  background: color,\n                }}\n              />\n            ))}\n          </div>\n          <span\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              textAlign: \"center\",\n              color: palette.dim,\n              fontSize: 15,\n              fontWeight: 500,\n              letterSpacing: 0.2,\n              pointerEvents: \"none\",\n            }}\n          >\n            {title}\n          </span>\n          <span\n            style={{\n              marginLeft: \"auto\",\n              display: \"flex\",\n              gap: 12,\n              fontSize: 14,\n              fontVariantNumeric: \"tabular-nums\",\n              // There is nothing to tally until the patch starts landing.\n              opacity: interpolate(\n                frame,\n                [wipeStart - seconds(0.2), wipeStart + seconds(0.2)],\n                [0, 1],\n                clamp,\n              ),\n            }}\n          >\n            <span style={{ color: addColor }}>\n              +\n              {Math.round(\n                interpolate(frame, [wipeStart, wipeEnd], [0, added], clamp),\n              )}\n            </span>\n            <span style={{ color: removeColor }}>\n              −\n              {Math.round(\n                interpolate(frame, [wipeStart, wipeEnd], [0, removed], clamp),\n              )}\n            </span>\n          </span>\n        </div>\n\n        {/* Diff body */}\n        <div\n          style={{\n            height: bodyHeight,\n            paddingTop: BODY_PADDING,\n            paddingBottom: BODY_PADDING,\n            position: \"relative\",\n            overflow: \"hidden\",\n          }}\n        >\n          <div style={{ position: \"relative\" }}>\n            {rows.map((row, index) => {\n              const progress = rowProgress(index);\n              const isAdd = row.kind === \"add\";\n              const isRemove = row.kind === \"remove\";\n              const tint = isRemove ? progress : (1 - settle) * progress;\n              const color = isAdd\n                ? addColor\n                : isRemove\n                  ? removeColor\n                  : palette.faint;\n              const revealed = isAdd\n                ? Math.round(progress * row.text.length)\n                : row.text.length;\n\n              return (\n                <div\n                  key={`${index}-${row.text}`}\n                  style={{\n                    height: heights[index],\n                    overflow: \"hidden\",\n                    display: \"flex\",\n                    // Rows close like a shutter from the bottom — centring the\n                    // content would clip the type from both sides at once.\n                    alignItems: \"flex-start\",\n                    fontSize: FONT_SIZE,\n                    lineHeight: `${ROW_HEIGHT}px`,\n                    background:\n                      isAdd || isRemove ? `${color}${tintAlpha(tint)}` : \"none\",\n                    // A removed line drains toward the gutter as it closes.\n                    opacity: isRemove ? 1 - progress * 0.3 - dropOld * 0.5 : 1,\n                  }}\n                >\n                  <span\n                    style={{\n                      width: GUTTER_WIDTH,\n                      height: ROW_HEIGHT,\n                      flexShrink: 0,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      justifyContent: \"center\",\n                    }}\n                  >\n                    {isAdd ? (\n                      <Plus size={glyphSize} color={addColor} />\n                    ) : isRemove ? (\n                      <Minus size={glyphSize} color={removeColor} />\n                    ) : null}\n                  </span>\n                  <span\n                    style={{\n                      paddingLeft: CODE_INSET,\n                      height: ROW_HEIGHT,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      // Struck-through text stays legible while it leaves.\n                      // Longhand only — mixing the `textDecoration` shorthand\n                      // with `textDecorationColor` warns on rerender.\n                      textDecorationLine:\n                        isRemove && progress > 0.15 ? \"line-through\" : \"none\",\n                      textDecorationColor: removeColor,\n                    }}\n                  >\n                    <CodeLine\n                      tokens={tokenRows[index]}\n                      theme={palette}\n                      reveal={revealed >= row.text.length ? undefined : revealed}\n                      muted={row.kind === \"context\"}\n                    />\n                  </span>\n                </div>\n              );\n            })}\n\n            {/* The apply front */}\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                right: 0,\n                top: frontY,\n                height: 2,\n                background: accentColor,\n                opacity: frontVisible * 0.9,\n                boxShadow: `0 0 18px 2px ${accentColor}66`,\n              }}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/** 0 → 1 tint as a two-digit hex alpha suffix for an 8-digit colour. */\nfunction tintAlpha(value: number): string {\n  const alpha = Math.round(Math.max(0, Math.min(1, value)) * 30);\n  return alpha.toString(16).padStart(2, \"0\");\n}\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "time",
    "tier": "core",
    "tags": [
      "code"
    ]
  }
}