{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"diff","title":"Diff","description":"Shows the changes to a file as a unified or split diff — line numbers, added and removed lines, +/- counts in the header and syntax highlighting — from a patch or from two versions of the file.","dependencies":["cn","diff","lucide-react","shiki"],"files":[{"path":"components/ui/diff.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport {\n  diffWordsWithSpace,\n  parsePatch,\n  structuredPatch,\n  type StructuredPatchHunk,\n} from \"diff\"\nimport { UnfoldVerticalIcon } from \"lucide-react\"\nimport { cn } from \"cn\"\n\ntype DiffLineKind = \"context\" | \"added\" | \"removed\"\n\ntype DiffLine = {\n  kind: DiffLineKind\n  content: string\n  oldNumber?: number\n  newNumber?: number\n  oldIndex?: number\n  newIndex?: number\n  change?: number\n  marks?: [number, number][]\n}\n\n/** One change: a run of removed and / or added lines with no unchanged line between. */\ntype DiffChange = {\n  index: number\n  /** Where the change starts in the old / new file. */\n  oldStart?: number\n  newStart?: number\n  removed: string[]\n  added: string[]\n}\n\ntype DiffHunk = { header: string; lines: DiffLine[] }\n\ntype Token = { content: string; color?: string }\n\ntype DiffContextValue = {\n  hunks: DiffHunk[]\n  changes: DiffChange[]\n  foldable: boolean\n  contextLines: number\n  view: \"unified\" | \"split\"\n  lineNumbers: \"both\" | \"single\"\n  added: number\n  removed: number\n  tokens: { old: Token[][]; new: Token[][] } | null\n}\n\nconst DiffContext = React.createContext<DiffContextValue | null>(null)\n\nfunction useDiff() {\n  const context = React.useContext(DiffContext)\n  if (!context) throw new Error(\"Diff parts must be used within Diff.\")\n  return context\n}\n\nfunction toHunks(hunks: StructuredPatchHunk[]) {\n  let oldIndex = 0\n  let newIndex = 0\n  return hunks.map((hunk): DiffHunk => {\n    let oldNumber = hunk.oldStart\n    let newNumber = hunk.newStart\n    const lines: DiffLine[] = []\n    for (const raw of hunk.lines) {\n      const sign = raw === \"\" ? \" \" : raw[0]\n      const content = raw.slice(1)\n      if (sign === \"-\") {\n        lines.push({\n          kind: \"removed\",\n          content,\n          oldNumber: oldNumber++,\n          oldIndex: oldIndex++,\n        })\n      } else if (sign === \"+\") {\n        lines.push({\n          kind: \"added\",\n          content,\n          newNumber: newNumber++,\n          newIndex: newIndex++,\n        })\n      } else if (sign === \" \") {\n        lines.push({\n          kind: \"context\",\n          content,\n          oldNumber: oldNumber++,\n          newNumber: newNumber++,\n          oldIndex: oldIndex++,\n          newIndex: newIndex++,\n        })\n      }\n    }\n    const oldStart = hunk.oldLines ? hunk.oldStart : hunk.oldStart - 1\n    const newStart = hunk.newLines ? hunk.newStart : hunk.newStart - 1\n    return {\n      header: `@@ -${oldStart},${hunk.oldLines} +${newStart},${hunk.newLines} @@`,\n      lines,\n    }\n  })\n}\n\nconst WORD_MARK_SIMILARITY = 0.4\nconst WORD_MARK_MAX_LENGTH = 1000\n\nfunction markWords(before: DiffLine, after: DiffLine) {\n  if (\n    before.content.length > WORD_MARK_MAX_LENGTH ||\n    after.content.length > WORD_MARK_MAX_LENGTH\n  )\n    return\n  const parts = diffWordsWithSpace(before.content, after.content)\n  const kept = parts\n    .filter((part) => !part.added && !part.removed)\n    .reduce((sum, part) => sum + part.value.length, 0)\n  const longest = Math.max(before.content.length, after.content.length, 1)\n  if (kept / longest < WORD_MARK_SIMILARITY) return\n  let oldAt = 0\n  let newAt = 0\n  for (const part of parts) {\n    const length = part.value.length\n    if (part.removed) {\n      before.marks ??= []\n      before.marks.push([oldAt, oldAt + length])\n      oldAt += length\n    } else if (part.added) {\n      after.marks ??= []\n      after.marks.push([newAt, newAt + length])\n      newAt += length\n    } else {\n      oldAt += length\n      newAt += length\n    }\n  }\n}\n\nfunction annotate(hunks: DiffHunk[]) {\n  const changes: DiffChange[] = []\n  let current: DiffChange | null = null\n  let removed: DiffLine[] = []\n  let added: DiffLine[] = []\n  const close = () => {\n    if (!current) return\n    for (let i = 0; i < Math.min(removed.length, added.length); i++)\n      markWords(removed[i], added[i])\n    changes.push(current)\n    current = null\n    removed = []\n    added = []\n  }\n  for (const hunk of hunks) {\n    for (const line of hunk.lines) {\n      if (line.kind === \"context\") {\n        close()\n        continue\n      }\n      current ??= { index: changes.length, removed: [], added: [] }\n      line.change = current.index\n      if (line.kind === \"removed\") {\n        current.oldStart ??= line.oldNumber\n        current.removed.push(line.content)\n        removed.push(line)\n      } else {\n        current.newStart ??= line.newNumber\n        current.added.push(line.content)\n        added.push(line)\n      }\n    }\n    close()\n  }\n  return changes\n}\n\nfunction languageOf(\n  language: string | undefined,\n  fileName: string | undefined\n) {\n  if (language) return language\n  const extension = fileName?.match(/\\.([a-z0-9]+)$/i)?.[1]\n  return extension?.toLowerCase()\n}\n\nconst syntaxColors: Record<string, string> = {\n  \"#6a737d\": \"var(--syntax-comment)\",\n  \"#586069\": \"var(--syntax-comment)\",\n  \"#d73a49\": \"var(--syntax-keyword)\",\n  \"#032f62\": \"var(--syntax-string)\",\n  \"#005cc5\": \"var(--syntax-constant)\",\n  \"#6f42c1\": \"var(--syntax-function)\",\n  \"#e36209\": \"var(--syntax-variable)\",\n  \"#22863a\": \"var(--syntax-tag)\",\n}\n\nlet syntaxTheme: ReturnType<typeof loadSyntaxTheme> | undefined\n\nasync function loadSyntaxTheme() {\n  const { default: github } = await import(\"shiki/themes/github-light.mjs\")\n  return {\n    name: \"sortui-syntax\",\n    type: \"light\" as const,\n    colors: {\n      \"editor.foreground\": \"var(--syntax-foreground)\",\n      \"editor.background\": \"transparent\",\n    },\n    tokenColors: github.tokenColors?.map((rule) => ({\n      ...rule,\n      settings: rule.settings.foreground\n        ? {\n            foreground:\n              syntaxColors[rule.settings.foreground.toLowerCase()] ??\n              \"var(--syntax-foreground)\",\n          }\n        : {},\n    })),\n  }\n}\n\nfunction useHighlight(hunks: DiffHunk[], language: string | undefined) {\n  const { oldText, newText } = React.useMemo(() => {\n    const lines = hunks.flatMap((hunk) => hunk.lines)\n    const text = (side: \"oldIndex\" | \"newIndex\") =>\n      lines\n        .filter((line) => line[side] !== undefined)\n        .map((line) => line.content)\n        .join(\"\\n\")\n    return { oldText: text(\"oldIndex\"), newText: text(\"newIndex\") }\n  }, [hunks])\n  const [highlighted, setHighlighted] = React.useState<{\n    source: string\n    tokens: NonNullable<DiffContextValue[\"tokens\"]>\n  } | null>(null)\n  const source = React.useMemo(\n    () => `${language}\\0${oldText}\\0${newText}`,\n    [language, oldText, newText]\n  )\n\n  React.useEffect(() => {\n    if (!language) return\n    let cancelled = false\n    import(\"shiki\")\n      .then(async ({ codeToTokens, bundledLanguages }) => {\n        if (!(language in bundledLanguages)) return\n        syntaxTheme ??= loadSyntaxTheme().catch((error: unknown) => {\n          syntaxTheme = undefined\n          throw error\n        })\n        const options = {\n          lang: language as keyof typeof bundledLanguages,\n          theme: await syntaxTheme,\n        }\n        const [old, next] = await Promise.all([\n          codeToTokens(oldText, options),\n          codeToTokens(newText, options),\n        ])\n        if (!cancelled)\n          setHighlighted({\n            source,\n            tokens: { old: old.tokens, new: next.tokens },\n          })\n      })\n      .catch(() => {})\n    return () => {\n      cancelled = true\n    }\n  }, [source, oldText, newText, language])\n\n  return highlighted?.source === source ? highlighted.tokens : null\n}\n\nfunction Diff({\n  patch,\n  oldValue,\n  newValue,\n  fileName,\n  language,\n  view = \"unified\",\n  lineNumbers = \"both\",\n  contextLines = 3,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** A unified diff; its first file is shown. */\n  patch?: string\n  /** The two versions of the file, diffed here (when there is no `patch`). */\n  oldValue?: string\n  newValue?: string\n  /** Picks the language from its extension when `language` is not set. */\n  fileName?: string\n  /** A Shiki language id, e.g. \"ts\", \"tsx\", \"css\". */\n  language?: string\n  view?: \"unified\" | \"split\"\n  /**\n   * In the unified view, the old and the new line number side by side\n   * (\"both\") or one column (\"single\": a removed line's old number, every\n   * other line's new one). Split shows one per side either way.\n   */\n  lineNumbers?: \"both\" | \"single\"\n  /**\n   * Unchanged lines kept around each change. With two values the rest fold\n   * into rows that open them; a patch carries only what it has.\n   */\n  contextLines?: number\n}) {\n  const parsed = React.useMemo(() => {\n    if (patch !== undefined) {\n      try {\n        return parsePatch(patch)[0]\n      } catch {\n        return undefined\n      }\n    }\n    return structuredPatch(\n      fileName ?? \"\",\n      fileName ?? \"\",\n      oldValue ?? \"\",\n      newValue ?? \"\",\n      undefined,\n      undefined,\n      { context: Number.MAX_SAFE_INTEGER }\n    )\n  }, [patch, oldValue, newValue, fileName])\n\n  const { hunks, changes, added, removed } = React.useMemo(() => {\n    const hunks = toHunks(parsed?.hunks ?? [])\n    const lines = hunks.flatMap((hunk) => hunk.lines)\n    return {\n      hunks,\n      changes: annotate(hunks),\n      added: lines.filter((line) => line.kind === \"added\").length,\n      removed: lines.filter((line) => line.kind === \"removed\").length,\n    }\n  }, [parsed])\n  const foldable = patch === undefined\n  const patchFileName = [parsed?.newFileName, parsed?.oldFileName]\n    .find((name) => name && name !== \"/dev/null\")\n    ?.replace(/^[ab]\\//, \"\")\n  const tokens = useHighlight(\n    hunks,\n    languageOf(language, fileName ?? patchFileName)\n  )\n\n  const value = React.useMemo(\n    () => ({\n      hunks,\n      changes,\n      foldable,\n      contextLines,\n      view,\n      lineNumbers,\n      tokens,\n      added,\n      removed,\n    }),\n    [\n      hunks,\n      changes,\n      foldable,\n      contextLines,\n      view,\n      lineNumbers,\n      tokens,\n      added,\n      removed,\n    ]\n  )\n\n  return (\n    <DiffContext.Provider value={value}>\n      <div\n        data-slot=\"diff\"\n        data-view={view}\n        className={cn(\n          \"flex min-w-0 flex-col overflow-hidden rounded-2xl bg-card text-sm text-card-foreground shadow-(--shadow-card) ring-1 ring-border\",\n          className\n        )}\n        {...props}\n      />\n    </DiffContext.Provider>\n  )\n}\n\nfunction DiffHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"diff-header\"\n      className={cn(\n        \"flex min-w-0 items-center gap-2 border-b px-3 py-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>svg]:text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DiffTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"diff-title\"\n      className={cn(\"min-w-0 truncate font-mono\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DiffStats({ className, ...props }: React.ComponentProps<\"span\">) {\n  const { added, removed } = useDiff()\n  return (\n    <span\n      data-slot=\"diff-stats\"\n      className={cn(\"ml-auto flex shrink-0 gap-2 font-mono\", className)}\n      {...props}\n    >\n      <span className=\"text-green-700 dark:text-green-200\">\n        +{added}\n        <span className=\"sr-only\"> lines added</span>\n      </span>\n      <span className=\"text-destructive-foreground\">\n        −{removed}\n        <span className=\"sr-only\"> lines removed</span>\n      </span>\n    </span>\n  )\n}\n\ntype CodeRow = {\n  key: string\n  kind: DiffLineKind\n  numbers: (number | undefined)[]\n  content: string\n  tokens?: Token[]\n  marks?: [number, number][]\n}\n\ntype PaneRow =\n  | CodeRow\n  | { key: string; kind: \"hunk\"; header: string }\n  | { key: string; kind: \"empty\" }\n  | { key: string; kind: \"folded\"; id: string; count: number; mirror?: boolean }\n  | { key: string; kind: \"actions\"; node?: React.ReactNode }\n\nconst rowHeight = {\n  context: \"h-5\",\n  added: \"h-5\",\n  removed: \"h-5\",\n  empty: \"h-5\",\n  hunk: \"h-5\",\n  folded: \"h-7\",\n  actions: \"h-8\",\n} as const\n\nconst rowTint = {\n  context: \"\",\n  added: \"bg-syntax-added\",\n  removed: \"bg-syntax-removed\",\n  empty: \"bg-fill-1\",\n  hunk: \"bg-fill-1 text-muted-foreground\",\n  folded: \"bg-fill-1 text-muted-foreground\",\n  actions: \"\",\n} as const\n\nconst numberColor = {\n  context: \"text-muted-foreground\",\n  added: \"text-green-700 dark:text-green-200\",\n  removed: \"text-destructive-foreground\",\n} as const\n\nconst wordMark = {\n  context: \"\",\n  added: \"bg-syntax-added-word\",\n  removed: \"bg-syntax-removed-word\",\n} as const\n\nconst rowRule = {\n  added: \"before:bg-green-600\",\n  removed:\n    \"before:bg-[repeating-linear-gradient(-45deg,var(--color-red-500)_0_2px,transparent_2px_4px)]\",\n} as const\n\nfunction Code({ row }: { row: CodeRow }) {\n  const pieces: Token[] = row.tokens ?? [{ content: row.content }]\n  const marks = row.marks ?? []\n  const out: (Token & { marked: boolean })[] = []\n  let at = 0\n  for (const piece of pieces) {\n    let start = 0\n    while (start < piece.content.length) {\n      const position = at + start\n      const mark = marks.find(([from, to]) => position >= from && position < to)\n      const next = mark\n        ? mark[1]\n        : Math.min(\n            ...marks.map(([from]) => from).filter((from) => from > position),\n            Infinity\n          )\n      const end = Math.min(piece.content.length, next - at)\n      out.push({\n        content: piece.content.slice(start, end),\n        color: piece.color,\n        marked: !!mark,\n      })\n      start = end\n    }\n    at += piece.content.length\n  }\n  return out.map((piece, index) => (\n    <span\n      key={index}\n      style={{ color: piece.color }}\n      className={piece.marked ? wordMark[row.kind] : undefined}\n    >\n      {piece.content}\n    </span>\n  ))\n}\n\nfunction indentOf(content: string) {\n  let columns = 0\n  for (const char of content) {\n    if (char === \" \") columns += 1\n    else if (char === \"\\t\") columns += 2\n    else return columns\n  }\n  return -1\n}\n\nfunction indentUnit(hunks: DiffHunk[]) {\n  let unit = Infinity\n  for (const hunk of hunks)\n    for (const line of hunk.lines) {\n      if (line.content.trimStart().startsWith(\"*\")) continue\n      const indent = indentOf(line.content)\n      if (indent > 0) unit = Math.min(unit, indent)\n    }\n  return Number.isFinite(unit) ? unit : 2\n}\n\nfunction guideLevels(rows: PaneRow[], unit: number) {\n  const levels = rows.map((row) =>\n    \"content\" in row ? Math.floor(indentOf(row.content) / unit) : -1\n  )\n  return rows.map((row, index) => {\n    if (!(\"content\" in row) || levels[index] >= 0)\n      return Math.max(levels[index], 0)\n    let before = 0\n    for (let i = index - 1; i >= 0; i--)\n      if (levels[i] >= 0) {\n        before = levels[i]\n        break\n      }\n    let after = 0\n    for (let i = index + 1; i < rows.length; i++)\n      if (levels[i] >= 0) {\n        after = levels[i]\n        break\n      }\n    return Math.min(before, after)\n  })\n}\n\nfunction Pane({\n  rows,\n  columns,\n  unit,\n  onUnfold,\n}: {\n  rows: PaneRow[]\n  columns: number\n  unit: number\n  onUnfold: (id: string) => void\n}) {\n  const guides = guideLevels(rows, unit)\n  return (\n    <div data-slot=\"diff-pane\" className=\"flex min-w-0\">\n      <div\n        aria-hidden\n        className=\"grid shrink-0 border-r\"\n        style={{ gridTemplateColumns: `repeat(${columns}, auto)` }}\n      >\n        {rows.map((row) => {\n          const kind = row.kind\n          return (\n            <div\n              key={row.key}\n              onClick={kind === \"folded\" ? () => onUnfold(row.id) : undefined}\n              className={cn(\n                \"relative col-span-full grid grid-cols-subgrid items-center\",\n                rowHeight[kind],\n                rowTint[kind],\n                kind === \"folded\" && \"cursor-pointer\",\n                (kind === \"added\" || kind === \"removed\") &&\n                  cn(\n                    \"before:absolute before:inset-y-0 before:left-0 before:w-0.75\",\n                    rowRule[kind]\n                  )\n              )}\n            >\n              {Array.from({ length: columns }, (_, column) => (\n                <span\n                  key={column}\n                  className={cn(\n                    \"flex min-w-8 justify-end px-2 tabular-nums\",\n                    \"numbers\" in row\n                      ? numberColor[row.kind]\n                      : \"text-muted-foreground\"\n                  )}\n                >\n                  {\"numbers\" in row ? (\n                    row.numbers[column]\n                  ) : kind === \"hunk\" && column === 0 ? (\n                    \"⋯\"\n                  ) : kind === \"folded\" && column === 0 ? (\n                    <UnfoldVerticalIcon className=\"size-3.5\" />\n                  ) : null}\n                </span>\n              ))}\n            </div>\n          )\n        })}\n      </div>\n      <div className=\"min-w-0 flex-1 overflow-x-auto\">\n        <div className=\"w-max min-w-full\">\n          {rows.map((row, index) => (\n            <div\n              key={row.key}\n              className={cn(\n                \"flex items-center pr-4 whitespace-pre\",\n                rowHeight[row.kind],\n                rowTint[row.kind]\n              )}\n            >\n              {row.kind === \"hunk\" ? (\n                <span className=\"pl-2\">{row.header}</span>\n              ) : row.kind === \"folded\" ? (\n                <button\n                  type=\"button\"\n                  aria-hidden={row.mirror || undefined}\n                  tabIndex={row.mirror ? -1 : undefined}\n                  onClick={(event) => {\n                    if (event.currentTarget.matches(\":focus\"))\n                      event.currentTarget\n                        .closest<HTMLElement>(\"[data-slot=diff-content]\")\n                        ?.focus({ preventScroll: true })\n                    onUnfold(row.id)\n                  }}\n                  className=\"sticky left-0 ml-2 rounded-sm px-0.5 font-sans text-2xs outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30\"\n                >\n                  Show {row.count} unchanged{\" \"}\n                  {row.count === 1 ? \"line\" : \"lines\"}\n                </button>\n              ) : row.kind === \"actions\" ? (\n                row.node !== undefined ? (\n                  <div\n                    data-slot=\"diff-change-actions\"\n                    className=\"sticky left-0 flex items-center gap-1 pl-6.5 font-sans\"\n                  >\n                    {row.node}\n                  </div>\n                ) : null\n              ) : row.kind === \"empty\" ? null : (\n                <>\n                  <span\n                    aria-hidden\n                    className={cn(\"w-4.5 shrink-0 pl-2\", numberColor[row.kind])}\n                  >\n                    {row.kind === \"added\"\n                      ? \"+\"\n                      : row.kind === \"removed\"\n                        ? \"-\"\n                        : \" \"}\n                  </span>\n                  {row.kind !== \"context\" && (\n                    <span className=\"sr-only\">\n                      {row.kind === \"added\" ? \"Added: \" : \"Removed: \"}\n                    </span>\n                  )}\n                  <span className=\"relative self-stretch pl-2 leading-5 [tab-size:2]\">\n                    {Array.from({ length: guides[index] }, (_, level) => (\n                      <span\n                        key={level}\n                        aria-hidden\n                        className=\"absolute inset-y-0 border-l border-border\"\n                        style={{\n                          left: `calc(var(--spacing) * 2 + ${level * unit}ch)`,\n                        }}\n                      />\n                    ))}\n                    <Code row={row} />\n                  </span>\n                </>\n              )}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n\ntype Item =\n  | { type: \"line\"; line: DiffLine }\n  | { type: \"hunk\"; id: string; header: string }\n  | { type: \"folded\"; id: string; count: number }\n\nfunction lineKey(line: DiffLine) {\n  return `l:${line.oldIndex ?? \"\"}:${line.newIndex ?? \"\"}`\n}\n\nfunction foldLines(\n  hunks: DiffHunk[],\n  foldable: boolean,\n  contextLines: number,\n  unfolded: ReadonlySet<string>\n) {\n  const items: Item[] = []\n  hunks.forEach((hunk, h) => {\n    if (h > 0) items.push({ type: \"hunk\", id: String(h), header: hunk.header })\n    const lines = hunk.lines\n    for (let i = 0; i < lines.length;) {\n      if (lines[i].kind !== \"context\" || !foldable) {\n        items.push({ type: \"line\", line: lines[i++] })\n        continue\n      }\n      let j = i\n      while (j < lines.length && lines[j].kind === \"context\") j++\n      const run = lines.slice(i, j)\n      const id = `${h}:${i}`\n      const keepStart = i === 0 ? 0 : contextLines\n      const keepEnd = j === lines.length ? 0 : contextLines\n      const hidden = run.length - keepStart - keepEnd\n      if (hidden > 1 && !unfolded.has(id)) {\n        for (const line of run.slice(0, keepStart))\n          items.push({ type: \"line\", line })\n        items.push({ type: \"folded\", id, count: hidden })\n        for (const line of run.slice(run.length - keepEnd))\n          items.push({ type: \"line\", line })\n      } else {\n        for (const line of run) items.push({ type: \"line\", line })\n      }\n      i = j\n    }\n  })\n  return items\n}\n\nconst noIds: ReadonlySet<string> = new Set()\n\nfunction DiffContent({\n  renderChangeActions,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /**\n   * Controls shown under each change, e.g. accept / reject buttons for an\n   * edit an agent proposes. Receives the change: its index, where it starts,\n   * the removed and the added lines.\n   */\n  renderChangeActions?: (change: DiffChange) => React.ReactNode\n}) {\n  const { hunks, changes, foldable, contextLines, view, lineNumbers, tokens } =\n    useDiff()\n  const [opened, setOpened] = React.useState<{\n    hunks: DiffHunk[]\n    ids: Set<string>\n  }>(() => ({ hunks, ids: new Set() }))\n  const unfolded = opened.hunks === hunks ? opened.ids : noIds\n  const onUnfold = (id: string) =>\n    setOpened((current) => ({\n      hunks,\n      ids: new Set(current.hunks === hunks ? current.ids : []).add(id),\n    }))\n  const unit = React.useMemo(() => indentUnit(hunks), [hunks])\n  const items = React.useMemo(\n    () => foldLines(hunks, foldable, contextLines, unfolded),\n    [hunks, foldable, contextLines, unfolded]\n  )\n\n  const codeRow = (\n    line: DiffLine,\n    numbers: (number | undefined)[]\n  ): CodeRow => ({\n    key: lineKey(line),\n    kind: line.kind,\n    numbers,\n    content: line.content,\n    marks: line.marks,\n    tokens:\n      line.kind === \"removed\"\n        ? line.oldIndex !== undefined\n          ? tokens?.old[line.oldIndex]\n          : undefined\n        : line.newIndex !== undefined\n          ? tokens?.new[line.newIndex]\n          : undefined,\n  })\n  const actionsAfter = (index: number) => {\n    const item = items[index]\n    if (item.type !== \"line\" || item.line.change === undefined) return undefined\n    const next = items[index + 1]\n    if (next?.type === \"line\" && next.line.change === item.line.change)\n      return undefined\n    const change = changes[item.line.change]\n    const node = renderChangeActions?.(change)\n    if (node == null || typeof node === \"boolean\") return undefined\n    return { key: `a:${change.index}`, node }\n  }\n  const pane = (rows: PaneRow[], columns: number) => (\n    <Pane rows={rows} columns={columns} unit={unit} onUnfold={onUnfold} />\n  )\n\n  if (view === \"split\") {\n    const left: PaneRow[] = []\n    const right: PaneRow[] = []\n    let removed: DiffLine[] = []\n    let added: DiffLine[] = []\n    const flush = () => {\n      for (let i = 0; i < Math.max(removed.length, added.length); i++) {\n        const before = removed[i]\n        const after = added[i]\n        left.push(\n          before\n            ? codeRow(before, [before.oldNumber])\n            : { key: `e:${lineKey(after)}`, kind: \"empty\" }\n        )\n        right.push(\n          after\n            ? codeRow(after, [after.newNumber])\n            : { key: `e:${lineKey(before)}`, kind: \"empty\" }\n        )\n      }\n      removed = []\n      added = []\n    }\n    items.forEach((item, index) => {\n      if (item.type === \"line\" && item.line.kind === \"removed\") {\n        if (added.length) flush()\n        removed.push(item.line)\n      } else if (item.type === \"line\" && item.line.kind === \"added\") {\n        added.push(item.line)\n      } else {\n        flush()\n        if (item.type === \"line\") {\n          left.push(codeRow(item.line, [item.line.oldNumber]))\n          right.push(codeRow(item.line, [item.line.newNumber]))\n        } else {\n          const row: PaneRow =\n            item.type === \"hunk\"\n              ? { key: `h:${item.id}`, kind: \"hunk\", header: item.header }\n              : {\n                  key: `f:${item.id}`,\n                  kind: \"folded\",\n                  id: item.id,\n                  count: item.count,\n                }\n          left.push(row.kind === \"folded\" ? { ...row, mirror: true } : row)\n          right.push(row)\n        }\n      }\n      const actions = actionsAfter(index)\n      if (actions) {\n        flush()\n        left.push({ key: actions.key, kind: \"actions\" })\n        right.push({ kind: \"actions\", ...actions })\n      }\n    })\n    flush()\n    return (\n      <div\n        data-slot=\"diff-content\"\n        tabIndex={-1}\n        className={cn(\n          \"grid grid-cols-2 divide-x py-1 font-mono outline-none\",\n          className\n        )}\n        {...props}\n      >\n        {pane(left, 1)}\n        {pane(right, 1)}\n      </div>\n    )\n  }\n\n  const rows: PaneRow[] = []\n  items.forEach((item, index) => {\n    if (item.type === \"hunk\")\n      rows.push({ key: `h:${item.id}`, kind: \"hunk\", header: item.header })\n    else if (item.type === \"folded\")\n      rows.push({\n        key: `f:${item.id}`,\n        kind: \"folded\",\n        id: item.id,\n        count: item.count,\n      })\n    else\n      rows.push(\n        codeRow(\n          item.line,\n          lineNumbers === \"single\"\n            ? [\n                item.line.kind === \"removed\"\n                  ? item.line.oldNumber\n                  : item.line.newNumber,\n              ]\n            : [item.line.oldNumber, item.line.newNumber]\n        )\n      )\n    const actions = actionsAfter(index)\n    if (actions) rows.push({ kind: \"actions\", ...actions })\n  })\n  return (\n    <div\n      data-slot=\"diff-content\"\n      tabIndex={-1}\n      className={cn(\"py-1 font-mono outline-none\", className)}\n      {...props}\n    >\n      {pane(rows, lineNumbers === \"single\" ? 1 : 2)}\n    </div>\n  )\n}\n\nexport { Diff, DiffHeader, DiffTitle, DiffStats, DiffContent, type DiffChange }\n","type":"registry:ui"}],"cssVars":{"theme":{"color-destructive-foreground":"var(--destructive-foreground)","color-fill-1":"var(--fill-1)","color-syntax-foreground":"var(--syntax-foreground)","color-syntax-comment":"var(--syntax-comment)","color-syntax-keyword":"var(--syntax-keyword)","color-syntax-string":"var(--syntax-string)","color-syntax-constant":"var(--syntax-constant)","color-syntax-function":"var(--syntax-function)","color-syntax-variable":"var(--syntax-variable)","color-syntax-tag":"var(--syntax-tag)","color-syntax-added":"var(--syntax-added)","color-syntax-removed":"var(--syntax-removed)","color-syntax-added-word":"var(--syntax-added-word)","color-syntax-removed-word":"var(--syntax-removed-word)","text-2xs--letter-spacing":"-0.005em","text-sm--letter-spacing":"-0.005em","text-2xs":"0.75rem","text-2xs--line-height":"calc(1 / 0.75)","shadow-card":"0 1px 1px -1px oklch(0.371 0 0 / 6%), 0 2px 2px -1px oklch(0.371 0 0 / 3%)"},"light":{"destructive-foreground":"oklch(0.505 0.213 27.518)","fill-1":"oklch(0.556 0 0 / 5%)","syntax-foreground":"oklch(0.145 0 0)","syntax-comment":"oklch(0.4975 0 0)","syntax-keyword":"oklch(0.577 0.245 27.325)","syntax-string":"oklch(0.282 0.091 267.935)","syntax-constant":"oklch(0.488 0.243 264.376)","syntax-function":"oklch(0.432 0.232 292.759)","syntax-variable":"oklch(0.666 0.179 58.318)","syntax-tag":"oklch(0.527 0.154 150.069)","syntax-added":"oklch(0.723 0.219 149.579 / 8%)","syntax-removed":"oklch(0.637 0.237 25.331 / 8%)","syntax-added-word":"oklch(0.723 0.219 149.579 / 20%)","syntax-removed-word":"oklch(0.637 0.237 25.331 / 20%)"},"dark":{"destructive-foreground":"oklch(0.704 0.191 22.216)","fill-1":"oklch(1 0 0 / 5%)","syntax-foreground":"oklch(0.985 0 0)","syntax-comment":"oklch(0.708 0 0)","syntax-keyword":"oklch(0.704 0.191 22.216)","syntax-string":"oklch(0.882 0.059 254.128)","syntax-constant":"oklch(0.707 0.165 254.624)","syntax-function":"oklch(0.702 0.183 293.541)","syntax-variable":"oklch(0.837 0.128 66.29)","syntax-tag":"oklch(0.871 0.15 154.449)","syntax-added":"oklch(0.723 0.219 149.579 / 15%)","syntax-removed":"oklch(0.637 0.237 25.331 / 10%)","syntax-added-word":"oklch(0.723 0.219 149.579 / 30%)","syntax-removed-word":"oklch(0.637 0.237 25.331 / 30%)"}},"categories":["content"],"type":"registry:ui"}