{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-swap",
  "title": "Diff Swap",
  "description": "A before line is diffed into the after line, ending 'Install the scene. Edit the props.'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/diff-swap/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadGeistMono } from \"@remotion/google-fonts/GeistMono\";\nimport { loadFont as loadInstrumentSans } from \"@remotion/google-fonts/InstrumentSans\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst sans = loadInstrumentSans(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst mono = loadGeistMono(\"normal\", { weights: [\"500\"], subsets: [\"latin\"] });\n\nexport interface DiffSwapProps {\n  before?: string;\n  after?: string;\n  highlight?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst FLUTED = [\"#1d2a8f\", \"#2f7bff\", \"#ff7a3d\", \"#ffe2c4\"] as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F4F3F0\",\n    dim: \"#77767D\",\n    sign: \"#B9B8BE\",\n    minusBand: `linear-gradient(90deg, color-mix(in oklab, ${FLUTED[2]} 22%, transparent) 0%, color-mix(in oklab, ${FLUTED[2]} 7%, transparent) 55%, transparent 100%)`,\n    minusEdge: `color-mix(in oklab, ${FLUTED[2]} 70%, transparent)`,\n    plusBand: (a: string) =>\n      `linear-gradient(90deg, color-mix(in oklab, ${a} 30%, transparent) 0%, color-mix(in oklab, ${a} 9%, transparent) 55%, transparent 100%)`,\n    ground: [\n      `radial-gradient(70% 70% at 104% 108%, color-mix(in oklab, ${FLUTED[0]} 62%, transparent) 0%, transparent 70%)`,\n      `radial-gradient(40% 40% at -4% -6%, color-mix(in oklab, ${FLUTED[2]} 12%, transparent) 0%, transparent 75%)`,\n    ].join(\", \"),\n    hl: (a: string) =>\n      `linear-gradient(95deg, ${FLUTED[3]} 0%, #ffb183 22%, ${FLUTED[1]} 62%, color-mix(in oklab, ${a} 60%, ${FLUTED[1]}) 100%)`,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#111113\",\n    dim: \"#9A9893\",\n    sign: \"#55534F\",\n    minusBand: `linear-gradient(90deg, color-mix(in oklab, ${FLUTED[2]} 18%, transparent) 0%, color-mix(in oklab, ${FLUTED[2]} 6%, transparent) 55%, transparent 100%)`,\n    minusEdge: `color-mix(in oklab, ${FLUTED[2]} 75%, transparent)`,\n    plusBand: (a: string) =>\n      `linear-gradient(90deg, color-mix(in oklab, ${a} 18%, transparent) 0%, color-mix(in oklab, ${a} 6%, transparent) 55%, transparent 100%)`,\n    ground: [\n      `radial-gradient(70% 70% at 104% 108%, color-mix(in oklab, ${FLUTED[1]} 16%, transparent) 0%, transparent 70%)`,\n      `radial-gradient(40% 40% at -4% -6%, color-mix(in oklab, ${FLUTED[3]} 60%, transparent) 0%, transparent 75%)`,\n    ].join(\", \"),\n    hl: (a: string) =>\n      `linear-gradient(95deg, #d9531c 0%, ${FLUTED[2]} 26%, ${a} 64%, ${FLUTED[0]} 100%)`,\n  },\n} as const;\n\nconst TRACK = -0.03;\nconst LINE = 1.1;\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IO = Easing.bezier(0.7, 0, 0.2, 1);\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\n\ntype Measure = (s: string) => number;\n\nfunction useMeasure(): Measure {\n  const [ready, setReady] = useState(false);\n  const [handle] = useState(() => delayRender(\"diff-swap: measuring\"));\n  const released = useRef(false);\n  const ctxRef = useRef<CanvasRenderingContext2D | null>(null);\n\n  useEffect(() => {\n    let live = true;\n    const font = `500 100px \"${sans.fontFamily}\"`;\n    Promise.all([sans.waitUntilDone(), document.fonts.load(font)])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n        if (ctx) ctx.font = font;\n        ctxRef.current = ctx;\n        setReady(true);\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [handle]);\n\n  const ctx = ready ? ctxRef.current : null;\n  return (s: string) =>\n    (ctx ? ctx.measureText(s).width / 100 : s.length * 0.55) +\n    TRACK * Math.max(0, s.length - 1);\n}\n\nfunction wrapTwo(text: string, measure: Measure, maxEm: number) {\n  const words = text.split(/\\s+/).filter(Boolean);\n  const one = words.join(\" \");\n  if (measure(one) <= maxEm || words.length < 2) {\n    return { lines: [one], widest: measure(one) };\n  }\n  let best = { lines: [one], widest: Number.POSITIVE_INFINITY };\n  for (let k = 1; k < words.length; k++) {\n    const a = words.slice(0, k).join(\" \");\n    const b = words.slice(k).join(\" \");\n    const w = Math.max(measure(a), measure(b));\n    if (w < best.widest) best = { lines: [a, b], widest: w };\n  }\n  return best;\n}\n\nfunction splitHighlight(line: string, phrase: string) {\n  if (!phrase) return [line, \"\", \"\"] as const;\n  const i = line.toLowerCase().indexOf(phrase.toLowerCase());\n  if (i < 0) return [line, \"\", \"\"] as const;\n  return [\n    line.slice(0, i),\n    line.slice(i, i + phrase.length),\n    line.slice(i + phrase.length),\n  ] as const;\n}\n\nexport function DiffSwap({\n  before = \"Keyframe every frame by hand.\",\n  after = \"Install the scene. Edit the props.\",\n  highlight = \"Edit the props.\",\n  accent = \"#3B5BFD\",\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: DiffSwapProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width, height, durationInFrames } = useVideoConfig();\n  const t = THEMES[theme];\n  const measure = useMeasure();\n\n  const portrait = height > width * 1.1;\n  const s = portrait ? width / 1080 : Math.min(width / 1280, height / 720);\n  const padX = width * (portrait ? 0.07 : 0.085);\n  const gutterEm = 0.95;\n\n  const cap = (portrait ? 118 : 92) * s;\n  const avail = width - padX * 2;\n  const fitRows = (fs: number) => {\n    const maxEm = avail / fs - gutterEm;\n    return {\n      b: wrapTwo(before, measure, maxEm),\n      a: wrapTwo(after, measure, maxEm),\n      maxEm,\n    };\n  };\n  let fs = cap;\n  let rows = fitRows(fs);\n  for (let i = 0; i < 24; i++) {\n    const tooWide = rows.b.widest > rows.maxEm || rows.a.widest > rows.maxEm;\n    const tall =\n      (rows.b.lines.length + rows.a.lines.length) * LINE * fs + fs * 0.6 >\n      height * (portrait ? 0.56 : 0.72);\n    if (!tooWide && !tall) break;\n    fs *= 0.94;\n    rows = fitRows(fs);\n  }\n  const rowPadY = fs * 0.2;\n  const rowH = (n: number) => n * LINE * fs + rowPadY * 2;\n  const hB = rowH(rows.b.lines.length);\n  const hA = rowH(rows.a.lines.length);\n  const gap = fs * 0.12;\n\n  const scaleEnd = Math.min(\n    portrait ? 1.5 : 1.42,\n    avail / fs / Math.max(rows.a.widest, 0.01),\n    (height * (portrait ? 0.5 : 0.62)) / (rows.a.lines.length * LINE * fs),\n  );\n\n  const enter = (i: number) => EASE_OUT(clamp01((frame - i * 3) / 18));\n  const mark = EASE_OUT(clamp01((frame - 30) / 14));\n  const open = EASE_IO(clamp01((frame - 42) / 18));\n  const addIn = (i: number) => EASE_OUT(clamp01((frame - 53 - i * 3) / 18));\n  const plusBand = EASE_OUT(clamp01((frame - 46) / 14));\n  const fold = EASE_IO(clamp01((frame - 72) / 20));\n  const clear = EASE_OUT(clamp01((frame - 78) / 16));\n  const grow = EASE_IO(clamp01((frame - 76) / 22));\n  const hlT = EASE_OUT(clamp01((frame - 84) / 18));\n  const drift = interpolate(\n    frame,\n    [0, durationInFrames * speed],\n    [0, 1],\n    CLAMP,\n  );\n\n  const curB = hB * (1 - fold);\n  const curGap = gap * open * (1 - fold);\n  const curA = hA * open;\n  const scale = 1 + (scaleEnd - 1) * grow;\n  const total = curB + curGap + curA * scale;\n  const top = height / 2 - total / 2;\n  const topA = top + curB + curGap;\n\n  const transparent = background === \"transparent\";\n  const gutter = gutterEm * fs;\n  const left = padX;\n  const bandW = width - padX * 2 + fs * 0.3;\n\n  const lineStyle: React.CSSProperties = {\n    fontFamily: sans.fontFamily,\n    fontWeight: 500,\n    fontSize: fs,\n    lineHeight: LINE,\n    letterSpacing: `${TRACK}em`,\n    whiteSpace: \"pre\",\n  };\n  const signStyle = (o: number, x: number): React.CSSProperties => ({\n    position: \"absolute\",\n    left: 0,\n    top: rowPadY,\n    width: gutter * 0.7,\n    textAlign: \"center\",\n    fontFamily: mono.fontFamily,\n    fontWeight: 500,\n    fontSize: fs * 0.72,\n    lineHeight: `${LINE * fs}px`,\n    color: t.sign,\n    opacity: o,\n    transform: `translateX(${x}px)`,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: transparent ? \"transparent\" : (background ?? t.bg),\n        overflow: \"hidden\",\n      }}\n    >\n      {!transparent && background === undefined ? (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: t.ground,\n            transform: `translate(${(-drift * 1.5).toFixed(3)}%, ${(-drift * 1).toFixed(3)}%) scale(1.04)`,\n          }}\n        />\n      ) : null}\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: left - fs * 0.15,\n          top,\n          width: bandW,\n          height: curB,\n          overflow: \"hidden\",\n          borderRadius: fs * 0.12,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: t.minusBand,\n            clipPath: `inset(0 ${((1 - mark) * 100).toFixed(2)}% 0 0)`,\n            opacity: 1 - clear,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            bottom: 0,\n            width: Math.max(2, fs * 0.04),\n            background: t.minusEdge,\n            opacity: mark * (1 - clear),\n          }}\n        />\n        <div style={signStyle(mark * (1 - fold), (1 - mark) * -fs * 0.3)}>\n          −\n        </div>\n        <div\n          style={{\n            position: \"absolute\",\n            left: gutter + fs * 0.15,\n            top: rowPadY - fold * hB * 0.55,\n            opacity: clamp01(1 - fold * 1.5),\n          }}\n        >\n          {rows.b.lines.map((ln, i) => {\n            const e = enter(i);\n            return (\n              <div\n                key={`b${i}`}\n                style={{\n                  overflow: \"hidden\",\n                  paddingBottom: fs * 0.12,\n                  marginBottom: -fs * 0.12,\n                }}\n              >\n                <div\n                  style={{\n                    ...lineStyle,\n                    color: `color-mix(in oklab, ${t.fg} ${((1 - mark) * 100).toFixed(1)}%, ${t.dim})`,\n                    transform: `translateY(${((1 - e) * 105).toFixed(2)}%)`,\n                  }}\n                >\n                  {ln}\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: left - fs * 0.15,\n          top: topA,\n          width: bandW,\n          height: curA,\n          overflow: \"hidden\",\n          borderRadius: fs * 0.12,\n          transform: `translateX(${(grow * (fs * 0.15 - (gutter + fs * 0.15) * scale)).toFixed(2)}px) scale(${scale.toFixed(4)})`,\n          transformOrigin: \"left top\",\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: t.plusBand(accent),\n            clipPath: `inset(0 ${((1 - plusBand) * 100).toFixed(2)}% 0 0)`,\n            opacity: 1 - clear,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            bottom: 0,\n            width: Math.max(2, fs * 0.04),\n            background: accent,\n            opacity: plusBand * (1 - clear),\n          }}\n        />\n        <div\n          style={signStyle(plusBand * (1 - clear), (1 - plusBand) * -fs * 0.3)}\n        >\n          +\n        </div>\n        <div\n          style={{\n            position: \"absolute\",\n            left: gutter + fs * 0.15,\n            top: rowPadY,\n          }}\n        >\n          {rows.a.lines.map((ln, i) => {\n            const e = addIn(i);\n            const [pre, hl, post] = splitHighlight(ln, highlight);\n            return (\n              <div\n                key={`a${i}`}\n                style={{\n                  overflow: \"hidden\",\n                  paddingBottom: fs * 0.12,\n                  marginBottom: -fs * 0.12,\n                }}\n              >\n                <div\n                  style={{\n                    ...lineStyle,\n                    color: t.fg,\n                    opacity: clamp01(e * 1.8),\n                    transform: `translateY(${((1 - e) * 105).toFixed(2)}%)`,\n                  }}\n                >\n                  {pre}\n                  {hl ? (\n                    <span style={{ position: \"relative\" }}>\n                      <span style={{ opacity: 1 - hlT }}>{hl}</span>\n                      <span\n                        style={{\n                          position: \"absolute\",\n                          left: 0,\n                          top: 0,\n                          opacity: hlT,\n                          backgroundImage: t.hl(accent),\n                          backgroundSize: \"160% 100%\",\n                          backgroundPosition: `${(drift * 50).toFixed(2)}% 0%`,\n                          WebkitBackgroundClip: \"text\",\n                          backgroundClip: \"text\",\n                          color: \"transparent\",\n                          paddingRight: \"0.05em\",\n                        }}\n                      >\n                        {hl}\n                      </span>\n                    </span>\n                  ) : null}\n                  {post}\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/diff-swap.tsx"
    }
  ],
  "type": "registry:component"
}
