{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-morph",
  "title": "Code Morph",
  "description": "A flat code editor that morphs between versions of a file, gliding each surviving token to its new line and column while removed code dissolves and new code fades in.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/code-morph/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Easing, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type CodeMorphTheme = \"dark\" | \"light\";\n\nexport type CodeMorphKind =\n  | \"keyword\"\n  | \"string\"\n  | \"number\"\n  | \"comment\"\n  | \"function\"\n  | \"type\"\n  | \"text\"\n  | \"punct\";\n\nexport type CodeMorphRole = \"rest\" | \"stay\" | \"move\" | \"exit\" | \"enter\";\n\nexport interface CodeMorphStep {\n  at: number;\n  code: string;\n}\n\nexport interface CodeMorphProps {\n  steps?: CodeMorphStep[];\n  morphDuration?: number;\n  highlight?: boolean;\n  fontSize?: number;\n  lineNumbers?: boolean;\n  filename?: string;\n  windowDots?: boolean;\n  theme?: CodeMorphTheme;\n  accentColor?: string;\n  speed?: number;\n  className?: string;\n}\n\ntype Paint = \"theme\" | \"accentColor\" | \"className\";\n\nexport interface CodeMorphOptions extends Omit<CodeMorphProps, Paint> {\n  width?: number;\n  height?: number;\n  fps?: number;\n}\n\nexport interface CodeMorphToken {\n  text: string;\n  kind: CodeMorphKind;\n  line: number;\n  col: number;\n  word: boolean;\n}\n\nexport interface CodeMorphDiff {\n  fromTo: number[];\n  toFrom: number[];\n}\n\nexport interface CodeMorphRenderToken {\n  key: string;\n  text: string;\n  kind: CodeMorphKind;\n  role: CodeMorphRole;\n  line: number;\n  col: number;\n  opacity: number;\n  scale: number;\n  blur: number;\n}\n\nexport interface CodeMorphRow {\n  key: string;\n  row: number;\n  opacity: number;\n}\n\nexport interface CodeMorphState {\n  time: number;\n  version: number;\n  morphing: boolean;\n  progress: number;\n  lines: number;\n  tokens: CodeMorphRenderToken[];\n  lineNumbers: CodeMorphRow[];\n  highlights: CodeMorphRow[];\n}\n\nconst BEFORE = `import { useEffect, useState } from \"react\";\n\nexport function Invoices() {\n  const [invoices, setInvoices] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    fetch(\"/api/invoices\")\n      .then((res) => res.json())\n      .then(setInvoices)\n      .finally(() => setLoading(false));\n  }, []);\n\n  if (loading) return <Spinner />;\n  return <InvoiceTable rows={invoices} />;\n}`;\n\nconst AFTER = `import useSWR from \"swr\";\n\nexport function Invoices() {\n  const { data, isLoading } = useSWR(\"/api/invoices\", fetcher);\n\n  if (isLoading) return <Spinner />;\n  return <InvoiceTable rows={data} />;\n}`;\n\nexport const codeMorphDefaults: Required<Omit<CodeMorphProps, \"className\">> = {\n  steps: [\n    { at: 0, code: BEFORE },\n    { at: 48, code: AFTER },\n  ],\n  morphDuration: 36,\n  highlight: true,\n  fontSize: 20,\n  lineNumbers: true,\n  filename: \"invoices.tsx\",\n  windowDots: true,\n  theme: \"dark\",\n  accentColor: \"#0ea5e9\",\n  speed: 1,\n};\n\nconst FONT_MONO =\n  \"var(--font-geist-mono, ui-monospace), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace\";\nconst REFERENCE_WIDTH = 1280;\nconst REFERENCE_HEIGHT = 720;\nconst MAX_WIDTH = 1152;\nconst MAX_HEIGHT = 640;\nconst ADVANCE = 0.6;\nconst LEADING = 1.55;\nconst HEADER = 42;\nconst PAD_TOP = 20;\nconst PAD_BOTTOM = 22;\nconst PAD_LEFT = 22;\nconst PAD_RIGHT = 28;\nconst GUTTER_GAP = 2;\nconst RADIUS = 14;\nconst DOT = 11;\nconst DOT_GAP = 7;\nconst DOT_LEFT = 16;\nconst DOTS = [0, 1, 2];\nconst TAB_OFFSET = 14;\nconst TAB_AFTER_DOTS = DOT_LEFT + 3 * DOT + 2 * DOT_GAP + TAB_OFFSET;\nconst TAB_LEFT = 10;\nconst TAB_TOP = 8;\nconst TAB_PAD = 14;\nconst TAB_RADIUS = 8;\nconst TAB_FONT = 13;\nconst BAR = 2;\nconst BLUR = 3.5;\nconst SHRINK = 0.08;\nconst GROW_FROM = 0.96;\nconst BASE = 36;\nconst EXIT_SPREAD = 4;\nconst EXIT_DURATION = 12;\nconst MOVE_START = 6;\nconst MOVE_DURATION = 22;\nconst CASCADE = 4;\nconst ENTER_START = 20;\nconst ENTER_SPREAD = 4;\nconst ENTER_DURATION = 12;\nconst ARC = 0.08;\nconst RISE = 8;\nconst SHOW = 24;\nconst FALL = 14;\nconst HOLD = 24;\nconst MIN_MORPH = 12;\nconst MAX_MORPH = 120;\nconst MIN_FONT = 10;\nconst MAX_FONT = 40;\nconst LINE_COST = 10;\nconst COLUMN_COST = 0.5;\nconst MAX_COST = 1000;\nconst PUNCT_VALUE = 12;\nconst TAB = \"  \";\n\nconst KEYWORDS = new Set([\n  \"abstract\",\n  \"as\",\n  \"async\",\n  \"await\",\n  \"break\",\n  \"case\",\n  \"catch\",\n  \"class\",\n  \"const\",\n  \"continue\",\n  \"debugger\",\n  \"declare\",\n  \"default\",\n  \"delete\",\n  \"do\",\n  \"else\",\n  \"enum\",\n  \"export\",\n  \"extends\",\n  \"false\",\n  \"finally\",\n  \"for\",\n  \"from\",\n  \"function\",\n  \"if\",\n  \"implements\",\n  \"import\",\n  \"in\",\n  \"instanceof\",\n  \"interface\",\n  \"keyof\",\n  \"let\",\n  \"namespace\",\n  \"new\",\n  \"null\",\n  \"of\",\n  \"private\",\n  \"protected\",\n  \"public\",\n  \"readonly\",\n  \"return\",\n  \"satisfies\",\n  \"static\",\n  \"super\",\n  \"switch\",\n  \"this\",\n  \"throw\",\n  \"true\",\n  \"try\",\n  \"type\",\n  \"typeof\",\n  \"undefined\",\n  \"var\",\n  \"void\",\n  \"while\",\n  \"yield\",\n]);\n\nconst OPERATORS = [\n  \"===\",\n  \"!==\",\n  \"**=\",\n  \"...\",\n  \"&&=\",\n  \"||=\",\n  \"??=\",\n  \"=>\",\n  \"==\",\n  \"!=\",\n  \"<=\",\n  \">=\",\n  \"&&\",\n  \"||\",\n  \"??\",\n  \"?.\",\n  \"++\",\n  \"--\",\n  \"+=\",\n  \"-=\",\n  \"*=\",\n  \"/=\",\n  \"%=\",\n  \"**\",\n  \"</\",\n  \"/>\",\n];\n\nconst NUMBER =\n  /0[xXbBoO][\\dA-Fa-f_]+n?|\\d[\\d_]*(?:\\.\\d[\\d_]*)?(?:[eE][+-]?\\d+)?n?/y;\nconst WORD =\n  /[A-Za-z_$\\u00c0-\\u024f\\u0370-\\u03ff][\\w$\\u00c0-\\u024f\\u0370-\\u03ff]*/y;\nconst SPACE = /\\s/;\nconst CAPITAL = /^[A-Z]/;\nconst OPENERS = new Set([\"(\", \"[\", \"{\"]);\nconst CLOSERS = new Map([\n  [\")\", \"(\"],\n  [\"]\", \"[\"],\n  [\"}\", \"{\"],\n]);\n\nconst glide = Easing.bezier(0.6, 0, 0.2, 1);\n\ninterface CodeMorphPalette {\n  panel: string;\n  chrome: string;\n  border: string;\n  dot: string;\n  label: string;\n  gutter: string;\n  keyword: string;\n  function: string;\n  type: string;\n  text: string;\n  punct: string;\n  comment: string;\n  mix: string;\n  share: number;\n  band: number;\n}\n\nconst PALETTES: Record<CodeMorphTheme, CodeMorphPalette> = {\n  dark: {\n    panel: \"#111113\",\n    chrome: \"#0c0c0e\",\n    border: \"#27272a\",\n    dot: \"#3f3f46\",\n    label: \"#e4e4e7\",\n    gutter: \"#52525b\",\n    keyword: \"#a1a1aa\",\n    function: \"#fafafa\",\n    type: \"#fafafa\",\n    text: \"#d4d4d8\",\n    punct: \"#71717a\",\n    comment: \"#5b5b63\",\n    mix: \"#ffffff\",\n    share: 80,\n    band: 0.16,\n  },\n  light: {\n    panel: \"#ffffff\",\n    chrome: \"#f7f7f8\",\n    border: \"#e4e4e7\",\n    dot: \"#d4d4d8\",\n    label: \"#27272a\",\n    gutter: \"#b4b4bb\",\n    keyword: \"#71717a\",\n    function: \"#09090b\",\n    type: \"#09090b\",\n    text: \"#3f3f46\",\n    punct: \"#909098\",\n    comment: \"#a1a1aa\",\n    mix: \"#000000\",\n    share: 70,\n    band: 0.12,\n  },\n};\n\ntype Open = \"comment\" | \"string\" | null;\ntype Lexeme = \"comment\" | \"string\" | \"number\" | \"word\" | \"punct\";\ntype Pair = [number, number];\n\ninterface Piece {\n  start: number;\n  end: number;\n  lexeme: Lexeme;\n}\n\ninterface Lexed {\n  text: string;\n  lexeme: Lexeme;\n  line: number;\n  col: number;\n}\n\nconst finite = (value: number | undefined, fallback: number) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst unit01 = (value: number) => clamp(value, 0, 1);\nconst easeOut = (value: number) => 1 - (1 - unit01(value)) ** 3;\nconst easeOutQuad = (value: number) => 1 - (1 - unit01(value)) ** 2;\nconst phase = (value: number, morph: number) => (value * morph) / BASE;\nconst range = (start: number, end: number) =>\n  Array.from({ length: Math.max(0, end - start) }, (_, k) => start + k);\n\nfunction smoothstep(value: number) {\n  const t = unit01(value);\n  return t * t * (3 - 2 * t);\n}\n\nexport function normalizeCodeMorphCode(code: string) {\n  const lines = code\n    .replace(/\\r\\n?/g, \"\\n\")\n    .replace(/\\t/g, TAB)\n    .split(\"\\n\")\n    .map((line) => line.trimEnd());\n  let start = 0;\n  while (start < lines.length && lines[start] === \"\") start += 1;\n  let end = lines.length;\n  while (end > start && lines[end - 1] === \"\") end -= 1;\n  const body = lines.slice(start, end);\n  const indents = body\n    .filter((line) => line !== \"\")\n    .map((line) => line.length - line.trimStart().length);\n  const cut = indents.length > 0 ? Math.min(...indents) : 0;\n  return body.map((line) => line.slice(cut)).join(\"\\n\");\n}\n\nfunction closing(line: string, from: number, quote: string) {\n  let index = from;\n  while (index < line.length) {\n    const char = line[index];\n    if (char === quote) return index;\n    index += char === \"\\\\\" ? 2 : 1;\n  }\n  return -1;\n}\n\nfunction matchAt(pattern: RegExp, line: string, index: number) {\n  pattern.lastIndex = index;\n  const match = pattern.exec(line);\n  return match ? match[0] : \"\";\n}\n\nfunction scanLine(line: string, open: Open): { pieces: Piece[]; open: Open } {\n  const pieces: Piece[] = [];\n  const emit = (start: number, end: number, lexeme: Lexeme) => {\n    pieces.push({ start, end, lexeme });\n    return end;\n  };\n  let index = 0;\n  if (open !== null) {\n    const comment = open === \"comment\";\n    const end = comment ? line.indexOf(\"*/\") : closing(line, 0, \"`\");\n    if (end === -1) {\n      emit(0, line.length, open);\n      return { pieces, open };\n    }\n    index = emit(0, end + (comment ? 2 : 1), open);\n  }\n  while (index < line.length) {\n    const char = line[index];\n    if (SPACE.test(char)) {\n      index += 1;\n      continue;\n    }\n    if (line.startsWith(\"//\", index)) {\n      emit(index, line.length, \"comment\");\n      break;\n    }\n    if (line.startsWith(\"/*\", index)) {\n      const end = line.indexOf(\"*/\", index + 2);\n      if (end === -1) {\n        emit(index, line.length, \"comment\");\n        return { pieces, open: \"comment\" };\n      }\n      index = emit(index, end + 2, \"comment\");\n      continue;\n    }\n    if (char === '\"' || char === \"'\" || char === \"`\") {\n      const end = closing(line, index + 1, char);\n      if (end === -1 && char === \"`\") {\n        emit(index, line.length, \"string\");\n        return { pieces, open: \"string\" };\n      }\n      index = emit(index, end === -1 ? line.length : end + 1, \"string\");\n      continue;\n    }\n    const number = matchAt(NUMBER, line, index);\n    if (number !== \"\") {\n      index = emit(index, index + number.length, \"number\");\n      continue;\n    }\n    const word = matchAt(WORD, line, index);\n    if (word !== \"\") {\n      index = emit(index, index + word.length, \"word\");\n      continue;\n    }\n    const operator = OPERATORS.find((item) => line.startsWith(item, index));\n    const glyph = String.fromCodePoint(line.codePointAt(index) ?? 0);\n    index = emit(index, index + (operator ?? glyph).length, \"punct\");\n  }\n  return { pieces, open: null };\n}\n\nfunction columnsOf(line: string) {\n  const columns: number[] = [];\n  let index = 0;\n  let column = 0;\n  for (const char of line) {\n    for (let unit = 0; unit < char.length; unit += 1) {\n      columns[index + unit] = column;\n    }\n    index += char.length;\n    column += 1;\n  }\n  columns[index] = column;\n  return columns;\n}\n\nfunction kindOf(lexed: Lexed[], index: number): CodeMorphKind {\n  const { lexeme, text } = lexed[index];\n  if (lexeme !== \"word\") return lexeme;\n  const previous = index > 0 ? lexed[index - 1].text : \"\";\n  const next = index + 1 < lexed.length ? lexed[index + 1].text : \"\";\n  const member = previous === \".\" || previous === \"?.\";\n  if (!member && KEYWORDS.has(text)) return \"keyword\";\n  if (next === \"(\") return \"function\";\n  if (CAPITAL.test(text)) return \"type\";\n  return \"text\";\n}\n\nfunction versionOf(source: string) {\n  const code = normalizeCodeMorphCode(source);\n  const lines = code.split(\"\\n\");\n  const lexed: Lexed[] = [];\n  let open: Open = null;\n  let columns = 0;\n  for (const [line, text] of lines.entries()) {\n    const map = columnsOf(text);\n    columns = Math.max(columns, map[text.length]);\n    const scanned = scanLine(text, open);\n    open = scanned.open;\n    for (const piece of scanned.pieces) {\n      let start = piece.start;\n      let end = piece.end;\n      while (start < end && SPACE.test(text[start])) start += 1;\n      while (end > start && SPACE.test(text[end - 1])) end -= 1;\n      if (end === start) continue;\n      const slice = text.slice(start, end);\n      const col = map[start];\n      lexed.push({ text: slice, lexeme: piece.lexeme, line, col });\n    }\n  }\n  const tokens: CodeMorphToken[] = lexed.map((item, index) => {\n    const kind = kindOf(lexed, index);\n    const { text, line, col } = item;\n    return { text, kind, line, col, word: kind !== \"punct\" };\n  });\n  return { code, tokens, lines: lines.length, columns };\n}\n\ntype CodeMorphVersion = ReturnType<typeof versionOf>;\n\nexport function tokenizeCodeMorph(code: string) {\n  return versionOf(code).tokens;\n}\n\ninterface Sides {\n  from: CodeMorphToken[];\n  to: CodeMorphToken[];\n  fromWidths: Map<number, number>;\n  toWidths: Map<number, number>;\n}\n\nfunction widthsOf(tokens: CodeMorphToken[]) {\n  const widths = new Map<number, number>();\n  for (const token of tokens) {\n    const end = token.col + Array.from(token.text).length;\n    widths.set(token.line, Math.max(widths.get(token.line) ?? 0, end));\n  }\n  return widths;\n}\n\ninterface Shift {\n  line: number;\n  lines: number;\n  cols: number;\n}\n\nfunction shiftOf(sides: Sides, pair: Pair): Shift {\n  const anchor = sides.from[pair[0]];\n  const landed = sides.to[pair[1]];\n  return {\n    line: anchor.line,\n    lines: landed.line - anchor.line,\n    cols: landed.col - anchor.col,\n  };\n}\n\nconst STILL: Shift = { line: -1, lines: 0, cols: 0 };\n\nfunction expectedCost(sides: Sides, i: number, j: number, shift: Shift) {\n  const source = sides.from[i];\n  const target = sides.to[j];\n  const lineOff = Math.abs(target.line - (source.line + shift.lines));\n  if (source.line !== shift.line) {\n    const colOff = Math.abs(target.col - source.col);\n    return lineOff * LINE_COST + colOff * COLUMN_COST;\n  }\n  const left = source.col + shift.cols;\n  const tail = (sides.fromWidths.get(source.line) ?? 0) - source.col;\n  const right = (sides.toWidths.get(target.line) ?? 0) - tail;\n  const colOff = Math.min(\n    Math.abs(target.col - left),\n    Math.abs(target.col - right),\n  );\n  return lineOff * LINE_COST + colOff * COLUMN_COST;\n}\n\nfunction costOf(\n  sides: Sides,\n  i: number,\n  j: number,\n  before: Shift | null,\n  after: Shift | null,\n) {\n  if (before === null && after === null) {\n    return Math.min(MAX_COST, expectedCost(sides, i, j, STILL));\n  }\n  const via = (shift: Shift | null) =>\n    shift === null ? MAX_COST : expectedCost(sides, i, j, shift);\n  return Math.min(MAX_COST, via(before), via(after));\n}\n\nfunction align(\n  sources: number[],\n  targets: number[],\n  value: (i: number, j: number) => number,\n): Pair[] {\n  const rows = sources.length;\n  const cols = targets.length;\n  if (rows === 0 || cols === 0) return [];\n  const width = cols + 1;\n  const worth = new Float64Array(rows * cols);\n  for (let a = 0; a < rows; a += 1) {\n    for (let b = 0; b < cols; b += 1) {\n      worth[a * cols + b] = value(sources[a], targets[b]);\n    }\n  }\n  const table = new Float64Array((rows + 1) * width);\n  const at = (a: number, b: number) => table[a * width + b];\n  for (let a = rows - 1; a >= 0; a -= 1) {\n    for (let b = cols - 1; b >= 0; b -= 1) {\n      const gain = worth[a * cols + b];\n      const skip = Math.max(at(a + 1, b), at(a, b + 1));\n      const take = gain > 0 ? at(a + 1, b + 1) + gain : 0;\n      table[a * width + b] = Math.max(skip, take);\n    }\n  }\n  const pairs: Pair[] = [];\n  let p = 0;\n  let q = 0;\n  while (p < rows && q < cols) {\n    const gain = worth[p * cols + q];\n    if (gain > 0 && at(p, q) === at(p + 1, q + 1) + gain) {\n      pairs.push([sources[p], targets[q]]);\n      p += 1;\n      q += 1;\n    } else if (at(p, q) === at(p + 1, q)) {\n      p += 1;\n    } else {\n      q += 1;\n    }\n  }\n  return pairs;\n}\n\nfunction increasing(candidates: Pair[]): Pair[] {\n  const length: number[] = [];\n  const previous: number[] = [];\n  let best = -1;\n  for (const [k, [, j]] of candidates.entries()) {\n    length[k] = 1;\n    previous[k] = -1;\n    for (let p = 0; p < k; p += 1) {\n      if (candidates[p][1] < j && length[p] + 1 > length[k]) {\n        length[k] = length[p] + 1;\n        previous[k] = p;\n      }\n    }\n    if (best === -1 || length[k] > length[best]) best = k;\n  }\n  const chain: Pair[] = [];\n  for (let k = best; k !== -1; k = previous[k]) chain.push(candidates[k]);\n  return chain.reverse();\n}\n\nfunction anchorsOf(from: CodeMorphToken[], to: CodeMorphToken[]) {\n  const fromCount = new Map<string, number>();\n  const toCount = new Map<string, number>();\n  const toIndex = new Map<string, number>();\n  for (const token of from) {\n    if (token.word) {\n      fromCount.set(token.text, (fromCount.get(token.text) ?? 0) + 1);\n    }\n  }\n  for (const [j, token] of to.entries()) {\n    if (!token.word) continue;\n    toCount.set(token.text, (toCount.get(token.text) ?? 0) + 1);\n    toIndex.set(token.text, j);\n  }\n  const candidates: Pair[] = [];\n  for (const [i, token] of from.entries()) {\n    const j = toIndex.get(token.text);\n    const unique =\n      fromCount.get(token.text) === 1 && toCount.get(token.text) === 1;\n    if (token.word && unique && j !== undefined) candidates.push([i, j]);\n  }\n  return increasing(candidates);\n}\n\nfunction guidesOf(sides: Sides, pairs: Pair[]) {\n  const size = sides.from.length;\n  const before: (Shift | null)[] = [];\n  const after: (Shift | null)[] = [];\n  let last: Shift | null = null;\n  let cursor = 0;\n  for (let i = 0; i < size; i += 1) {\n    while (cursor < pairs.length && pairs[cursor][0] <= i) {\n      last = shiftOf(sides, pairs[cursor]);\n      cursor += 1;\n    }\n    before.push(last);\n  }\n  let next: Shift | null = null;\n  cursor = pairs.length - 1;\n  for (let i = size - 1; i >= 0; i -= 1) {\n    while (cursor >= 0 && pairs[cursor][0] >= i) {\n      next = shiftOf(sides, pairs[cursor]);\n      cursor -= 1;\n    }\n    after[i] = next;\n  }\n  return { before, after };\n}\n\nfunction partnersOf(tokens: CodeMorphToken[]) {\n  const partner = tokens.map(() => -1);\n  const stack: number[] = [];\n  for (const [index, token] of tokens.entries()) {\n    if (token.kind !== \"punct\") continue;\n    if (OPENERS.has(token.text)) {\n      stack.push(index);\n      continue;\n    }\n    const opener = CLOSERS.get(token.text);\n    if (opener === undefined) continue;\n    let depth = stack.length - 1;\n    while (depth >= 0 && tokens[stack[depth]].text !== opener) depth -= 1;\n    if (depth < 0) continue;\n    partner[stack[depth]] = index;\n    partner[index] = stack[depth];\n    stack.length = depth;\n  }\n  return partner;\n}\n\nfunction linesOf(tokens: CodeMorphToken[]) {\n  const lines = new Map<number, number[]>();\n  for (const [index, token] of tokens.entries()) {\n    const list = lines.get(token.line);\n    if (list) list.push(index);\n    else lines.set(token.line, [index]);\n  }\n  return lines;\n}\n\nfunction signatureOf(tokens: CodeMorphToken[], indices: number[]) {\n  const base = tokens[indices[0]].col;\n  const shape = indices.map((i) => [tokens[i].col - base, tokens[i].text]);\n  return JSON.stringify(shape);\n}\n\nexport function diffCodeMorphTokens(\n  from: CodeMorphToken[],\n  to: CodeMorphToken[],\n): CodeMorphDiff {\n  const sides: Sides = {\n    from,\n    to,\n    fromWidths: widthsOf(from),\n    toWidths: widthsOf(to),\n  };\n  const guides = guidesOf(sides, anchorsOf(from, to));\n  const fromWords = range(0, from.length).filter((i) => from[i].word);\n  const toWords = range(0, to.length).filter((j) => to[j].word);\n  const weight = (Math.min(fromWords.length, toWords.length) + 1) * MAX_COST;\n  const words = align(fromWords, toWords, (i, j) => {\n    if (from[i].text !== to[j].text) return 0;\n    return weight - costOf(sides, i, j, guides.before[i], guides.after[i]);\n  });\n  const fromTo = from.map(() => -1);\n  const toFrom = to.map(() => -1);\n  const link = ([i, j]: Pair) => {\n    fromTo[i] = j;\n    toFrom[j] = i;\n  };\n  const bounds: Pair[] = [[-1, -1], ...words, [from.length, to.length]];\n  for (let k = 0; k + 1 < bounds.length; k += 1) {\n    const [a0, b0] = bounds[k];\n    const [a1, b1] = bounds[k + 1];\n    const inner = k + 2 < bounds.length;\n    const before = k > 0 ? shiftOf(sides, bounds[k]) : null;\n    const after = inner ? shiftOf(sides, bounds[k + 1]) : null;\n    const sources = range(a0 + 1, a1).filter((i) => !from[i].word);\n    const targets = range(b0 + 1, b1).filter((j) => !to[j].word);\n    const found = align(sources, targets, (i, j) => {\n      if (from[i].text !== to[j].text) return 0;\n      return PUNCT_VALUE - costOf(sides, i, j, before, after);\n    });\n    for (const pair of found) link(pair);\n    if (inner) link(bounds[k + 1]);\n  }\n  const fromPartner = partnersOf(from);\n  const toPartner = partnersOf(to);\n  for (const [i, token] of from.entries()) {\n    const j = fromTo[i];\n    if (j < 0 || !OPENERS.has(token.text)) continue;\n    const close = fromPartner[i];\n    const target = toPartner[j];\n    if (close < 0 || target < 0 || fromTo[close] === target) continue;\n    if (fromTo[close] >= 0) toFrom[fromTo[close]] = -1;\n    if (toFrom[target] >= 0) fromTo[toFrom[target]] = -1;\n    link([close, target]);\n  }\n  const fromLines = linesOf(from);\n  const pool = new Map<string, number[]>();\n  for (const [line, indices] of fromLines) {\n    const removed = indices.every((i) => fromTo[i] < 0);\n    if (!removed || !indices.some((i) => from[i].word)) continue;\n    const key = signatureOf(from, indices);\n    const list = pool.get(key);\n    if (list) list.push(line);\n    else pool.set(key, [line]);\n  }\n  for (const [line, indices] of linesOf(to)) {\n    if (!indices.every((j) => toFrom[j] < 0)) continue;\n    const list = pool.get(signatureOf(to, indices));\n    if (list === undefined || list.length === 0) continue;\n    let best = 0;\n    for (const [k, candidate] of list.entries()) {\n      if (Math.abs(candidate - line) < Math.abs(list[best] - line)) best = k;\n    }\n    const [source] = list.splice(best, 1);\n    const origin = fromLines.get(source) ?? [];\n    for (const [k, j] of indices.entries()) link([origin[k], j]);\n  }\n  return { fromTo, toFrom };\n}\n\nfunction ranks(lines: number[]) {\n  const distinct = Array.from(new Set(lines)).sort((a, b) => a - b);\n  const map = new Map<number, number>();\n  const last = distinct.length - 1;\n  for (const [index, line] of distinct.entries()) {\n    map.set(line, last > 0 ? index / last : 0);\n  }\n  return map;\n}\n\nfunction changedLines(\n  from: CodeMorphVersion,\n  to: CodeMorphVersion,\n  diff: CodeMorphDiff,\n) {\n  const sizes = new Map<number, number>();\n  for (const token of from.tokens) {\n    sizes.set(token.line, (sizes.get(token.line) ?? 0) + 1);\n  }\n  const members = new Map<number, number[]>();\n  for (const [j, token] of to.tokens.entries()) {\n    const list = members.get(token.line);\n    if (list) list.push(j);\n    else members.set(token.line, [j]);\n  }\n  const changed: number[] = [];\n  for (const [line, indices] of members) {\n    const sources = indices.map((j) => diff.toFrom[j]);\n    const origin = sources[0] >= 0 ? from.tokens[sources[0]].line : -1;\n    const inLine = (i: number) => i >= 0 && from.tokens[i].line === origin;\n    const copied = sources.every(inLine);\n    if (!(copied && sizes.get(origin) === indices.length)) changed.push(line);\n  }\n  return changed.sort((a, b) => a - b);\n}\n\nfunction morphOf(from: CodeMorphVersion, to: CodeMorphVersion) {\n  const diff = diffCodeMorphTokens(from.tokens, to.tokens);\n  const leaving: number[] = [];\n  const moving: number[] = [];\n  const entering: number[] = [];\n  for (const [i, token] of from.tokens.entries()) {\n    if (diff.fromTo[i] < 0) leaving.push(token.line);\n  }\n  for (const [j, token] of to.tokens.entries()) {\n    const i = diff.toFrom[j];\n    if (i < 0) {\n      entering.push(token.line);\n      continue;\n    }\n    const source = from.tokens[i];\n    if (source.line !== token.line || source.col !== token.col) {\n      moving.push(token.line);\n    }\n  }\n  return {\n    diff,\n    exitRank: ranks(leaving),\n    moveRank: ranks(moving),\n    enterRank: ranks(entering),\n    changed: changedLines(from, to, diff),\n  };\n}\n\nfunction stepsOf(steps: CodeMorphStep[] | undefined) {\n  const source = Array.isArray(steps) ? steps : codeMorphDefaults.steps;\n  const ordered = source\n    .map((step, index) => ({\n      index,\n      at: Math.max(0, finite(step.at, 0)),\n      code: typeof step.code === \"string\" ? step.code : \"\",\n    }))\n    .sort((a, b) => a.at - b.at || a.index - b.index);\n  return ordered.length > 0 ? ordered : [{ index: 0, at: 0, code: \"\" }];\n}\n\nexport function getCodeMorphLayout(options: CodeMorphOptions = {}) {\n  const width = Math.max(1, finite(options.width, REFERENCE_WIDTH));\n  const height = Math.max(1, finite(options.height, REFERENCE_HEIGHT));\n  const requested = finite(options.fontSize, codeMorphDefaults.fontSize);\n  const size = clamp(requested, MIN_FONT, MAX_FONT);\n  const lineNumbers = options.lineNumbers !== false;\n  const windowDots = options.windowDots !== false;\n  const filename = (options.filename ?? codeMorphDefaults.filename).trim();\n  const versions = stepsOf(options.steps).map((step) => versionOf(step.code));\n  const morphs: ReturnType<typeof morphOf>[] = [];\n  for (let index = 1; index < versions.length; index += 1) {\n    morphs.push(morphOf(versions[index - 1], versions[index]));\n  }\n  const rows = Math.max(...versions.map((version) => version.lines));\n  const columns = Math.max(...versions.map((version) => version.columns));\n  const digits = lineNumbers ? Math.max(2, String(rows).length) : 0;\n  const gutter = lineNumbers ? digits + GUTTER_GAP : 0;\n  const header = windowDots || filename !== \"\" ? HEADER : 0;\n  const leading = size * LEADING;\n  const panelWidth = PAD_LEFT + PAD_RIGHT + (gutter + columns) * ADVANCE * size;\n  const panelHeight = header + PAD_TOP + PAD_BOTTOM + rows * leading;\n  const fit = Math.min(1, MAX_WIDTH / panelWidth, MAX_HEIGHT / panelHeight);\n  const scale = Math.min(width / REFERENCE_WIDTH, height / REFERENCE_HEIGHT);\n  const unit = scale * fit;\n  return {\n    width,\n    height,\n    unit,\n    fit,\n    fontSize: size * unit,\n    lineHeight: leading * unit,\n    header: header * unit,\n    padTop: PAD_TOP * unit,\n    padBottom: PAD_BOTTOM * unit,\n    padLeft: PAD_LEFT * unit,\n    padRight: PAD_RIGHT * unit,\n    rows,\n    columns,\n    digits,\n    gutter,\n    lineNumbers,\n    windowDots,\n    filename,\n    versions,\n    morphs,\n  };\n}\n\nexport type CodeMorphLayout = ReturnType<typeof getCodeMorphLayout>;\n\nexport function getCodeMorphTimeline(options: CodeMorphOptions = {}) {\n  const requested = finite(\n    options.morphDuration,\n    codeMorphDefaults.morphDuration,\n  );\n  const morph = clamp(requested, MIN_MORPH, MAX_MORPH);\n  const highlight = options.highlight !== false;\n  const steps = stepsOf(options.steps).map((step) => step.at);\n  const starts: number[] = [];\n  let ready = 0;\n  for (const at of steps.slice(1)) {\n    const start = Math.max(at, ready);\n    starts.push(start);\n    ready = start + morph;\n  }\n  const morphs = starts.map((start, index) => {\n    const highlightStart = start + phase(ENTER_START, morph);\n    const natural = highlightStart + RISE + SHOW;\n    const next = index + 1 < starts.length ? starts[index + 1] : natural;\n    const fallStart = Math.min(natural, next);\n    return {\n      from: index,\n      to: index + 1,\n      start,\n      end: start + morph,\n      highlightStart,\n      fallStart,\n      highlightEnd: fallStart + FALL,\n    };\n  });\n  let settled = 0;\n  if (morphs.length > 0) {\n    const last = morphs[morphs.length - 1];\n    settled = highlight ? Math.max(last.end, last.highlightEnd) : last.end;\n  }\n  return { morph, highlight, steps, morphs, settled, duration: settled + HOLD };\n}\n\nexport type CodeMorphTimeline = ReturnType<typeof getCodeMorphTimeline>;\n\nexport const codeMorphLength = getCodeMorphTimeline().settled;\n\nexport function getCodeMorphDuration(options: CodeMorphOptions = {}) {\n  const speed = Math.max(0, finite(options.speed, codeMorphDefaults.speed));\n  if (speed === 0) return 1;\n  const { duration } = getCodeMorphTimeline(options);\n  return Math.max(1, Math.ceil(duration / speed));\n}\n\nexport function getCodeMorphTime(\n  frame: number,\n  options: CodeMorphOptions = {},\n) {\n  const fps = Math.max(1, finite(options.fps, 30));\n  const speed = Math.max(0, finite(options.speed, codeMorphDefaults.speed));\n  return Math.max(0, finite(frame, 0)) * (30 / fps) * speed;\n}\n\nfunction rowsOf(count: number, opacity: (row: number) => number) {\n  return Array.from({ length: count }, (_, row) => ({\n    key: `${row}`,\n    row,\n    opacity: opacity(row),\n  }));\n}\n\nfunction restingTokens(layout: CodeMorphLayout, version: number) {\n  const { tokens } = layout.versions[version];\n  return tokens.map(\n    (token, j): CodeMorphRenderToken => ({\n      key: `${version}:${j}`,\n      text: token.text,\n      kind: token.kind,\n      role: \"rest\",\n      line: token.line,\n      col: token.col,\n      opacity: 1,\n      scale: 1,\n      blur: 0,\n    }),\n  );\n}\n\nfunction morphFrame(\n  layout: CodeMorphLayout,\n  index: number,\n  local: number,\n  morph: number,\n) {\n  const data = layout.morphs[index];\n  const from = layout.versions[index];\n  const to = layout.versions[index + 1];\n  const blur = BLUR * layout.unit;\n  const tokens: CodeMorphRenderToken[] = [];\n  for (const [i, token] of from.tokens.entries()) {\n    if (data.diff.fromTo[i] >= 0) continue;\n    const rank = data.exitRank.get(token.line) ?? 0;\n    const delay = phase(EXIT_SPREAD, morph) * rank;\n    const progress = (local - delay) / phase(EXIT_DURATION, morph);\n    if (progress >= 1) continue;\n    const eased = easeOut(progress);\n    tokens.push({\n      key: `${index}>${i}`,\n      text: token.text,\n      kind: token.kind,\n      role: \"exit\",\n      line: token.line,\n      col: token.col,\n      opacity: 1 - eased,\n      scale: 1 - SHRINK * eased,\n      blur: blur * eased,\n    });\n  }\n  const travel = (delay: number) =>\n    (local - phase(MOVE_START, morph) - delay) / phase(MOVE_DURATION, morph);\n  for (const [j, token] of to.tokens.entries()) {\n    const i = data.diff.toFrom[j];\n    if (i < 0) continue;\n    const source = from.tokens[i];\n    const still = source.line === token.line && source.col === token.col;\n    const rank = data.moveRank.get(token.line) ?? 0;\n    const progress = travel(phase(CASCADE, morph) * rank);\n    const vertical = glide(unit01(progress / (1 - ARC)));\n    const horizontal = glide(unit01((progress - ARC) / (1 - ARC)));\n    tokens.push({\n      key: `${index + 1}:${j}`,\n      text: token.text,\n      kind: token.kind,\n      role: still ? \"stay\" : \"move\",\n      line: source.line + (token.line - source.line) * vertical,\n      col: source.col + (token.col - source.col) * horizontal,\n      opacity: 1,\n      scale: 1,\n      blur: 0,\n    });\n  }\n  for (const [j, token] of to.tokens.entries()) {\n    if (data.diff.toFrom[j] >= 0) continue;\n    const rank = data.enterRank.get(token.line) ?? 0;\n    const start = phase(ENTER_START + ENTER_SPREAD * rank, morph);\n    const progress = (local - start) / phase(ENTER_DURATION, morph);\n    if (progress <= 0) continue;\n    const eased = easeOut(progress);\n    tokens.push({\n      key: `${index + 1}:${j}`,\n      text: token.text,\n      kind: token.kind,\n      role: \"enter\",\n      line: token.line,\n      col: token.col,\n      opacity: eased,\n      scale: GROW_FROM + (1 - GROW_FROM) * eased,\n      blur: blur * (1 - eased),\n    });\n  }\n  const shrinking = to.lines < from.lines;\n  const lag = shrinking ? phase(CASCADE, morph) : 0;\n  const grown = glide(unit01(travel(lag) / (1 - ARC)));\n  const lines = from.lines + (to.lines - from.lines) * grown;\n  const shared = Math.min(from.lines, to.lines);\n  const lineNumbers = rowsOf(Math.max(from.lines, to.lines), (row) => {\n    if (row < shared) return 1;\n    return shrinking ? 1 - grown : grown;\n  });\n  return { tokens, lines, lineNumbers };\n}\n\nfunction highlightsAt(\n  time: number,\n  timeline: CodeMorphTimeline,\n  layout: CodeMorphLayout,\n  active: number,\n) {\n  const rows: CodeMorphRow[] = [];\n  if (!timeline.highlight) return rows;\n  for (const index of [active - 1, active]) {\n    if (index < 0) continue;\n    const morph = timeline.morphs[index];\n    const rise = easeOutQuad((time - morph.highlightStart) / RISE);\n    const fall = smoothstep((time - morph.fallStart) / FALL);\n    const opacity = rise * (1 - fall);\n    if (opacity <= 0) continue;\n    for (const row of layout.morphs[index].changed) {\n      rows.push({ key: `${index}:${row}`, row, opacity });\n    }\n  }\n  return rows;\n}\n\nexport function getCodeMorphState(\n  frame: number,\n  options: CodeMorphOptions = {},\n  layout: CodeMorphLayout = getCodeMorphLayout(options),\n): CodeMorphState {\n  const timeline = getCodeMorphTimeline(options);\n  const time = getCodeMorphTime(frame, options);\n  let active = -1;\n  for (const [index, morph] of timeline.morphs.entries()) {\n    if (morph.start <= time) active = index;\n  }\n  const highlights = highlightsAt(time, timeline, layout, active);\n  if (active === -1) {\n    const lines = layout.versions[0].lines;\n    return {\n      time,\n      version: 0,\n      morphing: false,\n      progress: 0,\n      lines,\n      tokens: restingTokens(layout, 0),\n      lineNumbers: rowsOf(lines, () => 1),\n      highlights,\n    };\n  }\n  const local = time - timeline.morphs[active].start;\n  if (local >= timeline.morph) {\n    const version = active + 1;\n    const lines = layout.versions[version].lines;\n    return {\n      time,\n      version,\n      morphing: false,\n      progress: 1,\n      lines,\n      tokens: restingTokens(layout, version),\n      lineNumbers: rowsOf(lines, () => 1),\n      highlights,\n    };\n  }\n  const motion = morphFrame(layout, active, local, timeline.morph);\n  return {\n    time,\n    version: active + 1,\n    morphing: true,\n    progress: local / timeline.morph,\n    lines: motion.lines,\n    tokens: motion.tokens,\n    lineNumbers: motion.lineNumbers,\n    highlights,\n  };\n}\n\nfunction transformOf(token: CodeMorphRenderToken, lineHeight: number) {\n  const move = `translate(${token.col}ch, ${token.line * lineHeight}px)`;\n  return token.scale === 1 ? move : `${move} scale(${token.scale})`;\n}\n\nexport function CodeMorph({\n  steps = codeMorphDefaults.steps,\n  morphDuration = codeMorphDefaults.morphDuration,\n  highlight = codeMorphDefaults.highlight,\n  fontSize = codeMorphDefaults.fontSize,\n  lineNumbers = codeMorphDefaults.lineNumbers,\n  filename = codeMorphDefaults.filename,\n  windowDots = codeMorphDefaults.windowDots,\n  theme = codeMorphDefaults.theme,\n  accentColor = codeMorphDefaults.accentColor,\n  speed = codeMorphDefaults.speed,\n  className,\n}: CodeMorphProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const layout = useMemo(() => {\n    const shape = { steps, fontSize, lineNumbers, filename, windowDots };\n    return getCodeMorphLayout({ ...shape, width, height });\n  }, [steps, fontSize, lineNumbers, filename, windowDots, width, height]);\n  const motion = { steps, morphDuration, highlight, speed, fps };\n  const state = getCodeMorphState(frame, motion, layout);\n  const palette = theme === \"light\" ? PALETTES.light : PALETTES.dark;\n  const unit = layout.unit;\n  const hairline = Math.max(1, unit);\n  const edge = `${hairline}px solid ${palette.border}`;\n  const top = layout.header + layout.padTop;\n  const blend = `${palette.share}%, ${palette.mix}`;\n  const accentText = `color-mix(in oklab, ${accentColor} ${blend})`;\n  const colorOf = (kind: CodeMorphKind) =>\n    kind === \"string\" || kind === \"number\" ? accentText : palette[kind];\n  const panelColumns = `${layout.gutter + layout.columns}ch`;\n  const panelPadding = layout.padLeft + layout.padRight;\n  const codeLeft = `${layout.gutter}ch`;\n  const tabLeft = (layout.windowDots ? TAB_AFTER_DOTS : TAB_LEFT) * unit;\n  const tabRadius = TAB_RADIUS * unit;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          position: \"relative\",\n          flexShrink: 0,\n          boxSizing: \"content-box\",\n          width: `calc(${panelColumns} + ${panelPadding}px)`,\n          height: top + layout.rows * layout.lineHeight + layout.padBottom,\n          background: palette.panel,\n          color: palette.text,\n          border: edge,\n          borderRadius: RADIUS * unit,\n          overflow: \"hidden\",\n          fontFamily: FONT_MONO,\n          fontSize: layout.fontSize,\n          lineHeight: `${layout.lineHeight}px`,\n          fontVariantLigatures: \"none\",\n          fontKerning: \"none\",\n        }}\n      >\n        {layout.header > 0 ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              right: 0,\n              height: layout.header,\n              background: palette.chrome,\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                right: 0,\n                bottom: 0,\n                height: hairline,\n                background: palette.border,\n              }}\n            />\n            {layout.windowDots\n              ? DOTS.map((dot) => (\n                  <div\n                    key={dot}\n                    style={{\n                      position: \"absolute\",\n                      left: (DOT_LEFT + dot * (DOT + DOT_GAP)) * unit,\n                      top: ((HEADER - DOT) / 2) * unit,\n                      width: DOT * unit,\n                      height: DOT * unit,\n                      borderRadius: \"50%\",\n                      background: palette.dot,\n                    }}\n                  />\n                ))\n              : null}\n            {layout.filename !== \"\" ? (\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: tabLeft,\n                  bottom: 0,\n                  height: (HEADER - TAB_TOP) * unit,\n                  boxSizing: \"border-box\",\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  padding: `0 ${TAB_PAD * unit}px`,\n                  background: palette.panel,\n                  borderTop: edge,\n                  borderLeft: edge,\n                  borderRight: edge,\n                  borderRadius: `${tabRadius}px ${tabRadius}px 0 0`,\n                  color: palette.label,\n                  fontSize: TAB_FONT * unit,\n                  lineHeight: 1,\n                  whiteSpace: \"pre\",\n                }}\n              >\n                {layout.filename}\n              </div>\n            ) : null}\n          </div>\n        ) : null}\n        {state.highlights.map((band) => (\n          <div\n            key={band.key}\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: top + band.row * layout.lineHeight,\n              height: layout.lineHeight,\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                background: accentColor,\n                opacity: band.opacity * palette.band,\n              }}\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                bottom: 0,\n                width: BAR * unit,\n                background: accentColor,\n                opacity: band.opacity,\n              }}\n            />\n          </div>\n        ))}\n        <div style={{ position: \"absolute\", left: layout.padLeft, top }}>\n          {layout.lineNumbers\n            ? state.lineNumbers.map((row) => (\n                <div\n                  key={row.key}\n                  style={{\n                    position: \"absolute\",\n                    left: 0,\n                    top: row.row * layout.lineHeight,\n                    width: `${layout.digits}ch`,\n                    textAlign: \"right\",\n                    whiteSpace: \"pre\",\n                    color: palette.gutter,\n                    opacity: row.opacity,\n                  }}\n                >\n                  {row.row + 1}\n                </div>\n              ))\n            : null}\n          <div style={{ position: \"absolute\", left: codeLeft, top: 0 }}>\n            {state.tokens.map((token) => (\n              <span\n                key={token.key}\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  top: 0,\n                  display: \"block\",\n                  whiteSpace: \"pre\",\n                  color: colorOf(token.kind),\n                  opacity: token.opacity,\n                  transform: transformOf(token, layout.lineHeight),\n                  filter: token.blur > 0 ? `blur(${token.blur}px)` : undefined,\n                }}\n              >\n                {token.text}\n              </span>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/code-morph.tsx"
    }
  ],
  "type": "registry:component"
}
