{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"fancy-diff","type":"registry:ui","title":"FancyDiff","description":"Controlled split \/ inline diff viewer; per-hunk accept\/reject with a merged result, render-prop customization, and a git unified-diff datasource.","package":"fancy-diff","dependencies":["@particle-academy\/react-fancy","@particle-academy\/fancy-file-commons"],"registryDependencies":["activity"],"files":[{"path":"components\/fancy\/fancy-diff\/FancyDiff.tsx","content":"import {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { Button, Badge, Card, Separator } from \"@particle-academy\/react-fancy\";\nimport { resolveSource } from \"@particle-academy\/fancy-file-commons\";\nimport { mergeResult } from \"@particle-academy\/fancy-file-commons\";\nimport type {\n  AcceptanceState,\n  AcceptanceStatus,\n  Diff,\n  DiffSegment,\n  Hunk,\n} from \"@particle-academy\/fancy-file-commons\";\nimport { fileLabel } from \"@particle-academy\/fancy-file-commons\";\nimport { emitHunkActivity } from \"..\/activity\";\nimport type {\n  FancyDiffProps,\n  DiffViewMode,\n  MergedResult,\n  HunkRenderArgs,\n  ToolbarRenderArgs,\n  GutterRenderArgs,\n} from \".\/FancyDiff.types\";\n\n\/** Imperative handle exposed via ref. *\/\nexport interface FancyDiffHandle {\n  \/** Compute the merged result from the current acceptance state. *\/\n  getMergedResult: () => MergedResult;\n  \/** The resolved diffs (one per file). *\/\n  getDiffs: () => Diff[];\n  \/** Current acceptance state. *\/\n  getAcceptance: () => AcceptanceState;\n}\n\nconst statusBadge: Record<AcceptanceStatus, { color: string; label: string }> = {\n  accepted: { color: \"emerald\", label: \"accepted\" },\n  rejected: { color: \"rose\", label: \"rejected\" },\n  pending: { color: \"amber\", label: \"pending\" },\n};\n\nfunction cx(...parts: (string | false | undefined | null)[]): string {\n  return parts.filter(Boolean).join(\" \");\n}\n\n\/** True when the document root currently signals a dark theme. *\/\nfunction hostIsDark(): boolean {\n  if (typeof document === \"undefined\") return false;\n  const root = document.documentElement;\n  if (root.classList.contains(\"dark\")) return true;\n  if (root.getAttribute(\"data-theme\") === \"dark\") return true;\n  try {\n    if (getComputedStyle(root).colorScheme === \"dark\") return true;\n  } catch {\n    \/* getComputedStyle can throw in detached\/SSR contexts *\/\n  }\n  return false;\n}\n\n\/**\n * Resolve the effective dark state for a `theme` prop. `\"dark\"`\/`\"light\"` are\n * honored verbatim; `\"auto\"` probes the host (`.dark` \/ `data-theme` \/\n * `color-scheme` on <html>) and falls back to `prefers-color-scheme`,\n * re-resolving when either changes so a host theme toggle flips the diff too.\n *\/\nfunction useResolvedDark(theme: FancyDiffProps[\"theme\"]): boolean {\n  const explicit = theme === \"dark\" ? true : theme === \"light\" ? false : null;\n  const [isDark, setIsDark] = useState<boolean>(explicit ?? false);\n\n  useEffect(() => {\n    if (explicit !== null) {\n      setIsDark(explicit);\n      return;\n    }\n    if (typeof window === \"undefined\") return;\n\n    const mq = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n    const resolve = () => setIsDark(hostIsDark() || !!mq?.matches);\n    resolve();\n\n    mq?.addEventListener?.(\"change\", resolve);\n    const observer = new MutationObserver(resolve);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"data-theme\", \"style\"],\n    });\n    return () => {\n      mq?.removeEventListener?.(\"change\", resolve);\n      observer.disconnect();\n    };\n  }, [explicit]);\n\n  return isDark;\n}\n\nexport const FancyDiff = forwardRef<FancyDiffHandle, FancyDiffProps>(\n  function FancyDiff(props, ref) {\n    const {\n      source,\n      variant = \"review\",\n      value,\n      onChange,\n      defaultValue,\n      defaultStatus = \"pending\",\n      mode: modeProp,\n      onModeChange,\n      pendingMode = false,\n      onProposal,\n      onResult,\n      renderHunk,\n      renderToolbar,\n      renderGutter,\n      tokenizer,\n      actor,\n      activity,\n      showToolbar = true,\n      showGutter = true,\n      className,\n      theme = \"auto\",\n      header,\n    } = props;\n\n    \/\/ `compare` strips the whole acceptance UX \u2014 read-only comparison only.\n    const interactive = variant !== \"compare\";\n\n    \/\/ Resolve `theme=\"auto\"` at runtime: prefer an explicit host signal (a\n    \/\/ `.dark` class \/ `data-theme=\"dark\"` \/ `color-scheme` on <html>), then fall\n    \/\/ back to `prefers-color-scheme`. Re-resolves on system + host changes so a\n    \/\/ theme toggle flips the diff with it. (theme=\"dark\"\/\"light\" are honored\n    \/\/ verbatim; SSR starts from the explicit value, then resolves on mount.)\n    const isDark = useResolvedDark(theme);\n\n    \/\/ Resolve source -> diffs. Memoized on identity of source + tokenizer.\n    const diffs = useMemo(\n      () => resolveSource(source, { tokenizer }),\n      [source, tokenizer],\n    );\n\n    \/\/ Controlled \/ uncontrolled acceptance state.\n    const [internal, setInternal] = useState<AcceptanceState>(\n      () => defaultValue ?? {},\n    );\n    const acceptance = value ?? internal;\n\n    \/\/ Controlled \/ uncontrolled view mode.\n    const [internalMode, setInternalMode] = useState<DiffViewMode>(\n      modeProp ?? \"split\",\n    );\n    const mode = modeProp ?? internalMode;\n    const setMode = useCallback(\n      (m: DiffViewMode) => {\n        if (modeProp === undefined) setInternalMode(m);\n        onModeChange?.(m);\n      },\n      [modeProp, onModeChange],\n    );\n\n    const allHunks = useMemo(() => {\n      const list: { hunk: Hunk; fileIndex: number }[] = [];\n      diffs.forEach((d, fi) =>\n        d.hunks.forEach((h) => {\n          if (h.type !== \"equal\") list.push({ hunk: h, fileIndex: fi });\n        }),\n      );\n      return list;\n    }, [diffs]);\n\n    const statusOf = useCallback(\n      (id: string): AcceptanceStatus => acceptance[id] ?? defaultStatus,\n      [acceptance, defaultStatus],\n    );\n\n    const computeMerged = useCallback(\n      (state: AcceptanceState): MergedResult => {\n        const files = diffs.map((d) => ({\n          meta: d.file,\n          merged: mergeResult(d, state, { defaultStatus }),\n        }));\n        return { files, text: files[0]?.merged ?? \"\" };\n      },\n      [diffs, defaultStatus],\n    );\n\n    const apply = useCallback(\n      (hunkId: string, status: AcceptanceStatus, action: string, fileIndex?: number) => {\n        if (pendingMode) {\n          onProposal?.({ hunkId, proposedStatus: status, actor });\n        }\n        const next: AcceptanceState = { ...acceptance, [hunkId]: status };\n\n        emitHunkActivity({\n          action: status === \"accepted\" ? \"diff_accept_hunk\" : \"diff_reject_hunk\",\n          hunkId,\n          fileId: fileIndex !== undefined ? diffs[fileIndex]?.file?.newPath : undefined,\n          actor,\n          emitter: activity,\n        });\n\n        if (value === undefined) setInternal(next);\n        onChange?.(next, { hunkId, status, action });\n        onResult?.(computeMerged(next));\n      },\n      [acceptance, pendingMode, onProposal, actor, activity, value, onChange, onResult, computeMerged, diffs],\n    );\n\n    const setAll = useCallback(\n      (status: AcceptanceStatus, action: string) => {\n        const next: AcceptanceState = { ...acceptance };\n        for (const { hunk } of allHunks) next[hunk.id] = status;\n        emitHunkActivity({\n          action: status === \"accepted\" ? \"diff_accept_all\" : \"diff_reject_all\",\n          actor,\n          emitter: activity,\n        });\n        if (value === undefined) setInternal(next);\n        onChange?.(next, { hunkId: \"*\", status, action });\n        onResult?.(computeMerged(next));\n      },\n      [acceptance, allHunks, actor, activity, value, onChange, onResult, computeMerged],\n    );\n\n    useImperativeHandle(\n      ref,\n      () => ({\n        getMergedResult: () => computeMerged(acceptance),\n        getDiffs: () => diffs,\n        getAcceptance: () => acceptance,\n      }),\n      [computeMerged, acceptance, diffs],\n    );\n\n    const counts = useMemo(() => {\n      let accepted = 0;\n      let rejected = 0;\n      let pending = 0;\n      for (const { hunk } of allHunks) {\n        const s = statusOf(hunk.id);\n        if (s === \"accepted\") accepted++;\n        else if (s === \"rejected\") rejected++;\n        else pending++;\n      }\n      return { total: allHunks.length, accepted, rejected, pending };\n    }, [allHunks, statusOf]);\n\n    \/\/ ---- Toolbar ----\n    const defaultToolbar: ReactNode = showToolbar ? (\n      <div\n        data-fancy-diff-toolbar=\"\"\n        role=\"toolbar\"\n        aria-label=\"Diff actions\"\n        className=\"flex items-center gap-2 flex-wrap px-3 py-2\"\n      >\n        <Badge color=\"zinc\" variant=\"soft\" size=\"sm\">\n          {counts.total} change{counts.total === 1 ? \"\" : \"s\"}\n        <\/Badge>\n        {interactive && (\n          <>\n            <Badge color=\"emerald\" variant=\"soft\" size=\"sm\">\n              {counts.accepted} accepted\n            <\/Badge>\n            <Badge color=\"amber\" variant=\"soft\" size=\"sm\">\n              {counts.pending} pending\n            <\/Badge>\n          <\/>\n        )}\n        <span className=\"flex-1\" \/>\n        <Button\n          size=\"sm\"\n          variant=\"ghost\"\n          icon={mode === \"split\" ? \"columns\" : \"list\"}\n          onClick={() => setMode(mode === \"split\" ? \"inline\" : \"split\")}\n          data-fancy-diff-mode-toggle=\"\"\n        >\n          {mode === \"split\" ? \"Split\" : \"Inline\"}\n        <\/Button>\n        {interactive && (\n          <>\n            <Separator orientation=\"vertical\" className=\"h-5\" \/>\n            <Button\n              size=\"sm\"\n              variant=\"ghost\"\n              icon=\"check\"\n              onClick={() => setAll(\"accepted\", \"accept-all\")}\n              data-fancy-diff-accept-all=\"\"\n            >\n              Accept all\n            <\/Button>\n            <Button\n              size=\"sm\"\n              variant=\"ghost\"\n              icon=\"x\"\n              onClick={() => setAll(\"rejected\", \"reject-all\")}\n              data-fancy-diff-reject-all=\"\"\n            >\n              Reject all\n            <\/Button>\n          <\/>\n        )}\n      <\/div>\n    ) : null;\n\n    const toolbar: ReactNode = renderToolbar\n      ? renderToolbar({\n          acceptAll: () => setAll(\"accepted\", \"accept-all\"),\n          rejectAll: () => setAll(\"rejected\", \"reject-all\"),\n          counts,\n          mode,\n          setMode,\n          defaultNode: defaultToolbar,\n        })\n      : defaultToolbar;\n\n    const renderGutterCell = (args: GutterRenderArgs): ReactNode => {\n      if (renderGutter) return renderGutter(args);\n      return (\n        <span className=\"select-none tabular-nums text-zinc-400 dark:text-zinc-500 text-[11px] pr-2 text-right inline-block min-w-[2.5rem]\">\n          {args.side === \"after\"\n            ? args.afterLineNo ?? \"\"\n            : args.beforeLineNo ?? args.afterLineNo ?? \"\"}\n        <\/span>\n      );\n    };\n\n    return (\n      <Card\n        variant=\"outlined\"\n        padding=\"none\"\n        className={cx(\n          \"fancy-diff overflow-hidden font-mono text-[13px] text-zinc-800 dark:text-zinc-100\",\n          isDark && \"dark\",\n          className,\n        )}\n        data-fancy-diff=\"\"\n        data-fancy-diff-mode={mode}\n        data-fancy-diff-variant={variant}\n      >\n        {header}\n        {toolbar}\n        {(header || toolbar) && <Separator \/>}\n        <div data-fancy-diff-body=\"\" role=\"table\" aria-label=\"Diff\">\n          {diffs.map((diff, fileIndex) => (\n            <FileBlock\n              key={fileIndex}\n              diff={diff}\n              fileIndex={fileIndex}\n              mode={mode}\n              statusOf={statusOf}\n              pendingMode={pendingMode}\n              interactive={interactive}\n              showGutter={showGutter}\n              renderGutterCell={renderGutterCell}\n              renderHunk={renderHunk}\n              accept={(id) => apply(id, \"accepted\", \"accept\", fileIndex)}\n              reject={(id) => apply(id, \"rejected\", \"reject\", fileIndex)}\n            \/>\n          ))}\n        <\/div>\n      <\/Card>\n    );\n  },\n);\n\ninterface FileBlockProps {\n  diff: Diff;\n  fileIndex: number;\n  mode: DiffViewMode;\n  statusOf: (id: string) => AcceptanceStatus;\n  pendingMode: boolean;\n  interactive: boolean;\n  showGutter: boolean;\n  renderGutterCell: (args: GutterRenderArgs) => ReactNode;\n  renderHunk?: FancyDiffProps[\"renderHunk\"];\n  accept: (id: string) => void;\n  reject: (id: string) => void;\n}\n\nfunction FileBlock({\n  diff,\n  fileIndex,\n  mode,\n  statusOf,\n  pendingMode,\n  interactive,\n  showGutter,\n  renderGutterCell,\n  renderHunk,\n  accept,\n  reject,\n}: FileBlockProps) {\n  const label = fileLabel(diff.file);\n  return (\n    <div data-fancy-diff-file={fileIndex} role=\"rowgroup\">\n      {label && (\n        <div className=\"flex items-center gap-2 px-3 py-1.5 bg-zinc-50 dark:bg-zinc-800\/60 border-b border-zinc-200 dark:border-zinc-700 text-xs\">\n          <span className=\"font-semibold text-zinc-700 dark:text-zinc-300\">{label}<\/span>\n          {diff.file?.partial && (\n            <Badge color=\"amber\" variant=\"soft\" size=\"sm\" title=\"Unified diff: only changed hunks + context are present\">\n              partial\n            <\/Badge>\n          )}\n        <\/div>\n      )}\n      {diff.hunks.map((hunk) => {\n        const status = statusOf(hunk.id);\n        const defaultNode = (\n          <HunkRow\n            hunk={hunk}\n            mode={mode}\n            status={status}\n            pendingMode={pendingMode}\n            interactive={interactive}\n            showGutter={showGutter}\n            renderGutterCell={renderGutterCell}\n            accept={() => accept(hunk.id)}\n            reject={() => reject(hunk.id)}\n          \/>\n        );\n        if (renderHunk && hunk.type !== \"equal\") {\n          const args: HunkRenderArgs = {\n            hunk,\n            fileIndex,\n            status,\n            mode,\n            pending: pendingMode && status === \"pending\",\n            accept: () => accept(hunk.id),\n            reject: () => reject(hunk.id),\n            defaultNode,\n          };\n          const custom = renderHunk(args);\n          return <div key={hunk.id}>{custom ?? defaultNode}<\/div>;\n        }\n        return <div key={hunk.id}>{defaultNode}<\/div>;\n      })}\n    <\/div>\n  );\n}\n\ninterface HunkRowProps {\n  hunk: Hunk;\n  mode: DiffViewMode;\n  status: AcceptanceStatus;\n  pendingMode: boolean;\n  interactive: boolean;\n  showGutter: boolean;\n  renderGutterCell: (args: GutterRenderArgs) => ReactNode;\n  accept: () => void;\n  reject: () => void;\n}\n\nconst sideBg: Record<string, string> = {\n  add: \"bg-emerald-50 dark:bg-emerald-500\/10\",\n  remove: \"bg-rose-50 dark:bg-rose-500\/10\",\n  replaceBefore: \"bg-rose-50 dark:bg-rose-500\/10\",\n  replaceAfter: \"bg-emerald-50 dark:bg-emerald-500\/10\",\n};\n\nfunction HunkRow({\n  hunk,\n  mode,\n  status,\n  pendingMode,\n  interactive,\n  showGutter,\n  renderGutterCell,\n  accept,\n  reject,\n}: HunkRowProps) {\n  const isChange = hunk.type !== \"equal\";\n  const dimmed = interactive && isChange && status === \"rejected\";\n\n  return (\n    <div\n      data-fancy-diff-hunk={hunk.id}\n      data-fancy-diff-hunk-type={hunk.type}\n      data-fancy-diff-hunk-status={isChange && interactive ? status : undefined}\n      role=\"row\"\n      className={cx(\n        \"group relative\",\n        dimmed && \"opacity-50\",\n        isChange && \"border-l-2\",\n        \/\/ review: the left border tracks acceptance status.\n        interactive && isChange && status === \"accepted\" && \"border-emerald-400\",\n        interactive && isChange && status === \"rejected\" && \"border-rose-300\",\n        interactive && isChange && status === \"pending\" && \"border-amber-300\",\n        \/\/ compare: no acceptance \u2014 the border marks the change *type* instead.\n        !interactive && hunk.type === \"add\" && \"border-emerald-400\",\n        !interactive && hunk.type === \"remove\" && \"border-rose-300\",\n        !interactive && hunk.type === \"replace\" && \"border-amber-300\",\n        !isChange && \"border-l-2 border-transparent\",\n      )}\n    >\n      {mode === \"split\" && hunk.type === \"replace\" ? (\n        <SplitReplace hunk={hunk} showGutter={showGutter} renderGutterCell={renderGutterCell} \/>\n      ) : mode === \"split\" ? (\n        <SplitSimple hunk={hunk} showGutter={showGutter} renderGutterCell={renderGutterCell} \/>\n      ) : (\n        <Inline hunk={hunk} showGutter={showGutter} renderGutterCell={renderGutterCell} \/>\n      )}\n\n      {isChange && interactive && (\n        <div\n          data-fancy-diff-hunk-actions=\"\"\n          className=\"absolute right-1.5 top-1\/2 -translate-y-1\/2 flex gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity\"\n        >\n          <Button\n            size=\"xs\"\n            variant=\"circle\"\n            icon=\"check\"\n            color={status === \"accepted\" ? \"emerald\" : undefined}\n            checked={status === \"accepted\"}\n            aria-label={pendingMode ? \"Propose accept\" : \"Accept change\"}\n            title={pendingMode ? \"Propose accept\" : \"Accept change\"}\n            onClick={accept}\n            data-fancy-diff-accept={hunk.id}\n          \/>\n          <Button\n            size=\"xs\"\n            variant=\"circle\"\n            icon=\"x\"\n            warn={status === \"rejected\"}\n            aria-label={pendingMode ? \"Propose reject\" : \"Reject change\"}\n            title={pendingMode ? \"Propose reject\" : \"Reject change\"}\n            onClick={reject}\n            data-fancy-diff-reject={hunk.id}\n          \/>\n        <\/div>\n      )}\n    <\/div>\n  );\n}\n\nfunction gutterArgs(\n  side: \"before\" | \"after\" | \"both\",\n  beforeLineNo?: number,\n  afterLineNo?: number,\n): GutterRenderArgs {\n  return { side, beforeLineNo, afterLineNo };\n}\n\n\/** Split view for add\/remove\/equal hunks (no intra-line segments). *\/\nfunction SplitSimple({\n  hunk,\n  showGutter,\n  renderGutterCell,\n}: {\n  hunk: Hunk;\n  showGutter: boolean;\n  renderGutterCell: (a: GutterRenderArgs) => ReactNode;\n}) {\n  return (\n    <>\n      {hunk.lines.map((line, i) => {\n        const leftText = line.side === \"before\" || line.side === \"both\" ? line.text : \"\";\n        const rightText = line.side === \"after\" || line.side === \"both\" ? line.text : \"\";\n        const leftFilled = line.side === \"before\" || line.side === \"both\";\n        const rightFilled = line.side === \"after\" || line.side === \"both\";\n        return (\n          <div key={i} className=\"grid grid-cols-2\" role=\"row\">\n            <div\n              className={cx(\n                \"flex items-start gap-1 px-2 py-0.5 whitespace-pre-wrap break-words min-h-[1.4em] border-r border-zinc-100 dark:border-zinc-800\",\n                line.side === \"before\" && sideBg.remove,\n              )}\n            >\n              {showGutter && renderGutterCell(gutterArgs(\"before\", line.beforeLineNo))}\n              {line.side === \"before\" && <span className=\"text-rose-500 select-none\">-<\/span>}\n              <span>{leftFilled ? leftText : \"\"}<\/span>\n            <\/div>\n            <div\n              className={cx(\n                \"flex items-start gap-1 px-2 py-0.5 whitespace-pre-wrap break-words min-h-[1.4em]\",\n                line.side === \"after\" && sideBg.add,\n              )}\n            >\n              {showGutter && renderGutterCell(gutterArgs(\"after\", undefined, line.afterLineNo))}\n              {line.side === \"after\" && <span className=\"text-emerald-500 select-none\">+<\/span>}\n              <span>{rightFilled ? rightText : \"\"}<\/span>\n            <\/div>\n          <\/div>\n        );\n      })}\n    <\/>\n  );\n}\n\n\/** Split view for replace hunks, pairing before\/after rows with segment highlights. *\/\nfunction SplitReplace({\n  hunk,\n  showGutter,\n  renderGutterCell,\n}: {\n  hunk: Hunk;\n  showGutter: boolean;\n  renderGutterCell: (a: GutterRenderArgs) => ReactNode;\n}) {\n  const before = hunk.lines.filter((l) => l.side === \"before\");\n  const after = hunk.lines.filter((l) => l.side === \"after\");\n  const rows = Math.max(before.length, after.length);\n  const segs = hunk.segments ?? [];\n\n  return (\n    <>\n      {Array.from({ length: rows }).map((_, i) => {\n        const b = before[i];\n        const a = after[i];\n        const seg = segs.find((s) => s.pairIndex === i);\n        return (\n          <div key={i} className=\"grid grid-cols-2\" role=\"row\">\n            <div\n              className={cx(\n                \"flex items-start gap-1 px-2 py-0.5 whitespace-pre-wrap break-words min-h-[1.4em] border-r border-zinc-100 dark:border-zinc-800\",\n                b && sideBg.replaceBefore,\n              )}\n            >\n              {showGutter && renderGutterCell(gutterArgs(\"before\", b?.beforeLineNo))}\n              {b && <span className=\"text-rose-500 select-none\">-<\/span>}\n              <span>{b ? (seg ? renderSegments(seg.before, \"remove\") : b.text) : \"\"}<\/span>\n            <\/div>\n            <div\n              className={cx(\n                \"flex items-start gap-1 px-2 py-0.5 whitespace-pre-wrap break-words min-h-[1.4em]\",\n                a && sideBg.replaceAfter,\n              )}\n            >\n              {showGutter && renderGutterCell(gutterArgs(\"after\", undefined, a?.afterLineNo))}\n              {a && <span className=\"text-emerald-500 select-none\">+<\/span>}\n              <span>{a ? (seg ? renderSegments(seg.after, \"add\") : a.text) : \"\"}<\/span>\n            <\/div>\n          <\/div>\n        );\n      })}\n    <\/>\n  );\n}\n\n\/** Inline (single-column) view. *\/\nfunction Inline({\n  hunk,\n  showGutter,\n  renderGutterCell,\n}: {\n  hunk: Hunk;\n  showGutter: boolean;\n  renderGutterCell: (a: GutterRenderArgs) => ReactNode;\n}) {\n  const segs = hunk.segments ?? [];\n  let removeIdx = 0;\n  let addIdx = 0;\n  return (\n    <>\n      {hunk.lines.map((line, i) => {\n        const sign = line.side === \"before\" ? \"-\" : line.side === \"after\" ? \"+\" : \" \";\n        const bg =\n          line.side === \"before\"\n            ? sideBg.remove\n            : line.side === \"after\"\n              ? sideBg.add\n              : \"\";\n        let content: ReactNode = line.text;\n        if (hunk.type === \"replace\") {\n          if (line.side === \"before\") {\n            const seg = segs.find((s) => s.pairIndex === removeIdx++);\n            if (seg) content = renderSegments(seg.before, \"remove\");\n          } else if (line.side === \"after\") {\n            const seg = segs.find((s) => s.pairIndex === addIdx++);\n            if (seg) content = renderSegments(seg.after, \"add\");\n          }\n        }\n        return (\n          <div\n            key={i}\n            role=\"row\"\n            className={cx(\n              \"flex items-start gap-1 px-2 py-0.5 whitespace-pre-wrap break-words min-h-[1.4em]\",\n              bg,\n            )}\n          >\n            {showGutter &&\n              renderGutterCell(\n                gutterArgs(line.side, line.beforeLineNo, line.afterLineNo),\n              )}\n            <span\n              className={cx(\n                \"select-none w-3\",\n                sign === \"-\" && \"text-rose-500\",\n                sign === \"+\" && \"text-emerald-500\",\n              )}\n            >\n              {sign}\n            <\/span>\n            <span>{content}<\/span>\n          <\/div>\n        );\n      })}\n    <\/>\n  );\n}\n\n\/** Render intra-line segments with add\/remove emphasis. *\/\nfunction renderSegments(segments: DiffSegment[], emphasis: \"add\" | \"remove\"): ReactNode {\n  return segments.map((s, i) => {\n    if (s.op === \"equal\") return <span key={i}>{s.text}<\/span>;\n    const cls =\n      emphasis === \"add\"\n        ? \"bg-emerald-200\/70 dark:bg-emerald-400\/25 rounded-sm\"\n        : \"bg-rose-200\/70 dark:bg-rose-400\/25 rounded-sm\";\n    return (\n      <span key={i} className={cls} data-fancy-diff-seg={s.op}>\n        {s.text}\n      <\/span>\n    );\n  });\n}\n","type":"registry:ui","target":"components\/fancy\/fancy-diff\/FancyDiff.tsx"}]}