{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"code-diff","type":"registry:ui","title":"CodeDiff","description":"Unified diff tuned for chat width: one foldable card per file, tinted add/delete rows, and a copy-safe gutter","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["ai-agents"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/code-diff/CodeDiff.tsx","content":"\"use client\";\n\nimport { forwardRef, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useInertAttribute } from \"../../internals/dom/use-inert-attribute\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { parseUnifiedDiff, type DiffFile, type DiffLine } from \"../../internals/diff\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport \"./code-diff.css\";\n\n/**\n * Props for CodeDiff\n */\nexport interface CodeDiffProps {\n\t/**\n\t * Raw unified diff text — whatever `git diff` printed. Parsed on every\n\t * change, so a patch that is still arriving can be handed over as it grows.\n\t */\n\tdiff: string;\n\t/** Header label when the patch names no file, or names exactly one. */\n\tfilename?: string;\n\t/** Whether to show the old/new line-number gutters. */\n\tlineNumbers?: boolean;\n\t/**\n\t * Whether the bodies are folded away. A master switch: it seeds every file,\n\t * and flipping it re-folds or re-opens the whole patch. The React\n\t * counterpart of the Svelte source's bindable prop — pair it with\n\t * `onCollapsedChange` to observe the aggregate the way `bind:collapsed`\n\t * would report it.\n\t */\n\tcollapsed?: boolean;\n\t/**\n\t * Reports the whole-patch fold state after a header click: true only once\n\t * nothing is left open.\n\t */\n\tonCollapsedChange?: (collapsed: boolean) => void;\n\t/** Lines shown before the rest hide behind a \"show more\" button. 0 shows all. */\n\tmaxLines?: number;\n\t/** Whether long lines wrap instead of scrolling sideways. */\n\twrap?: boolean;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\ntype Row = { kind: \"sep\"; text: string } | { kind: \"line\"; line: DiffLine };\n\ninterface FileView {\n\tfile: DiffFile;\n\tname: string;\n\trows: Row[];\n\t/** Lines the clamp is currently withholding; 0 when everything is on screen. */\n\thidden: number;\n}\n\n/** Glyphs, not markers: they are never copied, so they can be the prettier pair. */\nconst GLYPH = { add: \"+\", del: \"−\", context: \" \", meta: \" \" } as const;\n\nexport const CodeDiff = forwardRef<HTMLDivElement, CodeDiffProps>(function CodeDiff(\n\t{\n\t\tdiff,\n\t\tfilename,\n\t\tlineNumbers = true,\n\t\tcollapsed = false,\n\t\tonCollapsedChange,\n\t\tmaxLines = 0,\n\t\twrap = false,\n\t\tclassName,\n\t\tsound = false,\n\t},\n\tref\n) {\n\tconst uid = useFancyId();\n\tconst playCue = useSoundCue(sound);\n\n\tconst files = useMemo(() => parseUnifiedDiff(diff), [diff]);\n\n\t// Per-file fold and clamp state, keyed by file name rather than position: a\n\t// patch that arrives with a different set of files must not hand file 0's\n\t// folded state to whatever now happens to sit first. `collapsed` is the\n\t// master switch: it seeds every file and wipes the overrides whenever the\n\t// consumer flips it, so driving the prop still folds the whole patch while\n\t// a click still folds one file.\n\tconst [folded, setFolded] = useState<Record<string, boolean>>({});\n\tconst [unclamped, setUnclamped] = useState<Record<string, boolean>>({});\n\n\t// The live fold seed — the counterpart of the Svelte source's local\n\t// `collapsed`, which a header click writes back to. The prop re-seeds it\n\t// whenever the consumer changes it; a click updates it directly.\n\tconst [collapsedValue, setCollapsedValue] = useState(collapsed);\n\tconst [lastProp, setLastProp] = useState(collapsed);\n\n\t// Resynced during render, not from an effect: what the switch decides is\n\t// visible on the very first painted frame, so the seed and the override\n\t// wipe have to land in the same commit the flip produced — the Svelte\n\t// source reads `collapsed` straight out of `isFolded` and defers only the\n\t// wipe to a pre-paint `$effect`. React restarts the render before\n\t// committing, so the pass below never reaches the screen.\n\tif (lastProp !== collapsed) {\n\t\tsetLastProp(collapsed);\n\t\t// Only a flip that actually moves the seed wipes the per-file\n\t\t// overrides: a value the consumer merely echoed back from\n\t\t// `onCollapsedChange` is the state this component already produced,\n\t\t// and folding one file of a two-file patch must not unfold the other.\n\t\tif (collapsedValue !== collapsed) {\n\t\t\tsetCollapsedValue(collapsed);\n\t\t\tsetFolded({});\n\t\t}\n\t}\n\n\tfunction nameOf(file: DiffFile, count: number): string {\n\t\tif (filename !== undefined && count <= 1) return filename;\n\t\tif (\n\t\t\tfile.isRename &&\n\t\t\tfile.oldPath !== null &&\n\t\t\tfile.newPath !== null &&\n\t\t\tfile.oldPath !== file.newPath\n\t\t) {\n\t\t\treturn `${file.oldPath} → ${file.newPath}`;\n\t\t}\n\t\treturn file.newPath ?? file.oldPath ?? \"\";\n\t}\n\n\t// Names alone would let a reused path carry state across two unrelated\n\t// patches, so the whole list is watched: when it changes, both records start\n\t// empty and every file is back under `collapsed`.\n\t// A name on its own cannot tell two patches apart when both touch the same\n\t// path, so a replacement would inherit the fold and clamp state of the patch\n\t// before it. The first hunk's declared start pins that down, and it is fixed\n\t// the moment that header parses — a patch still streaming in only ever gains\n\t// lines and later hunks, so growth in place still reads as the same file.\n\t//\n\t// Memoised for the reason the Svelte source keeps this in `$derived`: a\n\t// parent that re-renders per streamed token would otherwise walk the whole\n\t// file list again for a patch that did not move.\n\tconst signature = useMemo(\n\t\t() =>\n\t\t\tfiles\n\t\t\t\t.map((file) => {\n\t\t\t\t\tconst anchor = file.hunks[0];\n\t\t\t\t\treturn `${nameOf(file, files.length)}:${anchor?.oldStart ?? \"\"}:${anchor?.newStart ?? \"\"}`;\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\"),\n\t\t// `nameOf` is re-created every render but reads nothing beyond `filename`,\n\t\t// so the two inputs below are the whole dependency set.\n\t\t[files, filename]\n\t);\n\t// Seeded with the first render's value on purpose: this is the \"what did we\n\t// render last\" marker, and it is the comparison below — not the seeding —\n\t// that is meant to notice a change.\n\tconst [lastSignature, setLastSignature] = useState(signature);\n\n\t// Same render-phase resync as the master switch above, and for the same\n\t// reason: a replacement patch must not paint one frame with the previous\n\t// patch's fold and clamp overrides on top of the new content.\n\tif (lastSignature !== signature) {\n\t\tsetLastSignature(signature);\n\t\tsetFolded({});\n\t\tsetUnclamped({});\n\t}\n\n\tfunction isFolded(name: string): boolean {\n\t\treturn folded[name] ?? collapsedValue;\n\t}\n\n\tconst views: FileView[] = useMemo(\n\t\t() =>\n\t\t\tfiles.map((file) => {\n\t\t\t\tconst name = nameOf(file, files.length);\n\t\t\t\tconst rows: Row[] = [];\n\t\t\t\tlet total = 0;\n\t\t\t\tfor (const hunk of file.hunks) {\n\t\t\t\t\tif (hunk.header !== \"\") rows.push({ kind: \"sep\", text: hunk.header });\n\t\t\t\t\tfor (const line of hunk.lines) {\n\t\t\t\t\t\trows.push({ kind: \"line\", line });\n\t\t\t\t\t\ttotal++;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (maxLines <= 0 || unclamped[name] || total <= maxLines) {\n\t\t\t\t\treturn { file, name, rows, hidden: 0 };\n\t\t\t\t}\n\n\t\t\t\t// Cut on a line, never on a hunk header, so the clamp never leaves a\n\t\t\t\t// dangling `@@` with nothing under it.\n\t\t\t\tconst shown: Row[] = [];\n\t\t\t\tlet seen = 0;\n\t\t\t\tfor (const row of rows) {\n\t\t\t\t\tshown.push(row);\n\t\t\t\t\tif (row.kind !== \"line\") continue;\n\t\t\t\t\tseen++;\n\t\t\t\t\tif (seen === maxLines) break;\n\t\t\t\t}\n\t\t\t\treturn { file, name, rows: shown, hidden: total - maxLines };\n\t\t\t}),\n\t\t// Same as above: `filename` stands in for `nameOf`.\n\t\t[files, filename, maxLines, unclamped]\n\t);\n\n\tfunction toggle(name: string) {\n\t\t// Captured before the write, so the cue that plays matches the state this\n\t\t// click is actually producing — the resyncs above that also wipe `folded`\n\t\t// (a collapsed-prop flip, a patch-signature change) never run through this\n\t\t// function, so they can never trigger it either.\n\t\tconst next = !isFolded(name);\n\t\tconst nextFolded = { ...folded, [name]: next };\n\t\tsetFolded(nextFolded);\n\t\tplayCue(next ? \"close\" : \"open\");\n\t\t// `collapsed` reports the whole patch, so it is true only once nothing is left open.\n\t\tconst all = views.every((view) => nextFolded[view.name] ?? collapsedValue);\n\t\tsetCollapsedValue(all);\n\t\tonCollapsedChange?.(all);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\trole=\"group\"\n\t\t\taria-label=\"Code diff\"\n\t\t\tclassName={cn(\n\t\t\t\t\"fancy-code-diff\",\n\t\t\t\t\"border-border bg-card/50 w-full overflow-hidden rounded-lg border font-mono text-xs\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t>\n\t\t\t{views.map((view, index) => {\n\t\t\t\tconst open = !isFolded(view.name);\n\t\t\t\tconst headerId = `${uid}-${index}-header`;\n\t\t\t\tconst bodyId = `${uid}-${index}-body`;\n\t\t\t\treturn (\n\t\t\t\t\t<div key={index} className=\"border-border border-t first:border-t-0\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tid={headerId}\n\t\t\t\t\t\t\tclassName=\"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 px-3 py-2 text-left transition-colors\"\n\t\t\t\t\t\t\taria-expanded={open}\n\t\t\t\t\t\t\taria-controls={bodyId}\n\t\t\t\t\t\t\tonClick={() => toggle(view.name)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\tclassName={cn(\"ft-chevron size-3.5 shrink-0\", open && \"ft-open\")}\n\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\tstrokeWidth=\"2.5\"\n\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<path d=\"m9 6 6 6-6 6\" />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t{view.name ? (\n\t\t\t\t\t\t\t\t<span className=\"text-foreground min-w-0 truncate\">{view.name}</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t{view.file.isNew ? (\n\t\t\t\t\t\t\t\t<span className=\"ft-badge\">new file</span>\n\t\t\t\t\t\t\t) : view.file.isDeleted ? (\n\t\t\t\t\t\t\t\t<span className=\"ft-badge\">deleted</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t<span className=\"ml-auto flex shrink-0 items-center gap-2 tabular-nums\">\n\t\t\t\t\t\t\t\t{view.file.additions > 0 ? (\n\t\t\t\t\t\t\t\t\t<span className=\"ft-stat-add\">+{view.file.additions}</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t{view.file.deletions > 0 ? (\n\t\t\t\t\t\t\t\t\t<span className=\"ft-stat-del\">−{view.file.deletions}</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t<div className={cn(\"ft-body\", open && \"ft-open\")}>\n\t\t\t\t\t\t\t<div className=\"overflow-hidden\">\n\t\t\t\t\t\t\t\t<DiffBody id={bodyId} labelledBy={headerId} open={open}>\n\t\t\t\t\t\t\t\t\t<div className={cn(\"ft-scroll\", wrap && \"ft-wrap\")}>\n\t\t\t\t\t\t\t\t\t\t{view.rows.map((row, rowIndex) =>\n\t\t\t\t\t\t\t\t\t\t\trow.kind === \"sep\" ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div key={rowIndex} className=\"ft-sep\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.text}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={rowIndex}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"ft-row\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trow.line.type === \"add\" && \"ft-add\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trow.line.type === \"del\" && \"ft-del\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trow.line.type === \"context\" && \"ft-context\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trow.line.type === \"meta\" && \"ft-meta\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-kind={row.line.type}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{lineNumbers ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"ft-num\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{row.line.oldLine ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"ft-num\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{row.line.newLine ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"ft-glyph\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{GLYPH[row.line.type]}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{/* The glyph stays out of the reading flow — it is\n\t\t\t\t\t\t\t\t\t\t\t\t\t    punctuation, and it must not be copied — so the\n\t\t\t\t\t\t\t\t\t\t\t\t\t    verdict a sighted reader takes from the tint and\n\t\t\t\t\t\t\t\t\t\t\t\t\t    the +/− is spoken here instead. Unselectable for\n\t\t\t\t\t\t\t\t\t\t\t\t\t    the same copy hygiene the gutters follow. */}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.line.type === \"add\" || row.line.type === \"del\" ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"sr-only select-none\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{row.line.type === \"add\" ? \"Added line\" : \"Removed line\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"ft-code\">{row.line.text}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t{view.hidden > 0 ? (\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-muted-foreground hover:text-foreground hover:bg-muted/40 border-border w-full border-t px-3 py-1.5 text-left transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tplayCue(\"open\");\n\t\t\t\t\t\t\t\t\t\t\t\tsetUnclamped((current) => ({ ...current, [view.name]: true }));\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tShow {view.hidden} more {view.hidden === 1 ? \"line\" : \"lines\"}\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</DiffBody>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n});\n\n/**\n * One file's body region, extracted for the single reason that its inertness is\n * a hook: `inert` cannot be written as a JSX prop across this package's peer\n * range (React 18 drops `inert={true}`, React 19 rejects `inert=\"\"`), so it goes\n * to the node imperatively — and a hook cannot be called inside the `views.map`\n * above. The markup is unchanged.\n */\nfunction DiffBody({\n\tid,\n\tlabelledBy,\n\topen,\n\tchildren,\n}: {\n\tid: string;\n\tlabelledBy: string;\n\topen: boolean;\n\tchildren: ReactNode;\n}) {\n\tconst inertRef = useInertAttribute<HTMLDivElement>(!open);\n\treturn (\n\t\t<div ref={inertRef} id={id} role=\"group\" aria-labelledby={labelledBy}>\n\t\t\t{children}\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/code-diff/CodeDiff.tsx"},{"path":"react/src/components/code-diff/code-diff.css","content":"/*\n * `.fancy-code-diff` is a PORT-ADDED anchor (PORTING.md, styling rule 2). The\n * source's <style> block was compiler-scoped and had no root class; plain CSS\n * is global, so every rule here stays anchored under the root to keep names\n * like `.ft-row` from leaking into every consumer's page. The anchor is the\n * first token of the root `cn()` call.\n */\n\n/*\n * 0fr → 1fr on a one-row grid transitions \"auto\" height without measuring\n * anything; the inner wrapper does the clipping.\n */\n.fancy-code-diff .ft-body {\n\tdisplay: grid;\n\tgrid-template-rows: 0fr;\n}\n\n.fancy-code-diff .ft-body.ft-open {\n\tgrid-template-rows: 1fr;\n}\n\n.fancy-code-diff .ft-chevron.ft-open {\n\ttransform: rotate(90deg);\n}\n\n.fancy-code-diff .ft-scroll {\n\toverflow-x: auto;\n\toverflow-y: hidden;\n\tpadding-block: 0.25rem;\n}\n\n.fancy-code-diff .ft-scroll.ft-wrap {\n\toverflow-x: hidden;\n}\n\n/*\n * `max-content` with a 100% floor keeps a row's tint running the full scroll\n * width instead of stopping where the shortest line ends.\n */\n.fancy-code-diff .ft-row,\n.fancy-code-diff .ft-sep {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n\n.fancy-code-diff .ft-wrap .ft-row,\n.fancy-code-diff .ft-wrap .ft-sep {\n\twidth: auto;\n}\n\n.fancy-code-diff .ft-row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tline-height: 1.6;\n}\n\n.fancy-code-diff .ft-sep {\n\tpadding: 0.375rem 0.75rem;\n\twhite-space: pre;\n\topacity: 0.55;\n}\n\n/*\n * Tint and glyph both carry the verdict, so the rows stay readable when the\n * hues do not land — colour is never the only signal. Both tints are mixed\n * from the `--ft-status-*` vocabulary this component family shares, so a\n * theme that recolours success and failure once recolours the diff too.\n */\n.fancy-code-diff .ft-add {\n\tbackground-color: var(\n\t\t--ft-diff-add-bg,\n\t\tcolor-mix(\n\t\t\tin oklab,\n\t\t\tvar(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145))) 12%,\n\t\t\ttransparent\n\t\t)\n\t);\n}\n\n.fancy-code-diff .ft-del {\n\tbackground-color: var(\n\t\t--ft-diff-del-bg,\n\t\tcolor-mix(\n\t\t\tin oklab,\n\t\t\tvar(--ft-status-error, light-dark(oklch(0.5 0.19 25), oklch(0.7 0.18 25))) 12%,\n\t\t\ttransparent\n\t\t)\n\t);\n}\n\n.fancy-code-diff .ft-context {\n\tbackground-color: transparent;\n}\n\n.fancy-code-diff .ft-meta {\n\tfont-style: italic;\n\topacity: 0.6;\n}\n\n.fancy-code-diff .ft-stat-add {\n\tcolor: var(\n\t\t--ft-diff-add-fg,\n\t\tvar(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)))\n\t);\n}\n\n.fancy-code-diff .ft-stat-del {\n\tcolor: var(\n\t\t--ft-diff-del-fg,\n\t\tvar(--ft-status-error, light-dark(oklch(0.5 0.19 25), oklch(0.7 0.18 25)))\n\t);\n}\n\n.fancy-code-diff .ft-badge {\n\tflex: none;\n\tborder-radius: 0.25rem;\n\tpadding: 0.0625rem 0.375rem;\n\tfont-size: 0.6875rem;\n\tbackground-color: var(--ft-diff-badge-bg, color-mix(in oklab, currentColor 12%, transparent));\n}\n\n/* Gutters, glyphs and hunk headers are chrome: skipping them keeps a copied\n   selection compilable — no line numbers, no markers, no dangling `@@`. */\n.fancy-code-diff .ft-num,\n.fancy-code-diff .ft-glyph,\n.fancy-code-diff .ft-sep {\n\tuser-select: none;\n\t-webkit-user-select: none;\n}\n\n.fancy-code-diff .ft-num,\n.fancy-code-diff .ft-glyph {\n\tflex: none;\n}\n\n.fancy-code-diff .ft-num {\n\tmin-width: 4ch;\n\tpadding-inline: 0.5rem 0;\n\ttext-align: right;\n\topacity: 0.45;\n\tfont-variant-numeric: tabular-nums;\n}\n\n.fancy-code-diff .ft-glyph {\n\twidth: 2.5ch;\n\ttext-align: center;\n\topacity: 0.7;\n}\n\n.fancy-code-diff .ft-code {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\twhite-space: pre;\n\tpadding-inline-end: 0.75rem;\n}\n\n.fancy-code-diff .ft-wrap .ft-code {\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n}\n\n/* Everything that moves lives behind the query; reduced motion gets the same\n   states with nothing to shorten. */\n@media (prefers-reduced-motion: no-preference) {\n\t.fancy-code-diff .ft-body {\n\t\ttransition: grid-template-rows 260ms cubic-bezier(0.4, 0, 0.2, 1);\n\t}\n\n\t.fancy-code-diff .ft-chevron {\n\t\ttransition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/code-diff/code-diff.css"},{"path":"react/src/components/code-diff/index.ts","content":"\"use client\";\n\nexport { CodeDiff } from \"./CodeDiff\";\nexport type { CodeDiffProps } from \"./CodeDiff\";\n","type":"registry:ui","target":"@ui/fancy/components/code-diff/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/code-diff?framework=react"}}