{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "press-register",
  "title": "Press Register",
  "description": "An ambient conflict indicator staged as printing misregistration: a block with an unsynced collaborator prints a soft ghost plate of their version behind your text, offset by measured word-level divergence with matching SVG registration crosshairs at the corners, and resolving pulls the plates into register with a critically damped spring and a single crisp strike.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/press-register/component.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// PlateRegister — an ambient conflict indicator staged as printing\n// misregistration. While a collaborator holds unsynced changes to a block,\n// the paragraph prints slightly off-register: a ghost plate of THEIR version\n// sits behind your text, offset by the governing scalar `d` — word-level\n// edit distance between your text and theirs, normalized by your word count,\n// clamped 0..1. `d` maps linearly to a plate offset of 0-6px (+0.3deg skew),\n// capped at 6px because past that the doubling stops reading as \"how far\n// out of true\" and starts reading as noise. `d` is recomputed from the two\n// strings on every render that changes them — never from a clock — so a\n// collaborator who has been parked on an identical draft for an hour reads\n// as perfectly in register, and one who just typed a comma reads as barely\n// split at all.\n//\n// Four 8px SVG registration crosshairs sit at the block's corners, each one\n// two crosses at zero offset — the pressman's exact readout of the same\n// offset vector driving the ghost plate, legible even where the prose\n// itself is short enough that the text doubling alone is hard to judge.\n// The ghost text is intentionally soft (0.35 opacity, --ns-muted) so it\n// reads as peripheral texture while you're reading your own copy; the\n// crosshairs are full-strength on purpose — they're the instrument you\n// glance at to get the precise magnitude, not a texture.\n//\n// Resolving (Review -> keep mine / take theirs / merge) is irreversible for\n// the session: it drives `d` to 0 with a critically-damped-feeling spring\n// (k=280, zeta=0.95) over roughly 450ms, direct-DOM position, then fires one\n// 60ms full-contrast \"strike\" on the block — the plates landing as a single\n// crisp impression. Only your own text is ever in the accessibility tree\n// (the ghost plate and both crosshair marks are aria-hidden); the block\n// itself carries `aria-describedby` naming the collaborator and the word\n// count that differs, so the magnitude is available as text, never only as\n// a visual offset. `prefers-reduced-motion` skips the spring outright,\n// jumping straight to the 0-offset state and playing only the 60ms strike.\n// ---------------------------------------------------------------------------\n\nexport type PlateRegisterOutcome = \"kept\" | \"theirs\" | \"merged\";\n\nexport interface PlateRegisterProps {\n  /** your (locally synced) paragraph text */\n  text: string;\n  /** the collaborator's unsynced version of the same block */\n  theirText: string;\n  /** collaborator display name — named in the description and the dialog */\n  collaboratorName: string;\n  /** fires once, the moment a resolution is chosen */\n  onResolve?: (\n    outcome: PlateRegisterOutcome,\n    detail: { mine: string; theirs: string }\n  ) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst MAX_OFFSET_PX = 6;\nconst MAX_SKEW_DEG = 0.3;\nconst SPRING_K = 280;\nconst SPRING_ZETA = 0.95;\nconst SPRING_C = 2 * SPRING_ZETA * Math.sqrt(SPRING_K);\nconst SPRING_EPS = 0.002;\nconst SPRING_VEPS = 0.01;\nconst SPRING_MAX_MS = 900; // forced-settle deadline, never hangs mid-air\nconst STRIKE_MS = 60;\nconst STRIKE_CLASS = \"ns-pr-strike\";\nconst FOCUSABLE = 'button, [href], input, [tabindex]:not([tabindex=\"-1\"])';\n\nfunction clamp(v: number, lo: number, hi: number): number {\n  return v < lo ? lo : v > hi ? hi : v;\n}\n\nfunction tokenize(s: string): string[] {\n  const t = s.trim();\n  return t === \"\" ? [] : t.split(/\\s+/);\n}\n\n/** word-level Levenshtein distance — insertions, deletions, substitutions,\n * each cost 1. O(n*m), fine at paragraph length. */\nfunction wordEditDistance(a: string[], b: string[]): number {\n  const n = a.length;\n  const m = b.length;\n  const row = new Array<number>(m + 1);\n  for (let j = 0; j <= m; j++) row[j] = j;\n  for (let i = 1; i <= n; i++) {\n    let prevDiag = row[0]!;\n    row[0] = i;\n    for (let j = 1; j <= m; j++) {\n      const tmp = row[j]!;\n      row[j] = a[i - 1] === b[j - 1] ? prevDiag : 1 + Math.min(prevDiag, row[j]!, row[j - 1]!);\n      prevDiag = tmp;\n    }\n  }\n  return row[m]!;\n}\n\nfunction outcomeMessage(outcome: PlateRegisterOutcome, name: string): string {\n  if (outcome === \"kept\") return `Resolved — kept your version, out of register with ${name} no longer.`;\n  if (outcome === \"theirs\") return `Resolved — took ${name}'s version, plates back in register.`;\n  return `Resolved — merged with ${name}'s version, plates back in register.`;\n}\n\nfunction CrossMark({ ghost, dx, dy }: { ghost: boolean; dx: number; dy: number }) {\n  // 20x20 local viewBox, true cross fixed at (10,10); the ghost cross is the\n  // same 8px \"+\" translated by the plate's own offset vector — the corner\n  // readout of the exact same number driving the text doubling.\n  const x = ghost ? 10 + dx : 10;\n  const y = ghost ? 10 + dy : 10;\n  return (\n    <g>\n      <line\n        x1={x}\n        y1={y - 4}\n        x2={x}\n        y2={y + 4}\n        stroke={ghost ? \"var(--ns-muted)\" : \"var(--foreground)\"}\n        strokeWidth={1.1}\n        strokeLinecap=\"round\"\n      />\n      <line\n        x1={x - 4}\n        y1={y}\n        x2={x + 4}\n        y2={y}\n        stroke={ghost ? \"var(--ns-muted)\" : \"var(--foreground)\"}\n        strokeWidth={1.1}\n        strokeLinecap=\"round\"\n      />\n    </g>\n  );\n}\n\nfunction RegistrationMark({\n  corner,\n  dx,\n  dy,\n}: {\n  corner: \"tl\" | \"tr\" | \"bl\" | \"br\";\n  dx: number;\n  dy: number;\n}) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      width={20}\n      height={20}\n      viewBox=\"0 0 20 20\"\n      className={`ns-pr-mark ns-pr-mark-${corner}`}\n    >\n      <CrossMark ghost dx={dx} dy={dy} />\n      <CrossMark ghost={false} dx={0} dy={0} />\n    </svg>\n  );\n}\n\nexport function PlateRegister({\n  text,\n  theirText,\n  collaboratorName,\n  onResolve,\n  className = \"\",\n}: PlateRegisterProps) {\n  const [displayText, setDisplayText] = useState(text);\n\n  // divergence is measured, not stored: recomputed straight off the two\n  // strings every render they change, never off a clock. It only changes\n  // post-mount as a *result* of resolve() below, which sets displayText and\n  // the phase together — so there is no separate effect re-deriving phase\n  // from a moving dRaw, and no dependency-array staleness to reason about.\n  const { dist, dRaw } = useMemo(() => {\n    const mineWords = tokenize(displayText);\n    const theirWords = tokenize(theirText);\n    const distance = wordEditDistance(mineWords, theirWords);\n    const len = Math.max(mineWords.length, 1);\n    return { dist: distance, dRaw: clamp(distance / len, 0, 1) };\n  }, [displayText, theirText]);\n\n  const [phase, setPhase] = useState<\"conflict\" | \"resolving\" | \"clear\">(() =>\n    dRaw > 0 ? \"conflict\" : \"clear\"\n  );\n  const [springD, setSpringD] = useState(0);\n  const [dialogOpen, setDialogOpen] = useState(false);\n  const [announce, setAnnounce] = useState(\"\");\n\n  const decidedRef = useRef(false);\n  const springStartRef = useRef(0);\n  const blockRef = useRef<HTMLDivElement>(null);\n  const dialogRef = useRef<HTMLDialogElement>(null);\n  const reviewRef = useRef<HTMLButtonElement>(null);\n  const keepMineRef = useRef<HTMLButtonElement>(null);\n\n  const descId = useId();\n  const titleId = useId();\n\n  const d = phase === \"resolving\" ? springD : phase === \"conflict\" ? dRaw : 0;\n  const showMarks = phase !== \"clear\";\n\n  const offsetPx = d * MAX_OFFSET_PX;\n  const offsetXPx = offsetPx;\n  const offsetYPx = offsetPx * 0.4;\n  const skewDeg = d * MAX_SKEW_DEG;\n  const ghostTransform = `translate(${offsetXPx.toFixed(2)}px, ${offsetYPx.toFixed(2)}px) skewX(${skewDeg.toFixed(3)}deg)`;\n\n  const openDialog = useCallback(() => {\n    // showModal() throws if the dialog is already open — Review must stay\n    // idempotent under repeat clicks (the verifier presses the first\n    // interactive control, then a declared `gate` may click the same\n    // control again before checking the open state).\n    const dlg = dialogRef.current;\n    if (!dlg || dlg.open) return;\n    setDialogOpen(true);\n    dlg.showModal();\n  }, []);\n\n  const closeDialog = useCallback((focusTrigger: boolean) => {\n    setDialogOpen(false);\n    dialogRef.current?.close();\n    if (focusTrigger) reviewRef.current?.focus();\n  }, []);\n\n  const runStrike = useCallback(() => {\n    const el = blockRef.current;\n    if (el) {\n      el.classList.remove(STRIKE_CLASS);\n      void el.offsetWidth; // force reflow so the flash retriggers reliably\n      el.classList.add(STRIKE_CLASS);\n      window.setTimeout(() => el.classList.remove(STRIKE_CLASS), STRIKE_MS);\n    }\n    // native <dialog>.close() restores focus to whatever had it before\n    // showModal() — the Review button — and that button is about to unmount\n    // with the marks below. Catch focus before it's dropped to <body>.\n    if (document.activeElement === reviewRef.current) el?.focus();\n    setPhase(\"clear\");\n  }, []);\n\n  const resolve = useCallback(\n    (outcome: PlateRegisterOutcome) => {\n      if (decidedRef.current) return; // irreversible: first decision wins\n      decidedRef.current = true;\n      const nextText = outcome === \"theirs\" ? theirText : text;\n      setDisplayText(nextText);\n      setAnnounce(outcomeMessage(outcome, collaboratorName));\n      onResolve?.(outcome, { mine: text, theirs: theirText });\n      // no refocus here (unlike Escape/backdrop dismissal below): the Review\n      // button itself unmounts ~510ms from now when the block clears, and\n      // parking focus on a control that's about to vanish just drops it to\n      // <body>. The block stays put and the aria-live announcement already\n      // carries the outcome, so leaving focus where it is is the safer call.\n      closeDialog(false);\n\n      const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n      if (reduced) {\n        setSpringD(0);\n        setPhase(\"resolving\");\n        // one microtask so the 0-offset frame actually paints before strike\n        requestAnimationFrame(() => runStrike());\n        return;\n      }\n      springStartRef.current = dRaw;\n      setSpringD(dRaw);\n      setPhase(\"resolving\");\n    },\n    [text, theirText, collaboratorName, onResolve, closeDialog, dRaw, runStrike]\n  );\n\n  // critically-damped-feeling spring driving springD -> 0 once resolving.\n  useEffect(() => {\n    if (phase !== \"resolving\") return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (reduced) return; // handled synchronously in resolve()\n\n    let raf = 0;\n    let last = performance.now();\n    let x = springStartRef.current;\n    let v = 0;\n    const deadline = last + SPRING_MAX_MS;\n\n    const tick = (now: number) => {\n      const dt = Math.min((now - last) / 1000, 1 / 30);\n      last = now;\n      const accel = -SPRING_K * x - SPRING_C * v;\n      v += accel * dt;\n      x += v * dt;\n      const settled = Math.abs(x) < SPRING_EPS && Math.abs(v) < SPRING_VEPS;\n      if (settled || now >= deadline) {\n        setSpringD(0);\n        runStrike();\n        return;\n      }\n      setSpringD(clamp(x, 0, 1));\n      raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [phase, runStrike]);\n\n  // hand-rolled focus trap + outside-dismiss for the native <dialog>\n  useEffect(() => {\n    if (!dialogOpen) return;\n    const dlg = dialogRef.current;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"Tab\" || !dlg) return;\n      const focusable = Array.from(dlg.querySelectorAll<HTMLElement>(FOCUSABLE));\n      if (focusable.length === 0) return;\n      const first = focusable[0]!;\n      const last = focusable[focusable.length - 1]!;\n      if (e.shiftKey && document.activeElement === first) {\n        e.preventDefault();\n        last.focus();\n      } else if (!e.shiftKey && document.activeElement === last) {\n        e.preventDefault();\n        first.focus();\n      }\n    };\n    const onCancel = (e: Event) => {\n      e.preventDefault();\n      closeDialog(true);\n    };\n    const onPointerDown = (e: PointerEvent) => {\n      if (e.target === dlg) closeDialog(false);\n    };\n    document.addEventListener(\"keydown\", onKey);\n    dlg?.addEventListener(\"cancel\", onCancel);\n    dlg?.addEventListener(\"pointerdown\", onPointerDown);\n    keepMineRef.current?.focus();\n    return () => {\n      document.removeEventListener(\"keydown\", onKey);\n      dlg?.removeEventListener(\"cancel\", onCancel);\n      dlg?.removeEventListener(\"pointerdown\", onPointerDown);\n    };\n  }, [dialogOpen, closeDialog]);\n\n  return (\n    <div className={`relative ${className}`}>\n      <style>{CSS}</style>\n\n      <div\n        ref={blockRef}\n        tabIndex={-1}\n        className=\"ns-pr-block relative rounded-md border border-border bg-background p-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n        aria-describedby={showMarks ? descId : undefined}\n      >\n        <div className=\"ns-pr-stack\">\n          <p className=\"ns-pr-mine m-0 text-sm leading-relaxed text-foreground\">{displayText}</p>\n          {showMarks && (\n            <p\n              aria-hidden=\"true\"\n              className=\"ns-pr-ghost m-0 text-sm leading-relaxed\"\n              style={{ transform: ghostTransform, opacity: 0.35, color: \"var(--ns-muted)\" }}\n            >\n              {theirText}\n            </p>\n          )}\n        </div>\n\n        {showMarks && (\n          <>\n            <RegistrationMark corner=\"tl\" dx={offsetXPx} dy={offsetYPx} />\n            <RegistrationMark corner=\"tr\" dx={offsetXPx} dy={offsetYPx} />\n            <RegistrationMark corner=\"bl\" dx={offsetXPx} dy={offsetYPx} />\n            <RegistrationMark corner=\"br\" dx={offsetXPx} dy={offsetYPx} />\n          </>\n        )}\n      </div>\n\n      {showMarks && (\n        <>\n          <p id={descId} className=\"sr-only\">\n            {collaboratorName} has unsynced changes to this paragraph, {dist} word{dist === 1 ? \"\" : \"s\"} differ.\n          </p>\n          <div className=\"mt-2 flex items-center gap-2\">\n            <button\n              ref={reviewRef}\n              type=\"button\"\n              data-pr-review\n              onClick={openDialog}\n              className=\"ns-pr-review inline-flex items-center gap-1.5 rounded-sm border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:border-ns-accent hover:text-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n            >\n              Review changes\n            </button>\n            {/* the magnitude read without leaving the paragraph — the ≤6px\n                offset alone can't distinguish a comma from a rewrite, this can */}\n            <span aria-hidden=\"true\" className=\"font-mono text-[11px] text-ns-muted\">\n              {collaboratorName} · {dist} word{dist === 1 ? \"\" : \"s\"}\n            </span>\n          </div>\n        </>\n      )}\n\n      <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n\n      <dialog\n        ref={dialogRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-labelledby={titleId}\n        className=\"ns-pr-dialog\"\n      >\n        <h2 id={titleId} className=\"m-0 text-sm font-medium text-foreground\">\n          Unsynced changes from {collaboratorName}\n        </h2>\n        <p className=\"mb-0 mt-1.5 text-xs leading-relaxed text-ns-muted\">\n          {dist} word{dist === 1 ? \"\" : \"s\"} differ from your version of this paragraph.\n        </p>\n        <div className=\"mt-4 rounded-sm border border-border bg-background p-2.5 font-mono text-xs leading-relaxed text-ns-muted\">\n          {theirText}\n        </div>\n        <div className=\"mt-4 flex flex-wrap justify-end gap-2\">\n          <button\n            type=\"button\"\n            onClick={() => resolve(\"merged\")}\n            className=\"ns-pr-btn rounded-sm border border-border bg-background px-3 py-1.5 text-xs text-foreground transition-colors hover:border-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            Merge\n          </button>\n          <button\n            type=\"button\"\n            onClick={() => resolve(\"theirs\")}\n            className=\"ns-pr-btn rounded-sm border border-border bg-background px-3 py-1.5 text-xs text-foreground transition-colors hover:border-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            Take theirs\n          </button>\n          <button\n            ref={keepMineRef}\n            type=\"button\"\n            onClick={() => resolve(\"kept\")}\n            className=\"ns-pr-btn rounded-sm border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:border-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            Keep mine\n          </button>\n        </div>\n      </dialog>\n    </div>\n  );\n}\n\nconst CSS = `\n.ns-pr-stack{display:grid;}\n.ns-pr-stack > *{grid-area:1/1;}\n/* the reader's own text stays the top plate; the ghost is unpositioned\n   (static) and painted first in DOM order, so without this it would\n   composite OVER the real text instead of behind it. */\n.ns-pr-mine{position:relative;z-index:1;}\n.ns-pr-ghost{pointer-events:none;will-change:transform;}\n.ns-pr-mark{position:absolute;pointer-events:none;overflow:visible;}\n.ns-pr-mark-tl{top:-10px;left:-10px;}\n.ns-pr-mark-tr{top:-10px;right:-10px;}\n.ns-pr-mark-bl{bottom:-10px;left:-10px;}\n.ns-pr-mark-br{bottom:-10px;right:-10px;}\n\n@keyframes ns-pr-strike{\n  0%{filter:contrast(1) brightness(1);}\n  40%{filter:contrast(1.2) brightness(1.06);}\n  100%{filter:contrast(1) brightness(1);}\n}\n/* Deliberately NOT gated behind prefers-reduced-motion: this is the single\n   60ms acknowledgement frame the spec calls out as the reduced-motion\n   resolution itself (\"resolves with the strike frame alone\") — a filter\n   flash on existing pixels, not a translate/scale motion. */\n.ns-pr-strike{animation:ns-pr-strike ${STRIKE_MS}ms ease-out;}\n\ndialog.ns-pr-dialog{\n  /* A host that resets margin to 0 on every element (Tailwind preflight,\n     most CSS resets) kills the UA stylesheet's own <dialog> centering,\n     which depends on margin:auto with an implicit inset. Pin position and\n     centering explicitly instead of relying on that default. */\n  position:fixed;\n  top:50%;\n  left:50%;\n  margin:0;\n  transform:translate(-50%, -50%) scale(0.96);\n  border:1px solid var(--border);\n  border-radius:12px;\n  background:var(--background);\n  color:var(--foreground);\n  padding:16px;\n  width:min(26rem, calc(100vw - 2rem));\n  opacity:0;\n  transition:opacity 180ms cubic-bezier(0.16,1,0.3,1), transform 180ms cubic-bezier(0.16,1,0.3,1), overlay 180ms allow-discrete, display 180ms allow-discrete;\n}\ndialog.ns-pr-dialog[open]{\n  opacity:1;\n  transform:translate(-50%, -50%) scale(1);\n}\n@starting-style{\n  dialog.ns-pr-dialog[open]{\n    opacity:0;\n    transform:translate(-50%, -50%) scale(0.96);\n  }\n}\n/* Never a hardcoded grey: the scrim is a translucent wash of --background\n   itself (not --foreground), so it reads as a soft dim of the page in\n   whichever theme is active rather than a flat mid-grey overlay, with a\n   blur for the \"more smooth\" ask instead of a harder darkening trick. */\ndialog.ns-pr-dialog::backdrop{\n  background:color-mix(in srgb, var(--background) 55%, transparent);\n  backdrop-filter:blur(6px);\n  -webkit-backdrop-filter:blur(6px);\n  opacity:0;\n  transition:opacity 180ms cubic-bezier(0.16,1,0.3,1), display 180ms allow-discrete, overlay 180ms allow-discrete;\n}\ndialog.ns-pr-dialog[open]::backdrop{\n  opacity:1;\n}\n@starting-style{\n  dialog.ns-pr-dialog[open]::backdrop{\n    opacity:0;\n  }\n}\n@media (prefers-reduced-motion: reduce){\n  dialog.ns-pr-dialog{\n    transition:opacity 120ms linear;\n    transform:translate(-50%, -50%);\n  }\n  dialog.ns-pr-dialog[open]{\n    transform:translate(-50%, -50%);\n  }\n  @starting-style{\n    dialog.ns-pr-dialog[open]{\n      transform:translate(-50%, -50%);\n    }\n  }\n  dialog.ns-pr-dialog::backdrop{\n    transition:opacity 120ms linear;\n  }\n}\n`;\n",
      "type": "registry:ui",
      "target": "components/ui/press-register.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "conflict",
      "collaboration",
      "diff",
      "dialog",
      "svg",
      "spring-physics",
      "aria-live",
      "word-diff"
    ],
    "instruction": "Build `PlateRegister({ text, theirText, collaboratorName, onResolve?, className? })`: a paragraph block that renders `text` (yours, always full opacity, always the only version in the accessibility tree) and, whenever it differs from `theirText` (the collaborator's unsynced draft of the same block), a ghost plate of `theirText` stacked directly behind it via CSS grid (`display:grid`, both `<p>`s sharing `grid-area:1/1` so the container auto-sizes to whichever is taller — no absolute positioning, no clipping). The governing scalar is `d`: word-level edit distance between the two strings (standard Levenshtein over `.split(/\\s+/)` tokens, insertions/deletions/substitutions each cost 1) normalized by your own word count, clamped 0..1. `d` maps linearly to a plate offset of 0-6px (`translate(d*6px, d*6px*0.4)`) plus `d*0.3deg` of `skewX` on the ghost only — hard-capped at 6px/0.3deg because past that the doubling stops reading as a measurable warning and starts reading as noise, and the offset is computed fresh from the two strings on every render that changes them, NEVER derived from time-since-desync (a collaborator parked on an identical draft for an hour must read as perfectly in register, not increasingly stale). The ghost renders `aria-hidden`, at 0.35 opacity, colored `var(--ns-muted)`. Four 8px SVG registration crosshairs — printer's alignment marks — sit just outside the block's four corners, each one a 'true' cross fixed in `var(--foreground)` plus a 'ghost' cross in `var(--ns-muted)` translated by that same (dx, dy) offset vector: the pressman's exact readout of how far out of true the plates are, rendered at full stroke strength (not softened like the ghost text) because it's the precise instrument, not ambient texture. All four marks and the ghost text disappear together once resolved — a single crisp block, no marks. When `d > 0`, render a real `aria-describedby` on the block pointing at sr-only text reading '{collaboratorName} has unsynced changes to this paragraph, {N} word(s) differ' (N = the raw edit-distance integer, not the normalized/capped d — magnitude must be legible as text, never left to the visual offset alone) and a real 'Review changes' button (`data-pr-review`) that opens a plain native `<dialog>` (`showModal()`, hand-rolled Tab-cycle focus trap, Escape/backdrop-click both close without deciding) showing their full paragraph text and three buttons: Keep mine, Take theirs, Merge. The dialog is explicitly centered with `position:fixed; top/left:50%; transform:translate(-50%,-50%)` rather than relying on the UA stylesheet's `margin:auto` default, which a host's own `margin:0` reset (Tailwind preflight and most resets apply one globally) silently defeats. It fades and scales in from `@starting-style` (a short honest transition under `prefers-reduced-motion`, opacity-only and not a frozen snap) rather than the UA's hard cut, and its `::backdrop` is a blurred, translucent wash of `var(--background)` — never `var(--foreground)` or a literal grey — so the scrim reads as a soft dim of the page in whichever theme is active instead of a flat grey plate. The button that opens the dialog must be idempotent (`showModal()` again if already open, never a toggle-closed), since the registry's autoplay/verify harness presses the first interactive control before checking any declared open-state gate. Any of the three buttons is a one-time, irreversible decision for the component's lifetime (guard with a ref, first call wins): 'Take theirs' swaps the displayed text to `theirText` (so the measured divergence naturally becomes 0 — texts are now identical); 'Keep mine' and 'Merge' leave the displayed text as `text` and instead flip an explicit `resolved` flag that forces the visual marks to clear regardless of any remaining textual difference (a decision here is about the block's sync state, not a promise the strings become byte-identical — real merge logic is the caller's concern via `onResolve(outcome, {mine, theirs})`, fired once at the moment of decision). Whichever button is pressed: close the dialog, then drive the offset from its current value to 0 with a critically-damped-feeling spring (stiffness 280, damping ratio 0.95, i.e. `c = 2*0.95*sqrt(280)`, integrated `accel = -k*x - c*v` every rAF frame) over roughly 450ms, and the instant it settles (or a 900ms safety deadline elapses) fire one 60ms 'strike': a `filter: contrast(1.2) brightness(1.06)` flash on the whole block via a retriggerable CSS class (force a reflow before re-adding it), then unmount the ghost/crosshairs entirely — the plates landing as one impression. A single `role=status aria-live=polite` sr-only region announces the outcome exactly once at decision time (e.g. 'Resolved — took Nadia's version, plates back in register'), independent of whether the spring is actually running. `prefers-reduced-motion: reduce` skips the spring outright: jump the offset straight to 0 and play only the 60ms strike frame (the strike itself is intentionally NOT gated behind reduced-motion — it's a single acknowledgement frame on already-rendered pixels, not a translate/scale motion, and the spec calls it out as the reduced-motion resolution in its own right). Idle rendering (before any resolve) is never animated regardless of reduced-motion, by construction: the offset is a plain per-render calculation, not a running loop, so there is nothing to gate. Zero dependencies; DOM + SVG + CSS only, no canvas — every ink comes from `var(--background)`, `var(--foreground)`, `var(--ns-muted)`, `var(--border)`, or `var(--ns-accent)` (accent reserved for hover/focus-visible states on the Review and dialog buttons, never a resting fill)."
  },
  "type": "registry:ui"
}