{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-viewer",
  "type": "registry:component",
  "title": "Diff Viewer",
  "description": "A diff viewer component",
  "dependencies": [
    "refractor",
    "lucide-react",
    "gitdiff-parser",
    "diff"
  ],
  "registryDependencies": [
    "button",
    "https://ui.fredrika.dev/r/collapsible-card.json"
  ],
  "files": [
    {
      "path": "registry/ui/diff/index.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport { refractor } from \"refractor/all\";\nimport \"./theme.css\";\nimport { ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  guessLang,\n  Hunk as HunkType,\n  SkipBlock,\n  File,\n  Line as LineType,\n} from \"./utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                                — Context —                                 */\n/* -------------------------------------------------------------------------- */\n\ninterface DiffContextValue {\n  language: string;\n}\n\nconst DiffContext = React.createContext<DiffContextValue | null>(null);\n\nfunction useDiffContext() {\n  const context = React.useContext(DiffContext);\n  if (!context) {\n    throw new Error(\"useDiffContext must be used within a Diff component\");\n  }\n  return context;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                — Helpers —                                 */\n/* -------------------------------------------------------------------------- */\n\nfunction hastToReact(\n  node: ReturnType<typeof refractor.highlight>[\"children\"][number],\n  key: string\n): React.ReactNode {\n  if (node.type === \"text\") return node.value;\n  if (node.type === \"element\") {\n    const { tagName, properties, children } = node;\n    return React.createElement(\n      tagName,\n      {\n        key,\n        className: (properties.className as string[] | undefined)?.join(\" \"),\n      },\n      children.map((c, i) => hastToReact(c, `${key}-${i}`))\n    );\n  }\n  return null;\n}\n\nfunction highlight(code: string, lang: string): React.ReactNode[] {\n  const id = `${lang}:${code}`;\n  const tree = refractor.highlight(code, lang);\n  const nodes = tree.children.map((c, i) => hastToReact(c, `${id}-${i}`));\n  return nodes;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               — Root —                                     */\n/* -------------------------------------------------------------------------- */\nexport interface DiffSelectionRange {\n  startLine: number;\n  endLine: number;\n}\n\nexport interface DiffProps\n  extends React.TableHTMLAttributes<HTMLTableElement>,\n    Pick<File, \"hunks\" | \"type\"> {\n  fileName?: string;\n  language?: string;\n}\n\nexport const Hunk = ({ hunk }: { hunk: HunkType | SkipBlock }) => {\n  return hunk.type === \"hunk\" ? (\n    <>\n      {hunk.lines.map((line, index) => (\n        <Line key={index} line={line} />\n      ))}\n    </>\n  ) : (\n    <SkipBlockRow lines={hunk.count} content={hunk.content} />\n  );\n};\n\nexport const Diff: React.FC<DiffProps> = ({\n  fileName,\n  language = guessLang(fileName),\n  hunks,\n  className,\n  children,\n  ...props\n}) => {\n  return (\n    <DiffContext.Provider value={{ language }}>\n      <table\n        {...props}\n        className={cn(\n          \"[--code-added:var(--color-green-500)] [--code-removed:var(--color-orange-600)] font-mono text-[0.8rem] w-full m-0 border-separate border-0 outline-none overflow-x-auto border-spacing-0\",\n          className\n        )}\n      >\n        <tbody className=\"w-full box-border\">\n          {children ??\n            hunks.map((hunk, index) => <Hunk key={index} hunk={hunk} />)}\n        </tbody>\n      </table>\n    </DiffContext.Provider>\n  );\n};\n\nconst SkipBlockRow: React.FC<{\n  lines: number;\n  content?: string;\n}> = ({ lines, content }) => (\n  <>\n    <tr className=\"h-4\" />\n    <tr className={cn(\"h-10 font-mono bg-muted text-muted-foreground\")}>\n      <td />\n      <td className=\"opacity-50 select-none\">\n        <ChevronsUpDown className=\"size-4 mx-auto\" />\n      </td>\n      <td>\n        <span className=\"px-0 sticky left-2 italic opacity-50\">\n          {content || `${lines} lines hidden`}\n        </span>\n      </td>\n    </tr>\n    <tr className=\"h-4\" />\n  </>\n);\n\nconst Line: React.FC<{\n  line: LineType;\n}> = ({ line }) => {\n  const { language } = useDiffContext();\n  const Tag =\n    line.type === \"insert\" ? \"ins\" : line.type === \"delete\" ? \"del\" : \"span\";\n  const lineNumberNew =\n    line.type === \"normal\" ? line.newLineNumber : line.lineNumber;\n  const lineNumberOld = line.type === \"normal\" ? line.oldLineNumber : undefined;\n\n  return (\n    <tr\n      data-line-new={lineNumberNew ?? undefined}\n      data-line-old={lineNumberOld ?? undefined}\n      data-line-kind={line.type}\n      className={cn(\"whitespace-pre-wrap box-border border-none h-5 min-h-5\", {\n        \"bg-[var(--code-added)]/10\": line.type === \"insert\",\n        \"bg-[var(--code-removed)]/10\": line.type === \"delete\",\n      })}\n    >\n      <td\n        className={cn(\"border-transparent w-1 border-l-3\", {\n          \"border-[color:var(--code-added)]/60\": line.type === \"insert\",\n          \"border-[color:var(--code-removed)]/80\": line.type === \"delete\",\n        })}\n      />\n      <td className=\"tabular-nums text-center opacity-50 px-2 text-xs select-none\">\n        {line.type === \"delete\" ? \"–\" : lineNumberNew}\n      </td>\n      <td className=\"text-nowrap pr-6\">\n        <Tag>\n          {line.content.map((seg, i) => (\n            <span\n              key={i}\n              className={cn({\n                \"bg-[var(--code-added)]/20\": seg.type === \"insert\",\n                \"bg-[var(--code-removed)]/20\": seg.type === \"delete\",\n              })}\n            >\n              {highlight(seg.value, language).map((n, idx) => (\n                <React.Fragment key={idx}>{n}</React.Fragment>\n              ))}\n            </span>\n          ))}\n        </Tag>\n      </td>\n    </tr>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/ui/diff/index.tsx"
    },
    {
      "path": "registry/ui/diff/theme.css",
      "content": ":root {\n  /**\n * One Light theme for prism.js\n * Based on Atom's One Light theme: https://github.com/atom/atom/tree/master/packages/one-light-syntax\n */\n\n  /**\n * One Light colours (accurate as of commit eb064bf on 19 Feb 2021)\n * From colors.less\n * --mono-1: hsl(230, 8%, 24%);\n * --mono-2: hsl(230, 6%, 44%);\n * --mono-3: hsl(230, 4%, 64%)\n * --hue-1: hsl(198, 99%, 37%);\n * --hue-2: hsl(221, 87%, 60%);\n * --hue-3: hsl(301, 63%, 40%);\n * --hue-4: hsl(119, 34%, 47%);\n * --hue-5: hsl(5, 74%, 59%);\n * --hue-5-2: hsl(344, 84%, 43%);\n * --hue-6: hsl(35, 99%, 36%);\n * --hue-6-2: hsl(35, 99%, 40%);\n * --syntax-fg: hsl(230, 8%, 24%);\n * --syntax-bg: hsl(230, 1%, 98%);\n * --syntax-gutter: hsl(230, 1%, 62%);\n * --syntax-guide: hsla(230, 8%, 24%, 0.2);\n * --syntax-accent: hsl(230, 100%, 66%);\n * From syntax-variables.less\n * --syntax-selection-color: hsl(230, 1%, 90%);\n * --syntax-gutter-background-color-selected: hsl(230, 1%, 90%);\n * --syntax-cursor-line: hsla(230, 8%, 24%, 0.05);\n */\n\n  code[class*=\"language-\"],\n  pre[class*=\"language-\"] {\n    background: hsl(230, 1%, 98%);\n    color: hsl(230, 8%, 24%);\n    font-family:\n      \"Fira Code\", \"Fira Mono\", Menlo, Consolas, \"DejaVu Sans Mono\", monospace;\n    direction: ltr;\n    text-align: left;\n    white-space: pre;\n    word-spacing: normal;\n    word-break: normal;\n    line-height: 1.5;\n    -moz-tab-size: 2;\n    -o-tab-size: 2;\n    tab-size: 2;\n    -webkit-hyphens: none;\n    -moz-hyphens: none;\n    -ms-hyphens: none;\n    hyphens: none;\n  }\n\n  /* Selection */\n  code[class*=\"language-\"]::-moz-selection,\n  code[class*=\"language-\"] *::-moz-selection,\n  pre[class*=\"language-\"] *::-moz-selection {\n    background: hsl(230, 1%, 90%);\n    color: inherit;\n  }\n\n  code[class*=\"language-\"]::selection,\n  code[class*=\"language-\"] *::selection,\n  pre[class*=\"language-\"] *::selection {\n    background: hsl(230, 1%, 90%);\n    color: inherit;\n  }\n\n  /* Code blocks */\n  pre[class*=\"language-\"] {\n    padding: 1em;\n    margin: 0.5em 0;\n    overflow: auto;\n    border-radius: 0.3em;\n  }\n\n  /* Inline code */\n  :not(pre) > code[class*=\"language-\"] {\n    padding: 0.2em 0.3em;\n    border-radius: 0.3em;\n    white-space: normal;\n  }\n\n  .token.comment,\n  .token.prolog,\n  .token.cdata {\n    color: hsl(230, 4%, 64%);\n  }\n\n  .token.doctype,\n  .token.punctuation,\n  .token.entity {\n    color: hsl(230, 8%, 24%);\n  }\n\n  .token.attr-name,\n  .token.class-name,\n  .token.boolean,\n  .token.constant,\n  .token.number,\n  .token.atrule {\n    color: hsl(35, 99%, 36%);\n  }\n\n  .token.keyword {\n    color: hsl(301, 63%, 40%);\n  }\n\n  .token.property,\n  .token.tag,\n  .token.symbol,\n  .token.deleted,\n  .token.important {\n    color: hsl(5, 74%, 59%);\n  }\n\n  .token.selector,\n  .token.string,\n  .token.char,\n  .token.builtin,\n  .token.inserted,\n  .token.regex,\n  .token.attr-value,\n  .token.attr-value > .token.punctuation {\n    color: hsl(119, 34%, 47%);\n  }\n\n  ins,\n  del {\n    text-decoration: none;\n  }\n\n  .token.variable,\n  .token.operator,\n  .token.function {\n    color: hsl(221, 87%, 60%);\n  }\n\n  .token.url {\n    color: hsl(198, 99%, 37%);\n  }\n\n  /* HTML overrides */\n  .token.attr-value > .token.punctuation.attr-equals,\n  .token.special-attr > .token.attr-value > .token.value.css {\n    color: hsl(230, 8%, 24%);\n  }\n\n  /* CSS overrides */\n  .language-css .token.selector {\n    color: hsl(5, 74%, 59%);\n  }\n\n  .language-css .token.property {\n    color: hsl(230, 8%, 24%);\n  }\n\n  .language-css .token.function,\n  .language-css .token.url > .token.function {\n    color: hsl(198, 99%, 37%);\n  }\n\n  .language-css .token.url > .token.string.url {\n    color: hsl(119, 34%, 47%);\n  }\n\n  .language-css .token.important,\n  .language-css .token.atrule .token.rule {\n    color: hsl(301, 63%, 40%);\n  }\n\n  /* JS overrides */\n  .language-javascript .token.operator {\n    color: hsl(301, 63%, 40%);\n  }\n\n  .language-javascript\n    .token.template-string\n    > .token.interpolation\n    > .token.interpolation-punctuation.punctuation {\n    color: hsl(344, 84%, 43%);\n  }\n\n  /* JSON overrides */\n  .language-json .token.operator {\n    color: hsl(230, 8%, 24%);\n  }\n\n  .language-json .token.null.keyword {\n    color: hsl(35, 99%, 36%);\n  }\n\n  /* MD overrides */\n  .language-markdown .token.url,\n  .language-markdown .token.url > .token.operator,\n  .language-markdown .token.url-reference.url > .token.string {\n    color: hsl(230, 8%, 24%);\n  }\n\n  .language-markdown .token.url > .token.content {\n    color: hsl(221, 87%, 60%);\n  }\n\n  .language-markdown .token.url > .token.url,\n  .language-markdown .token.url-reference.url {\n    color: hsl(198, 99%, 37%);\n  }\n\n  .language-markdown .token.blockquote.punctuation,\n  .language-markdown .token.hr.punctuation {\n    color: hsl(230, 4%, 64%);\n    font-style: italic;\n  }\n\n  .language-markdown .token.code-snippet {\n    color: hsl(119, 34%, 47%);\n  }\n\n  .language-markdown .token.bold .token.content {\n    color: hsl(35, 99%, 36%);\n  }\n\n  .language-markdown .token.italic .token.content {\n    color: hsl(301, 63%, 40%);\n  }\n\n  .language-markdown .token.strike .token.content,\n  .language-markdown .token.strike .token.punctuation,\n  .language-markdown .token.list.punctuation,\n  .language-markdown .token.title.important > .token.punctuation {\n    color: hsl(5, 74%, 59%);\n  }\n\n  /* General */\n  .token.bold {\n    font-weight: bold;\n  }\n\n  .token.comment,\n  .token.italic {\n    font-style: italic;\n  }\n\n  .token.entity {\n    cursor: help;\n  }\n\n  .token.namespace {\n    opacity: 0.8;\n  }\n\n  /* Plugin overrides */\n  /* Selectors should have higher specificity than those in the plugins' default stylesheets */\n\n  /* Show Invisibles plugin overrides */\n  .token.token.tab:not(:empty):before,\n  .token.token.cr:before,\n  .token.token.lf:before,\n  .token.token.space:before {\n    color: hsla(230, 8%, 24%, 0.2);\n  }\n\n  /* Toolbar plugin overrides */\n  /* Space out all buttons and move them away from the right edge of the code block */\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item {\n    margin-right: 0.4em;\n  }\n\n  /* Styling the buttons */\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > button,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > a,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > span {\n    background: hsl(230, 1%, 90%);\n    color: hsl(230, 6%, 44%);\n    padding: 0.1em 0.4em;\n    border-radius: 0.3em;\n  }\n\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus {\n    background: hsl(230, 1%, 78%); /* custom: darken(--syntax-bg, 20%) */\n    color: hsl(230, 8%, 24%);\n  }\n\n  /* Line Highlight plugin overrides */\n  /* The highlighted line itself */\n  .line-highlight.line-highlight {\n    background: hsla(230, 8%, 24%, 0.05);\n  }\n\n  /* Default line numbers in Line Highlight plugin */\n  .line-highlight.line-highlight:before,\n  .line-highlight.line-highlight[data-end]:after {\n    background: hsl(230, 1%, 90%);\n    color: hsl(230, 8%, 24%);\n    padding: 0.1em 0.6em;\n    border-radius: 0.3em;\n    box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */\n  }\n\n  /* Hovering over a linkable line number (in the gutter area) */\n  /* Requires Line Numbers plugin as well */\n  pre[id].linkable-line-numbers.linkable-line-numbers\n    span.line-numbers-rows\n    > span:hover:before {\n    background-color: hsla(230, 8%, 24%, 0.05);\n  }\n\n  /* Line Numbers and Command Line plugins overrides */\n  /* Line separating gutter from coding area */\n  .line-numbers.line-numbers .line-numbers-rows,\n  .command-line .command-line-prompt {\n    border-right-color: hsla(230, 8%, 24%, 0.2);\n  }\n\n  /* Stuff in the gutter */\n  .line-numbers .line-numbers-rows > span:before,\n  .command-line .command-line-prompt > span:before {\n    color: hsl(230, 1%, 62%);\n  }\n\n  /* Match Braces plugin overrides */\n  /* Note: Outline colour is inherited from the braces */\n  .rainbow-braces .token.token.punctuation.brace-level-1,\n  .rainbow-braces .token.token.punctuation.brace-level-5,\n  .rainbow-braces .token.token.punctuation.brace-level-9 {\n    color: hsl(5, 74%, 59%);\n  }\n\n  .rainbow-braces .token.token.punctuation.brace-level-2,\n  .rainbow-braces .token.token.punctuation.brace-level-6,\n  .rainbow-braces .token.token.punctuation.brace-level-10 {\n    color: hsl(119, 34%, 47%);\n  }\n\n  .rainbow-braces .token.token.punctuation.brace-level-3,\n  .rainbow-braces .token.token.punctuation.brace-level-7,\n  .rainbow-braces .token.token.punctuation.brace-level-11 {\n    color: hsl(221, 87%, 60%);\n  }\n\n  .rainbow-braces .token.token.punctuation.brace-level-4,\n  .rainbow-braces .token.token.punctuation.brace-level-8,\n  .rainbow-braces .token.token.punctuation.brace-level-12 {\n    color: hsl(301, 63%, 40%);\n  }\n\n  /* Diff Highlight plugin overrides */\n  /* Taken from https://github.com/atom/github/blob/master/styles/variables.less */\n  pre.diff-highlight > code .token.token.deleted:not(.prefix),\n  pre > code.diff-highlight .token.token.deleted:not(.prefix) {\n    background-color: hsla(353, 100%, 66%, 0.15);\n  }\n\n  pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection,\n  pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection,\n  pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection,\n  pre\n    > code.diff-highlight\n    .token.token.deleted:not(.prefix)\n    *::-moz-selection {\n    background-color: hsla(353, 95%, 66%, 0.25);\n  }\n\n  pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection,\n  pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection,\n  pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection,\n  pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection {\n    background-color: hsla(353, 95%, 66%, 0.25);\n  }\n\n  pre.diff-highlight > code .token.token.inserted:not(.prefix),\n  pre > code.diff-highlight .token.token.inserted:not(.prefix) {\n    background-color: hsla(137, 100%, 55%, 0.15);\n  }\n\n  pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection,\n  pre.diff-highlight\n    > code\n    .token.token.inserted:not(.prefix)\n    *::-moz-selection,\n  pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection,\n  pre\n    > code.diff-highlight\n    .token.token.inserted:not(.prefix)\n    *::-moz-selection {\n    background-color: hsla(135, 73%, 55%, 0.25);\n  }\n\n  pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection,\n  pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection,\n  pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection,\n  pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection {\n    background-color: hsla(135, 73%, 55%, 0.25);\n  }\n\n  /* Previewers plugin overrides */\n  /* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-light-ui */\n  /* Border around popup */\n  .prism-previewer.prism-previewer:before,\n  .prism-previewer-gradient.prism-previewer-gradient div {\n    border-color: hsl(0, 0, 95%);\n  }\n\n  /* Angle and time should remain as circles and are hence not included */\n  .prism-previewer-color.prism-previewer-color:before,\n  .prism-previewer-gradient.prism-previewer-gradient div,\n  .prism-previewer-easing.prism-previewer-easing:before {\n    border-radius: 0.3em;\n  }\n\n  /* Triangles pointing to the code */\n  .prism-previewer.prism-previewer:after {\n    border-top-color: hsl(0, 0, 95%);\n  }\n\n  .prism-previewer-flipped.prism-previewer-flipped.after {\n    border-bottom-color: hsl(0, 0, 95%);\n  }\n\n  /* Background colour within the popup */\n  .prism-previewer-angle.prism-previewer-angle:before,\n  .prism-previewer-time.prism-previewer-time:before,\n  .prism-previewer-easing.prism-previewer-easing {\n    background: hsl(0, 0%, 100%);\n  }\n\n  /* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */\n  /* For time, this is the alternate colour */\n  .prism-previewer-angle.prism-previewer-angle circle,\n  .prism-previewer-time.prism-previewer-time circle {\n    stroke: hsl(230, 8%, 24%);\n    stroke-opacity: 1;\n  }\n\n  /* Stroke colours of the handle, direction point, and vector itself */\n  .prism-previewer-easing.prism-previewer-easing circle,\n  .prism-previewer-easing.prism-previewer-easing path,\n  .prism-previewer-easing.prism-previewer-easing line {\n    stroke: hsl(230, 8%, 24%);\n  }\n\n  /* Fill colour of the handle */\n  .prism-previewer-easing.prism-previewer-easing circle {\n    fill: transparent;\n  }\n}\n\n.dark {\n  /**\n * One Dark theme for prism.js\n * Based on Atom's One Dark theme: https://github.com/atom/atom/tree/master/packages/one-dark-syntax\n */\n\n  /**\n * One Dark colours (accurate as of commit 8ae45ca on 6 Sep 2018)\n * From colors.less\n * --mono-1: hsl(220, 14%, 71%);\n * --mono-2: hsl(220, 9%, 55%);\n * --mono-3: hsl(220, 10%, 40%);\n * --hue-1: hsl(187, 47%, 55%);\n * --hue-2: hsl(207, 82%, 66%);\n * --hue-3: hsl(286, 60%, 67%);\n * --hue-4: hsl(95, 38%, 62%);\n * --hue-5: hsl(355, 65%, 65%);\n * --hue-5-2: hsl(5, 48%, 51%);\n * --hue-6: hsl(29, 54%, 61%);\n * --hue-6-2: hsl(39, 67%, 69%);\n * --syntax-fg: hsl(220, 14%, 71%);\n * --syntax-bg: hsl(220, 13%, 18%);\n * --syntax-gutter: hsl(220, 14%, 45%);\n * --syntax-guide: hsla(220, 14%, 71%, 0.15);\n * --syntax-accent: hsl(220, 100%, 66%);\n * From syntax-variables.less\n * --syntax-selection-color: hsl(220, 13%, 28%);\n * --syntax-gutter-background-color-selected: hsl(220, 13%, 26%);\n * --syntax-cursor-line: hsla(220, 100%, 80%, 0.04);\n */\n\n  code[class*=\"language-\"],\n  pre[class*=\"language-\"] {\n    background: hsl(220, 13%, 18%);\n    color: hsl(220, 14%, 71%);\n    text-shadow: 0 1px rgba(0, 0, 0, 0.3);\n    font-family:\n      \"Fira Code\", \"Fira Mono\", Menlo, Consolas, \"DejaVu Sans Mono\", monospace;\n    direction: ltr;\n    text-align: left;\n    white-space: pre;\n    word-spacing: normal;\n    word-break: normal;\n    line-height: 1.5;\n    -moz-tab-size: 2;\n    -o-tab-size: 2;\n    tab-size: 2;\n    -webkit-hyphens: none;\n    -moz-hyphens: none;\n    -ms-hyphens: none;\n    hyphens: none;\n  }\n\n  /* Selection */\n  code[class*=\"language-\"]::-moz-selection,\n  code[class*=\"language-\"] *::-moz-selection,\n  pre[class*=\"language-\"] *::-moz-selection {\n    background: hsl(220, 13%, 28%);\n    color: inherit;\n    text-shadow: none;\n  }\n\n  code[class*=\"language-\"]::selection,\n  code[class*=\"language-\"] *::selection,\n  pre[class*=\"language-\"] *::selection {\n    background: hsl(220, 13%, 28%);\n    color: inherit;\n    text-shadow: none;\n  }\n\n  /* Code blocks */\n  pre[class*=\"language-\"] {\n    padding: 1em;\n    margin: 0.5em 0;\n    overflow: auto;\n    border-radius: 0.3em;\n  }\n\n  /* Inline code */\n  :not(pre) > code[class*=\"language-\"] {\n    padding: 0.2em 0.3em;\n    border-radius: 0.3em;\n    white-space: normal;\n  }\n\n  /* Print */\n  @media print {\n    code[class*=\"language-\"],\n    pre[class*=\"language-\"] {\n      text-shadow: none;\n    }\n  }\n\n  .token.comment,\n  .token.prolog,\n  .token.cdata {\n    color: hsl(220, 10%, 40%);\n  }\n\n  .token.doctype,\n  .token.punctuation,\n  .token.entity {\n    color: hsl(220, 14%, 71%);\n  }\n\n  .token.attr-name,\n  .token.class-name,\n  .token.boolean,\n  .token.constant,\n  .token.number,\n  .token.atrule {\n    color: hsl(29, 54%, 61%);\n  }\n\n  .token.keyword {\n    color: hsl(286, 60%, 67%);\n  }\n\n  .token.property,\n  .token.tag,\n  .token.symbol,\n  .token.deleted,\n  .token.important {\n    color: hsl(355, 65%, 65%);\n  }\n\n  .token.selector,\n  .token.string,\n  .token.char,\n  .token.builtin,\n  .token.inserted,\n  .token.regex,\n  .token.attr-value,\n  .token.attr-value > .token.punctuation {\n    color: hsl(95, 38%, 62%);\n  }\n\n  .token.variable,\n  .token.operator,\n  .token.function {\n    color: hsl(207, 82%, 66%);\n  }\n\n  .token.url {\n    color: hsl(187, 47%, 55%);\n  }\n\n  /* HTML overrides */\n  .token.attr-value > .token.punctuation.attr-equals,\n  .token.special-attr > .token.attr-value > .token.value.css {\n    color: hsl(220, 14%, 71%);\n  }\n\n  /* CSS overrides */\n  .language-css .token.selector {\n    color: hsl(355, 65%, 65%);\n  }\n\n  .language-css .token.property {\n    color: hsl(220, 14%, 71%);\n  }\n\n  .language-css .token.function,\n  .language-css .token.url > .token.function {\n    color: hsl(187, 47%, 55%);\n  }\n\n  .language-css .token.url > .token.string.url {\n    color: hsl(95, 38%, 62%);\n  }\n\n  .language-css .token.important,\n  .language-css .token.atrule .token.rule {\n    color: hsl(286, 60%, 67%);\n  }\n\n  /* JS overrides */\n  .language-javascript .token.operator {\n    color: hsl(286, 60%, 67%);\n  }\n\n  .language-javascript\n    .token.template-string\n    > .token.interpolation\n    > .token.interpolation-punctuation.punctuation {\n    color: hsl(5, 48%, 51%);\n  }\n\n  /* JSON overrides */\n  .language-json .token.operator {\n    color: hsl(220, 14%, 71%);\n  }\n\n  .language-json .token.null.keyword {\n    color: hsl(29, 54%, 61%);\n  }\n\n  /* MD overrides */\n  .language-markdown .token.url,\n  .language-markdown .token.url > .token.operator,\n  .language-markdown .token.url-reference.url > .token.string {\n    color: hsl(220, 14%, 71%);\n  }\n\n  .language-markdown .token.url > .token.content {\n    color: hsl(207, 82%, 66%);\n  }\n\n  .language-markdown .token.url > .token.url,\n  .language-markdown .token.url-reference.url {\n    color: hsl(187, 47%, 55%);\n  }\n\n  .language-markdown .token.blockquote.punctuation,\n  .language-markdown .token.hr.punctuation {\n    color: hsl(220, 10%, 40%);\n    font-style: italic;\n  }\n\n  .language-markdown .token.code-snippet {\n    color: hsl(95, 38%, 62%);\n  }\n\n  .language-markdown .token.bold .token.content {\n    color: hsl(29, 54%, 61%);\n  }\n\n  .language-markdown .token.italic .token.content {\n    color: hsl(286, 60%, 67%);\n  }\n\n  .language-markdown .token.strike .token.content,\n  .language-markdown .token.strike .token.punctuation,\n  .language-markdown .token.list.punctuation,\n  .language-markdown .token.title.important > .token.punctuation {\n    color: hsl(355, 65%, 65%);\n  }\n\n  /* General */\n  .token.bold {\n    font-weight: bold;\n  }\n\n  .token.comment,\n  .token.italic {\n    font-style: italic;\n  }\n\n  .token.entity {\n    cursor: help;\n  }\n\n  .token.namespace {\n    opacity: 0.8;\n  }\n\n  /* Plugin overrides */\n  /* Selectors should have higher specificity than those in the plugins' default stylesheets */\n\n  /* Show Invisibles plugin overrides */\n  .token.token.tab:not(:empty):before,\n  .token.token.cr:before,\n  .token.token.lf:before,\n  .token.token.space:before {\n    color: hsla(220, 14%, 71%, 0.15);\n    text-shadow: none;\n  }\n\n  /* Toolbar plugin overrides */\n  /* Space out all buttons and move them away from the right edge of the code block */\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item {\n    margin-right: 0.4em;\n  }\n\n  /* Styling the buttons */\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > button,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > a,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > span {\n    background: hsl(220, 13%, 26%);\n    color: hsl(220, 9%, 55%);\n    padding: 0.1em 0.4em;\n    border-radius: 0.3em;\n  }\n\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover,\n  div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus {\n    background: hsl(220, 13%, 28%);\n    color: hsl(220, 14%, 71%);\n  }\n\n  /* Line Highlight plugin overrides */\n  /* The highlighted line itself */\n  .line-highlight.line-highlight {\n    background: hsla(220, 100%, 80%, 0.04);\n  }\n\n  /* Default line numbers in Line Highlight plugin */\n  .line-highlight.line-highlight:before,\n  .line-highlight.line-highlight[data-end]:after {\n    background: hsl(220, 13%, 26%);\n    color: hsl(220, 14%, 71%);\n    padding: 0.1em 0.6em;\n    border-radius: 0.3em;\n    box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */\n  }\n\n  /* Hovering over a linkable line number (in the gutter area) */\n  /* Requires Line Numbers plugin as well */\n  pre[id].linkable-line-numbers.linkable-line-numbers\n    span.line-numbers-rows\n    > span:hover:before {\n    background-color: hsla(220, 100%, 80%, 0.04);\n  }\n\n  /* Line Numbers and Command Line plugins overrides */\n  /* Line separating gutter from coding area */\n  .line-numbers.line-numbers .line-numbers-rows,\n  .command-line .command-line-prompt {\n    border-right-color: hsla(220, 14%, 71%, 0.15);\n  }\n\n  /* Stuff in the gutter */\n  .line-numbers .line-numbers-rows > span:before,\n  .command-line .command-line-prompt > span:before {\n    color: hsl(220, 14%, 45%);\n  }\n\n  /* Match Braces plugin overrides */\n  /* Note: Outline colour is inherited from the braces */\n  .rainbow-braces .token.token.punctuation.brace-level-1,\n  .rainbow-braces .token.token.punctuation.brace-level-5,\n  .rainbow-braces .token.token.punctuation.brace-level-9 {\n    color: hsl(355, 65%, 65%);\n  }\n\n  .rainbow-braces .token.token.punctuation.brace-level-2,\n  .rainbow-braces .token.token.punctuation.brace-level-6,\n  .rainbow-braces .token.token.punctuation.brace-level-10 {\n    color: hsl(95, 38%, 62%);\n  }\n\n  .rainbow-braces .token.token.punctuation.brace-level-3,\n  .rainbow-braces .token.token.punctuation.brace-level-7,\n  .rainbow-braces .token.token.punctuation.brace-level-11 {\n    color: hsl(207, 82%, 66%);\n  }\n\n  .rainbow-braces .token.token.punctuation.brace-level-4,\n  .rainbow-braces .token.token.punctuation.brace-level-8,\n  .rainbow-braces .token.token.punctuation.brace-level-12 {\n    color: hsl(286, 60%, 67%);\n  }\n\n  /* Diff Highlight plugin overrides */\n  /* Taken from https://github.com/atom/github/blob/master/styles/variables.less */\n  pre.diff-highlight > code .token.token.deleted:not(.prefix),\n  pre > code.diff-highlight .token.token.deleted:not(.prefix) {\n    background-color: hsla(353, 100%, 66%, 0.15);\n  }\n\n  pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection,\n  pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection,\n  pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection,\n  pre\n    > code.diff-highlight\n    .token.token.deleted:not(.prefix)\n    *::-moz-selection {\n    background-color: hsla(353, 95%, 66%, 0.25);\n  }\n\n  pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection,\n  pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection,\n  pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection,\n  pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection {\n    background-color: hsla(353, 95%, 66%, 0.25);\n  }\n\n  pre.diff-highlight > code .token.token.inserted:not(.prefix),\n  pre > code.diff-highlight .token.token.inserted:not(.prefix) {\n    background-color: hsla(137, 100%, 55%, 0.15);\n  }\n\n  pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection,\n  pre.diff-highlight\n    > code\n    .token.token.inserted:not(.prefix)\n    *::-moz-selection,\n  pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection,\n  pre\n    > code.diff-highlight\n    .token.token.inserted:not(.prefix)\n    *::-moz-selection {\n    background-color: hsla(135, 73%, 55%, 0.25);\n  }\n\n  pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection,\n  pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection,\n  pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection,\n  pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection {\n    background-color: hsla(135, 73%, 55%, 0.25);\n  }\n\n  /* Previewers plugin overrides */\n  /* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-dark-ui */\n  /* Border around popup */\n  .prism-previewer.prism-previewer:before,\n  .prism-previewer-gradient.prism-previewer-gradient div {\n    border-color: hsl(224, 13%, 17%);\n  }\n\n  /* Angle and time should remain as circles and are hence not included */\n  .prism-previewer-color.prism-previewer-color:before,\n  .prism-previewer-gradient.prism-previewer-gradient div,\n  .prism-previewer-easing.prism-previewer-easing:before {\n    border-radius: 0.3em;\n  }\n\n  /* Triangles pointing to the code */\n  .prism-previewer.prism-previewer:after {\n    border-top-color: hsl(224, 13%, 17%);\n  }\n\n  .prism-previewer-flipped.prism-previewer-flipped.after {\n    border-bottom-color: hsl(224, 13%, 17%);\n  }\n\n  /* Background colour within the popup */\n  .prism-previewer-angle.prism-previewer-angle:before,\n  .prism-previewer-time.prism-previewer-time:before,\n  .prism-previewer-easing.prism-previewer-easing {\n    background: hsl(219, 13%, 22%);\n  }\n\n  /* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */\n  /* For time, this is the alternate colour */\n  .prism-previewer-angle.prism-previewer-angle circle,\n  .prism-previewer-time.prism-previewer-time circle {\n    stroke: hsl(220, 14%, 71%);\n    stroke-opacity: 1;\n  }\n\n  /* Stroke colours of the handle, direction point, and vector itself */\n  .prism-previewer-easing.prism-previewer-easing circle,\n  .prism-previewer-easing.prism-previewer-easing path,\n  .prism-previewer-easing.prism-previewer-easing line {\n    stroke: hsl(220, 14%, 71%);\n  }\n\n  /* Fill colour of the handle */\n  .prism-previewer-easing.prism-previewer-easing circle {\n    fill: transparent;\n  }\n}\n\ntr[data-comment-highlight=\"true\"] {\n  @apply !bg-[var(--color-yellow)]/20;\n  transition: background-color 150ms ease;\n}\n\n.dark tr[data-comment-highlight=\"true\"] {\n  @apply !bg-[var(--color-yellow)]/20;\n  background-color: rgba(188, 133, 255, 0.22);\n}\n\ntr[data-comment-draft=\"true\"] {\n  background-color: rgba(59, 130, 246, 0.12);\n}\n\ntr[data-comment-draft=\"true\"] td:first-child {\n  border-left-color: rgba(59, 130, 246, 0.4);\n}\n",
      "type": "registry:component",
      "target": "components/ui/diff/theme.css"
    },
    {
      "path": "registry/ui/diff/utils/index.ts",
      "content": "export * from \"./parse\";\nexport * from \"./guess-lang\";\n",
      "type": "registry:lib",
      "target": "components/ui/diff/utils/index.ts"
    },
    {
      "path": "registry/ui/diff/utils/parse.ts",
      "content": "import gitDiffParser, {\n  Hunk as _Hunk,\n  File as _File,\n  Change as _Change,\n  DeleteChange,\n  InsertChange,\n} from \"gitdiff-parser\";\nimport { diffChars, diffWords } from \"diff\";\n\nexport interface LineSegment {\n  value: string;\n  type: \"insert\" | \"delete\" | \"normal\";\n}\n\ntype ReplaceKey<T, K extends PropertyKey, V> = T extends unknown\n  ? Omit<T, K> & Record<K, V>\n  : never;\n\nexport type Line = ReplaceKey<_Change, \"content\", LineSegment[]>;\n\nexport interface Hunk extends Omit<_Hunk, \"changes\"> {\n  type: \"hunk\";\n  lines: Line[];\n}\n\nexport interface SkipBlock {\n  count: number;\n  type: \"skip\";\n  content: string;\n}\n\nexport interface File extends Omit<_File, \"hunks\"> {\n  hunks: (Hunk | SkipBlock)[];\n}\n\nexport interface ParseOptions {\n  maxDiffDistance: number;\n  maxChangeRatio: number;\n  mergeModifiedLines: boolean;\n  inlineMaxCharEdits: number;\n}\n\nconst calculateChangeRatio = (a: string, b: string): number => {\n  const totalChars = a.length + b.length;\n  if (totalChars === 0) return 1;\n  const tokens = diffWords(a, b);\n  const changedChars = tokens\n    .filter((token) => token.added || token.removed)\n    .reduce((sum, token) => sum + token.value.length, 0);\n  return changedChars / totalChars;\n};\n\nconst isSimilarEnough = (\n  a: string,\n  b: string,\n  maxChangeRatio: number\n): boolean => {\n  if (maxChangeRatio <= 0) return a === b;\n  if (maxChangeRatio >= 1) return true;\n  return calculateChangeRatio(a, b) <= maxChangeRatio;\n};\n\nconst changeToLine = (change: _Change): Line => ({\n  ...change,\n  content: [\n    {\n      value: change.content,\n      type: \"normal\",\n    },\n  ],\n});\n\nfunction diffCharsIfWithinEditLimit(\n  a: string,\n  b: string,\n  maxEdits = 4\n):\n  | {\n      exceededLimit: true;\n    }\n  | {\n      exceededLimit: false;\n      diffs: LineSegment[];\n    } {\n  const diffs = diffChars(a, b);\n\n  let edits = 0;\n  for (const part of diffs) {\n    if (part.added || part.removed) {\n      edits += part.value.length;\n      if (edits > maxEdits) return { exceededLimit: true };\n    }\n  }\n\n  return {\n    exceededLimit: false,\n    diffs: diffs.map((d) => ({\n      value: d.value,\n      type: d.added ? \"insert\" : d.removed ? \"delete\" : \"normal\",\n    })),\n  };\n}\n\nconst buildInlineDiffSegments = (\n  current: _Change,\n  next: _Change,\n  options: ParseOptions\n): Line[\"content\"] => {\n  const segments: LineSegment[] = diffWords(current.content, next.content).map(\n    (token) => ({\n      value: token.value,\n      type: token.added ? \"insert\" : token.removed ? \"delete\" : \"normal\",\n    })\n  );\n\n  const result: LineSegment[] = [];\n\n  const mergeIntoResult = (segment: LineSegment) => {\n    const last = result[result.length - 1];\n    if (last && last.type === segment.type) {\n      last.value += segment.value;\n    } else {\n      result.push(segment);\n    }\n  };\n\n  for (let i = 0; i < segments.length; i++) {\n    const current = segments[i];\n    const next = segments[i + 1];\n    if (current.type === \"delete\" && next?.type === \"insert\") {\n      const charDiff = diffCharsIfWithinEditLimit(\n        current.value,\n        next.value,\n        options.inlineMaxCharEdits\n      );\n\n      if (!charDiff.exceededLimit) {\n        charDiff.diffs.forEach(mergeIntoResult);\n\n        i++;\n      } else {\n        result.push(current);\n      }\n    } else {\n      mergeIntoResult(current);\n    }\n  }\n\n  return result;\n};\n\nconst mergeAdjacentLines = (\n  changes: _Change[],\n  options: ParseOptions\n): Line[] => {\n  const out: Line[] = [];\n  for (let i = 0; i < changes.length; i++) {\n    const current = changes[i];\n    const next = changes[i + 1];\n    if (\n      next &&\n      current.type === \"delete\" &&\n      next.type === \"insert\" &&\n      isSimilarEnough(current.content, next.content, options.maxChangeRatio)\n    ) {\n      out.push({\n        ...current,\n        type: \"normal\",\n        isNormal: true,\n        oldLineNumber: current.lineNumber,\n        newLineNumber: next.lineNumber,\n        content: buildInlineDiffSegments(current, next, options),\n      });\n      i++;\n    } else {\n      out.push(changeToLine(current));\n    }\n  }\n\n  return out;\n};\n\nconst UNPAIRED = -1;\n\nfunction buildChangeIndices(changes: _Change[]) {\n  const insertIdxs: number[] = [];\n  const deleteIdxs: number[] = [];\n\n  for (let i = 0; i < changes.length; i++) {\n    const c = changes[i];\n    if (c.type === \"insert\") insertIdxs.push(i);\n    else if (c.type === \"delete\") deleteIdxs.push(i);\n  }\n  return { insertIdxs, deleteIdxs };\n}\n\n// TODO: slight penalty for distance?\n// TODO: improve performance w binary search?\nfunction findBestInsertForDelete(\n  changes: _Change[],\n  delIdx: number,\n  insertIdxs: number[],\n  pairOfAdd: Int32Array,\n  options: ParseOptions\n): number {\n  const del = changes[delIdx] as DeleteChange;\n\n  const lower = del.lineNumber - options.maxDiffDistance;\n  const upper = del.lineNumber + options.maxDiffDistance;\n\n  let bestAddIdx = UNPAIRED;\n  let bestRatio = Infinity;\n\n  for (const addIdx of insertIdxs) {\n    const add = changes[addIdx] as InsertChange;\n\n    if (pairOfAdd[addIdx] !== UNPAIRED) continue;\n\n    if (add.lineNumber < lower) continue;\n    if (add.lineNumber > upper) break;\n\n    const ratio = calculateChangeRatio(del.content, add.content);\n\n    if (ratio > options.maxChangeRatio) continue;\n\n    if (ratio < bestRatio) {\n      bestRatio = ratio;\n      bestAddIdx = addIdx;\n    }\n  }\n\n  return bestAddIdx;\n}\n\nfunction buildInitialPairs(\n  changes: _Change[],\n  insertIdxs: number[],\n  deleteIdxs: number[],\n  options: ParseOptions\n) {\n  const n = changes.length;\n  const pairOfDel = new Int32Array(n).fill(UNPAIRED);\n  const pairOfAdd = new Int32Array(n).fill(UNPAIRED);\n\n  for (const di of deleteIdxs) {\n    const bestAddIdx = findBestInsertForDelete(\n      changes,\n      di,\n      insertIdxs,\n      pairOfAdd,\n      options\n    );\n    if (bestAddIdx !== UNPAIRED) {\n      pairOfDel[di] = bestAddIdx;\n      pairOfAdd[bestAddIdx] = di;\n    }\n  }\n\n  return { pairOfDel, pairOfAdd };\n}\n\nfunction buildUnpairedDeletePrefix(changes: _Change[], pairOfDel: Int32Array) {\n  const n = changes.length;\n  const prefix = new Int32Array(n + 1);\n\n  for (let i = 0; i < n; i++) {\n    const c = changes[i];\n    const isInitiallyUnpairedDelete =\n      c.type === \"delete\" && pairOfDel[i] === UNPAIRED;\n    prefix[i + 1] = prefix[i] + (isInitiallyUnpairedDelete ? 1 : 0);\n  }\n\n  return prefix;\n}\n\nfunction hasUnpairedDeleteBetween(\n  unpairedDelPrefix: Int32Array,\n  deleteIdx: number,\n  insertIdx: number\n) {\n  const lower = Math.max(0, deleteIdx);\n  const upper = Math.max(lower, insertIdx);\n  return unpairedDelPrefix[upper] - unpairedDelPrefix[lower] > 0;\n}\n\nfunction emitNormal(out: Line[], c: _Change) {\n  out.push(changeToLine(c));\n}\n\nfunction emitModified(\n  out: Line[],\n  del: DeleteChange,\n  add: InsertChange,\n  options: ParseOptions\n) {\n  out.push({\n    oldLineNumber: del.lineNumber,\n    newLineNumber: add.lineNumber,\n    type: \"normal\",\n    isNormal: true,\n    content: buildInlineDiffSegments(del, add, options),\n  });\n}\n\nfunction emitLines(\n  changes: _Change[],\n  pairOfDel: Int32Array,\n  pairOfAdd: Int32Array,\n  unpairedDelPrefix: Int32Array,\n  options: ParseOptions\n): Line[] {\n  const out: Line[] = [];\n  const processed = new Uint8Array(changes.length);\n\n  for (let i = 0; i < changes.length; i++) {\n    if (processed[i]) continue;\n    const c = changes[i];\n\n    if (c.type === \"normal\") {\n      processed[i] = 1;\n      emitNormal(out, c);\n    } else if (c.type === \"delete\") {\n      const pairedAddIdx = pairOfDel[i];\n\n      if (pairedAddIdx === UNPAIRED) {\n        processed[i] = 1;\n        emitNormal(out, c);\n      } else if (pairedAddIdx > i) {\n        const shouldUnpair = hasUnpairedDeleteBetween(\n          unpairedDelPrefix,\n          i + 1,\n          pairedAddIdx\n        );\n\n        if (shouldUnpair) {\n          pairOfAdd[pairedAddIdx] = UNPAIRED;\n          processed[i] = 1;\n          emitNormal(out, c);\n        } else {\n          // Defer emission to paired insert\n          processed[i] = 1;\n        }\n      } else {\n        const add = changes[pairedAddIdx] as InsertChange;\n        emitModified(out, c, add, options);\n        processed[i] = 1;\n        processed[pairedAddIdx] = 1;\n      }\n    } else {\n      const pairedDelIdx = pairOfAdd[i];\n\n      if (pairedDelIdx === UNPAIRED) {\n        processed[i] = 1;\n        emitNormal(out, c);\n      } else {\n        const del = changes[pairedDelIdx] as DeleteChange;\n        emitModified(out, del, c, options);\n        processed[i] = 1;\n        processed[pairedDelIdx] = 1;\n      }\n    }\n  }\n\n  return out;\n}\n\nexport function mergeModifiedLines(\n  changes: _Change[],\n  options: ParseOptions\n): Line[] {\n  const { insertIdxs, deleteIdxs } = buildChangeIndices(changes);\n\n  const { pairOfDel, pairOfAdd } = buildInitialPairs(\n    changes,\n    insertIdxs,\n    deleteIdxs,\n    options\n  );\n\n  const unpairedDelPrefix = buildUnpairedDeletePrefix(changes, pairOfDel);\n\n  return emitLines(changes, pairOfDel, pairOfAdd, unpairedDelPrefix, options);\n}\n\nconst parseHunk = (hunk: _Hunk, options: ParseOptions): Hunk => {\n  if (options.mergeModifiedLines) {\n    return {\n      ...hunk,\n      type: \"hunk\",\n      lines:\n        options.maxDiffDistance === 1\n          ? mergeAdjacentLines(hunk.changes, options)\n          : mergeModifiedLines(hunk.changes, options),\n    };\n  }\n\n  return {\n    ...hunk,\n    type: \"hunk\",\n    lines: hunk.changes.map(changeToLine),\n  };\n};\n\nconst HUNK_HEADER_REGEX = /^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@(.*)/;\n\nconst extractHunkContext = (header: string): string =>\n  HUNK_HEADER_REGEX.exec(header)?.[5]?.trim() ?? \"\";\n\nconst insertSkipBlocks = (hunks: Hunk[]): (Hunk | SkipBlock)[] => {\n  const result: (Hunk | SkipBlock)[] = [];\n  let lastHunkLine = 1;\n\n  for (const hunk of hunks) {\n    const distanceToLastHunk = hunk.oldStart - lastHunkLine;\n\n    const context = extractHunkContext(hunk.content);\n    if (distanceToLastHunk > 0) {\n      result.push({\n        count: distanceToLastHunk,\n        type: \"skip\",\n        content: context ?? hunk.content,\n      });\n    }\n    lastHunkLine = Math.max(hunk.oldStart + hunk.oldLines, lastHunkLine);\n    result.push(hunk);\n  }\n\n  return result;\n};\n\nconst defaultOptions: ParseOptions = {\n  maxDiffDistance: 30,\n  maxChangeRatio: 0.45,\n  mergeModifiedLines: true,\n  inlineMaxCharEdits: 4,\n};\n\nexport const parseDiff = (\n  diff: string,\n  options?: Partial<ParseOptions>\n): File[] => {\n  const opts = { ...defaultOptions, ...options };\n  const files = gitDiffParser.parse(diff);\n\n  return files.map((file) => ({\n    ...file,\n    hunks: insertSkipBlocks(file.hunks.map((hunk) => parseHunk(hunk, opts))),\n  }));\n};\n",
      "type": "registry:lib",
      "target": "components/ui/diff/utils/parse.ts"
    },
    {
      "path": "registry/ui/diff/utils/guess-lang.ts",
      "content": "const extToLang: Record<string, string> = {\n  // JavaScript/TypeScript\n  js: \"javascript\",\n  jsx: \"jsx\",\n  ts: \"typescript\",\n  tsx: \"tsx\",\n  mjs: \"javascript\",\n  cjs: \"javascript\",\n\n  // Web\n  html: \"markup\",\n  htm: \"markup\",\n  xml: \"markup\",\n  svg: \"markup\",\n  css: \"css\",\n  scss: \"scss\",\n  sass: \"sass\",\n  less: \"less\",\n  stylus: \"stylus\",\n\n  // Python\n  py: \"python\",\n  pyw: \"python\",\n  pyi: \"python\",\n\n  // Java/JVM\n  java: \"java\",\n  kt: \"kotlin\",\n  kts: \"kotlin\",\n  scala: \"scala\",\n  groovy: \"groovy\",\n\n  // C/C++\n  c: \"c\",\n  cpp: \"cpp\",\n  cc: \"cpp\",\n  cxx: \"cpp\",\n  h: \"cpp\",\n  hpp: \"cpp\",\n  hh: \"cpp\",\n  hxx: \"cpp\",\n\n  // C#/.NET\n  cs: \"csharp\",\n  vb: \"vbnet\",\n  fs: \"fsharp\",\n\n  // Rust\n  rs: \"rust\",\n\n  // Go\n  go: \"go\",\n\n  // Ruby\n  rb: \"ruby\",\n  rake: \"ruby\",\n\n  // PHP\n  php: \"php\",\n  phtml: \"php\",\n\n  // Shell\n  sh: \"bash\",\n  bash: \"bash\",\n  zsh: \"bash\",\n  fish: \"bash\",\n\n  // Data formats\n  json: \"json\",\n  json5: \"json5\",\n  yml: \"yaml\",\n  yaml: \"yaml\",\n  toml: \"toml\",\n  ini: \"ini\",\n  csv: \"csv\",\n\n  // Markdown/Docs\n  md: \"markdown\",\n  markdown: \"markdown\",\n  tex: \"latex\",\n\n  // Swift/Objective-C\n  swift: \"swift\",\n  m: \"objectivec\",\n  mm: \"objectivec\",\n\n  // SQL\n  sql: \"sql\",\n\n  // Other languages\n  r: \"r\",\n  lua: \"lua\",\n  perl: \"perl\",\n  pl: \"perl\",\n  dart: \"dart\",\n  elm: \"elm\",\n  ex: \"elixir\",\n  exs: \"elixir\",\n  erl: \"erlang\",\n  clj: \"clojure\",\n  cljs: \"clojure\",\n  lisp: \"lisp\",\n  hs: \"haskell\",\n  ml: \"ocaml\",\n\n  // Config files\n  dockerfile: \"docker\",\n  gitignore: \"ignore\",\n\n  // Other\n  graphql: \"graphql\",\n  proto: \"protobuf\",\n  wasm: \"wasm\",\n  vim: \"vim\",\n  zig: \"zig\",\n  mermaid: \"mermaid\",\n};\n\nexport const guessLang = (filename?: string): string => {\n  const ext = filename?.split(\".\").pop()?.toLowerCase() ?? \"\";\n  return extToLang[ext] ?? \"tsx\";\n};\n",
      "type": "registry:lib",
      "target": "components/ui/diff/utils/guess-lang.ts"
    },
    {
      "path": "registry/blocks/diff-viewer/page.tsx",
      "content": "import { DiffViewer } from \"@/registry/blocks/diff-viewer/diff-viewer\";\n\nexport default function Page() {\n  return (\n    <div className=\"container mx-auto p-4\">\n      <DiffViewer\n        patch={`diff --git a/file.tsx b/file.tsx\nindex 1234567..2345678 100644\n--- a/file.tsx\n+++ b/file.tsx\n@@ -1,1 +1,1 @@\n-<div>Hello</div>\n+<div>World</div>`}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/diff-viewer/page.tsx"
    },
    {
      "path": "registry/blocks/diff-viewer/diff-viewer.tsx",
      "content": "import { Diff, Hunk } from \"@/registry/ui/diff\";\n\nimport {\n  CollapsibleCard,\n  CollapsibleCardHeader,\n  CollapsibleCardTitle,\n  CollapsibleCardContent,\n} from \"@/registry/ui/collapsible-card\";\n\nimport { parseDiff, ParseOptions } from \"@/registry/ui/diff/utils/parse\";\n\nexport function DiffViewer({\n  patch,\n  options = {},\n}: {\n  patch: string;\n  options?: Partial<ParseOptions>;\n}) {\n  const [file] = parseDiff(patch, options);\n\n  return (\n    <CollapsibleCard\n      data-section-id=\"diff-viewer\"\n      id=\"diff-viewer\"\n      className=\"my-4 text-[0.8rem] w-full\"\n      title=\"File Changes\"\n      defaultOpen\n    >\n      <CollapsibleCardHeader>\n        <CollapsibleCardTitle title={file.newPath}>\n          {file.newPath}\n        </CollapsibleCardTitle>\n      </CollapsibleCardHeader>\n      <CollapsibleCardContent>\n        <Diff fileName=\"file-changes.tsx\" hunks={file.hunks} type={file.type}>\n          {file.hunks.map((hunk) => (\n            <Hunk key={hunk.content} hunk={hunk} />\n          ))}\n        </Diff>\n      </CollapsibleCardContent>\n    </CollapsibleCard>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/blocks/diff-viewer/diff-viewer.tsx"
    }
  ]
}