{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-doc-core",
  "title": "Doc core primitive",
  "description": "Documents for contract review and docs: a paragraph/clause model with numbering and a text parser, word redlines on the diff package grouped into accept-or-reject changes, block-level version diffs, DiffHunk-shaped line hunks, and durable text anchors for comments and AI suggestions that survive edits.",
  "dependencies": [
    "diff"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/doc-core/anchors.ts",
      "content": "import { redline, type RedlineSegment } from \"./redline\";\nimport { blockById, type Doc } from \"./types\";\n\n/**\n * A durable pointer at a span of one block's text, for comments and AI suggestions. It keeps\n * offsets for speed and a quote with context (like the W3C TextQuoteSelector) so it can be\n * found again after the text changes.\n */\nexport type TextAnchor = {\n  id: string;\n  blockId: string;\n  start: number;\n  end: number;\n  /** The anchored text when created. */\n  quote: string;\n  /** Up to `context` characters either side, to disambiguate repeated phrases. */\n  prefix: string;\n  suffix: string;\n};\n\nexport type Comment = TextAnchor & {\n  kind: \"comment\";\n  author: string;\n  body: string;\n  /** Epoch ms. */\n  createdAt: number;\n  resolved?: boolean;\n  /** Replies, oldest first. */\n  replies?: { id: string; author: string; body: string; createdAt: number }[];\n};\n\nexport type Suggestion = TextAnchor & {\n  kind: \"suggestion\";\n  /** The text proposed in place of `quote` (empty to delete it). */\n  replacement: string;\n  /** Why: \"Caps liability at 12 months' fees, per playbook §3.\" */\n  rationale?: string;\n  author?: string;\n  /** 0–1, for AI suggestions. */\n  confidence?: number;\n  status: \"pending\" | \"accepted\" | \"rejected\";\n};\n\n/** An anchor over `block.text.slice(start, end)`. */\nexport function createAnchor(doc: Doc, blockId: string, start: number, end: number, { id, context = 24 }: { id: string; context?: number }): TextAnchor {\n  const text = blockById(doc, blockId)?.text ?? \"\";\n  const s = Math.max(0, Math.min(start, end, text.length));\n  const e = Math.min(text.length, Math.max(start, end));\n  return { id, blockId, start: s, end: e, quote: text.slice(s, e), prefix: text.slice(Math.max(0, s - context), s), suffix: text.slice(e, e + context) };\n}\n\n/** An anchor over the first occurrence of `quote` in a block (after `from`): handy for fixtures and AI output that cites text. */\nexport function anchorQuote(doc: Doc, blockId: string, quote: string, { id, from = 0, context }: { id: string; from?: number; context?: number }): TextAnchor | null {\n  const text = blockById(doc, blockId)?.text ?? \"\";\n  const i = text.indexOf(quote, from);\n  return i < 0 ? null : createAnchor(doc, blockId, i, i + quote.length, { id, context });\n}\n\nexport type ResolvedAnchor = { blockId: string; start: number; end: number; status: \"exact\" | \"moved\" | \"orphaned\" };\n\n/**\n * Where an anchor points in `doc` now: `exact` when the offsets still hold the quote, `moved`\n * when the quote was found elsewhere (the occurrence whose context matches best, nearest the\n * old offset), `orphaned` when the quoted text is gone (show it as \"on deleted text\").\n */\nexport function resolveAnchor(doc: Doc, anchor: TextAnchor): ResolvedAnchor {\n  const home = blockById(doc, anchor.blockId);\n  if (home && home.text.slice(anchor.start, anchor.end) === anchor.quote) return { blockId: home.id, start: anchor.start, end: anchor.end, status: \"exact\" };\n  if (!anchor.quote) return { blockId: anchor.blockId, start: 0, end: 0, status: \"orphaned\" };\n  const candidates = home ? [home, ...doc.blocks.filter((b) => b !== home)] : doc.blocks;\n  let best: { blockId: string; start: number; score: number } | null = null;\n  for (const b of candidates) {\n    for (let i = b.text.indexOf(anchor.quote); i >= 0; i = b.text.indexOf(anchor.quote, i + 1)) {\n      const before = b.text.slice(Math.max(0, i - anchor.prefix.length), i);\n      const after = b.text.slice(i + anchor.quote.length, i + anchor.quote.length + anchor.suffix.length);\n      const score =\n        sharedSuffix(before, anchor.prefix) + sharedPrefix(after, anchor.suffix) + (b.id === anchor.blockId ? 1000 - Math.min(999, Math.abs(i - anchor.start)) / 1000 : 0);\n      if (!best || score > best.score) best = { blockId: b.id, start: i, score };\n    }\n  }\n  return best ? { blockId: best.blockId, start: best.start, end: best.start + anchor.quote.length, status: \"moved\" } : { blockId: anchor.blockId, start: 0, end: 0, status: \"orphaned\" };\n}\n\nconst sharedPrefix = (a: string, b: string) => {\n  let n = 0;\n  while (n < a.length && n < b.length && a[n] === b[n]) n++;\n  return n;\n};\nconst sharedSuffix = (a: string, b: string) => {\n  let n = 0;\n  while (n < a.length && n < b.length && a[a.length - 1 - n] === b[b.length - 1 - n]) n++;\n  return n;\n};\n\n/**\n * Maps an offset in the redline's \"before\" text to the \"after\" text. Offsets inside deleted\n * text land at the deletion point; `bias` decides which side of an insertion at the offset.\n */\nexport function mapOffset(offset: number, segments: readonly RedlineSegment[], bias: \"before\" | \"after\" = \"after\"): number {\n  let a = 0;\n  let b = 0;\n  for (const s of segments) {\n    if (s.kind === \"insert\") {\n      if (a < offset || (a === offset && bias === \"after\")) b += s.text.length;\n      continue;\n    }\n    const len = s.text.length;\n    if (offset < a + len) return s.kind === \"equal\" ? b + (offset - a) : b;\n    a += len;\n    if (s.kind === \"equal\") b += len;\n  }\n  return b;\n}\n\n/** An anchor moved through an edit of its block (offsets mapped, quote and context refreshed). */\nexport function rebaseAnchor<A extends TextAnchor>(anchor: A, before: string, after: string, segments = redline(before, after).segments): A {\n  const start = mapOffset(anchor.start, segments, \"after\");\n  const end = Math.max(start, mapOffset(anchor.end, segments, \"before\"));\n  const context = Math.max(anchor.prefix.length, anchor.suffix.length, 24);\n  return { ...anchor, start, end, quote: after.slice(start, end), prefix: after.slice(Math.max(0, start - context), start), suffix: after.slice(end, end + context) };\n}\n\nexport type MarkedRun = { text: string; start: number; end: number; ids: string[] };\n\n/**\n * A block's text cut into runs at every anchor edge, each run listing the anchors covering\n * it: render runs with `ids.length` as `<mark>` (overlaps nest naturally as multiple ids).\n */\nexport function splitByAnchors(text: string, anchors: readonly { id: string; start: number; end: number }[]): MarkedRun[] {\n  const cuts = new Set([0, text.length]);\n  for (const a of anchors) {\n    cuts.add(Math.max(0, Math.min(text.length, a.start)));\n    cuts.add(Math.max(0, Math.min(text.length, a.end)));\n  }\n  const points = [...cuts].sort((x, y) => x - y);\n  const runs: MarkedRun[] = [];\n  for (let i = 0; i < points.length - 1; i++) {\n    const [s, e] = [points[i], points[i + 1]];\n    if (e <= s) continue;\n    runs.push({ text: text.slice(s, e), start: s, end: e, ids: anchors.filter((a) => a.start < e && a.end > s).map((a) => a.id) });\n  }\n  return runs;\n}\n\n/**\n * The anchored span of a DOM selection inside one rendered block. Blocks must render their\n * text verbatim inside an element with `data-doc-block={block.id}` (marks and ins are fine;\n * extra text such as clause numbers must sit outside it). Null for collapsed or cross-block\n * selections.\n */\nexport function anchorFromSelection(selection: Selection | null = typeof window === \"undefined\" ? null : window.getSelection()): { blockId: string; start: number; end: number; quote: string } | null {\n  if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;\n  const range = selection.getRangeAt(0);\n  const blockOf = (n: Node) => (n.nodeType === Node.ELEMENT_NODE ? (n as Element) : n.parentElement)?.closest<HTMLElement>(\"[data-doc-block]\") ?? null;\n  const block = blockOf(range.startContainer);\n  if (!block || block !== blockOf(range.endContainer)) return null;\n  const offsetOf = (node: Node, offset: number) => {\n    const r = document.createRange();\n    r.selectNodeContents(block);\n    r.setEnd(node, offset);\n    return r.toString().length;\n  };\n  const start = offsetOf(range.startContainer, range.startOffset);\n  const end = offsetOf(range.endContainer, range.endOffset);\n  return { blockId: block.dataset.docBlock!, start, end, quote: range.toString() };\n}\n\n/** The block text with a suggestion applied, and its redline: render it inline or in an Approval Card. */\nexport function suggestionPreview(doc: Doc, s: Suggestion) {\n  const block = blockById(doc, s.blockId);\n  const at = resolveAnchor(doc, s);\n  const before = block?.text ?? \"\";\n  const after = at.status === \"orphaned\" ? before : before.slice(0, at.start) + s.replacement + before.slice(at.end);\n  return { before, after, anchor: at, redline: redline(before, after, { idPrefix: `${s.id}:` }) };\n}\n\n/**\n * Accepts a suggestion: a new document with the replacement in place, and every other anchor\n * (comments, other suggestions) on that block rebased through the edit so it still points at\n * its text.\n */\nexport function applySuggestion<A extends TextAnchor>(doc: Doc, s: Suggestion, others: readonly A[] = []): { doc: Doc; anchors: A[] } {\n  const { before, after, anchor, redline: r } = suggestionPreview(doc, s);\n  if (anchor.status === \"orphaned\") return { doc, anchors: [...others] };\n  const next: Doc = { ...doc, blocks: doc.blocks.map((b) => (b.id === anchor.blockId ? { ...b, text: after } : b)) };\n  const anchors = others.map((a) => (a.blockId === anchor.blockId && a.id !== s.id ? rebaseAnchor(a, before, after, r.segments) : a));\n  return { doc: next, anchors };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/doc-core/anchors.ts"
    },
    {
      "path": "src/registry/eased/_primitives/doc-core/doc-core.ts",
      "content": "/**\n * doc-core: documents for contract review and docs. A paragraph/clause model, inline redlines\n * (insert/delete, grouped into accept-or-reject changes) on the `diff` package, block-level\n * version diffs, and durable anchors for comments and AI suggestions.\n * Import from here; the files behind it may move.\n */\nexport { blockById, docFromText, numberClauses, type Doc, type DocBlock, type DocBlockKind, type Risk } from \"./types\";\nexport {\n  diffDocs,\n  lineHunks,\n  redline,\n  resolveRedline,\n  type BlockDiff,\n  type Decision,\n  type Redline,\n  type RedlineChange,\n  type RedlineOptions,\n  type RedlineSegment,\n} from \"./redline\";\nexport {\n  anchorFromSelection,\n  anchorQuote,\n  applySuggestion,\n  createAnchor,\n  mapOffset,\n  rebaseAnchor,\n  resolveAnchor,\n  splitByAnchors,\n  suggestionPreview,\n  type Comment,\n  type MarkedRun,\n  type ResolvedAnchor,\n  type Suggestion,\n  type TextAnchor,\n} from \"./anchors\";\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/doc-core/doc-core.ts"
    },
    {
      "path": "src/registry/eased/_primitives/doc-core/redline.ts",
      "content": "import { diffChars, diffSentences, diffWords, diffWordsWithSpace, structuredPatch, type ChangeObject } from \"diff\";\nimport type { Doc, DocBlock } from \"./types\";\n\n/** A run of text in an inline redline. Rendered as plain, `<ins>` or `<del>`. */\nexport type RedlineSegment = {\n  kind: \"equal\" | \"insert\" | \"delete\";\n  text: string;\n  /** Inserts and deletes that form one edit share a change id (a replacement is a delete + an insert). */\n  changeId?: string;\n};\n\n/** One reviewable edit: accept or reject it as a unit. */\nexport type RedlineChange = {\n  id: string;\n  kind: \"insert\" | \"delete\" | \"replace\";\n  deleted: string;\n  inserted: string;\n};\n\nexport type Redline = {\n  segments: RedlineSegment[];\n  changes: RedlineChange[];\n  /** Words added and removed, for \"+12 −3\". */\n  stats: { inserted: number; deleted: number; changes: number };\n};\n\nexport type RedlineOptions = {\n  /** `word` (default) reads like a lawyer's redline; `char` for codes and figures; `sentence` for long rewrites. */\n  granularity?: \"word\" | \"char\" | \"sentence\";\n  /** Word boundaries follow this locale's `Intl.Segmenter` (so \"1.5%\" changes as \"1.5\", not \".5\"). Default the runtime's. */\n  locale?: string;\n  /** Prefix for change ids, so two blocks' changes stay unique: `${prefix}${n}`. Default `c`. */\n  idPrefix?: string;\n};\n\nconst words = (s: string) => (s.match(/\\S+/g) ?? []).length;\n\n/**\n * An inline redline between two strings on the `diff` package: segments for rendering, and\n * the edits grouped into changes you can accept or reject. A delete next to an insert is one\n * replacement, and up to three characters of space or punctuation between two edits are folded\n * in, so \"three (3) months\" → \"twelve (12) months\" reads as one change, not two.\n *\n *   const { segments, changes, stats } = redline(clause.text, revised.text);\n */\nexport function redline(before: string, after: string, { granularity = \"word\", idPrefix = \"c\", locale }: RedlineOptions = {}): Redline {\n  const raw: ChangeObject<string>[] =\n    granularity === \"char\" ? diffChars(before, after) : granularity === \"sentence\" ? diffSentences(before, after) : diffByWords(before, after, locale);\n\n  // Group into runs: equal text, or a change (deleted + inserted), folding short space/punctuation equals between two changes.\n  type Run = { equal: string } | { deleted: string; inserted: string };\n  const runs: Run[] = [];\n  for (let i = 0; i < raw.length; i++) {\n    const p = raw[i];\n    const last = runs[runs.length - 1];\n    if (!p.added && !p.removed) {\n      const next = raw[i + 1];\n      const bridges = last && \"deleted\" in last && next && (next.added || next.removed) && /^[\\s\\p{P}]{1,3}$/u.test(p.value);\n      if (bridges) {\n        last.deleted += p.value;\n        last.inserted += p.value;\n      } else runs.push({ equal: p.value });\n      continue;\n    }\n    if (!last || \"equal\" in last) runs.push({ deleted: \"\", inserted: \"\" });\n    const run = runs[runs.length - 1] as { deleted: string; inserted: string };\n    if (p.removed) run.deleted += p.value;\n    else run.inserted += p.value;\n  }\n\n  // A pure insert or delete takes the space before it with it, so rejecting \"days of notice.\"\n  // gives \"days.\" and not \"days .\" (word diffs leave that space in the equal text).\n  for (let i = 1; i < runs.length; i++) {\n    const run = runs[i];\n    const prev = runs[i - 1];\n    const next = runs[i + 1];\n    if (\"equal\" in run || \"deleted\" in prev || (run.deleted && run.inserted)) continue;\n    const text = run.deleted || run.inserted;\n    const ws = /\\s+$/.exec(prev.equal)?.[0];\n    if (!ws || /\\s$/.test(text) || (next && \"equal\" in next && /^\\s/.test(next.equal))) continue;\n    prev.equal = prev.equal.slice(0, -ws.length);\n    if (run.deleted) run.deleted = ws + run.deleted;\n    else run.inserted = ws + run.inserted;\n  }\n\n  const segments: RedlineSegment[] = [];\n  const changes: RedlineChange[] = [];\n  let ins = 0;\n  let del = 0;\n  for (const run of runs) {\n    if (\"equal\" in run) {\n      segments.push({ kind: \"equal\", text: run.equal });\n      continue;\n    }\n    // Trim the whitespace both sides share at the ends back out into plain text.\n    let { deleted, inserted } = run;\n    const lead = commonEdge(deleted, inserted, \"start\");\n    const trail = commonEdge(deleted.slice(lead.length), inserted.slice(lead.length), \"end\");\n    deleted = deleted.slice(lead.length, deleted.length - trail.length);\n    inserted = inserted.slice(lead.length, inserted.length - trail.length);\n    if (lead) segments.push({ kind: \"equal\", text: lead });\n    const id = `${idPrefix}${changes.length + 1}`;\n    if (deleted) segments.push({ kind: \"delete\", text: deleted, changeId: id });\n    if (inserted) segments.push({ kind: \"insert\", text: inserted, changeId: id });\n    if (deleted || inserted) changes.push({ id, kind: deleted && inserted ? \"replace\" : deleted ? \"delete\" : \"insert\", deleted, inserted });\n    if (trail) segments.push({ kind: \"equal\", text: trail });\n    ins += words(inserted);\n    del += words(deleted);\n  }\n  return { segments: mergeEquals(segments), changes, stats: { inserted: ins, deleted: del, changes: changes.length } };\n}\n\nconst segmenters = new Map<string, Intl.Segmenter>();\nfunction diffByWords(before: string, after: string, locale?: string): ChangeObject<string>[] {\n  if (typeof Intl === \"undefined\" || typeof Intl.Segmenter !== \"function\") return diffWordsWithSpace(before, after);\n  const key = locale ?? \"\";\n  let seg = segmenters.get(key);\n  if (!seg) segmenters.set(key, (seg = new Intl.Segmenter(locale, { granularity: \"word\" })));\n  return diffWords(before, after, { intlSegmenter: seg });\n}\n\nfunction commonEdge(a: string, b: string, edge: \"start\" | \"end\"): string {\n  let n = 0;\n  const max = Math.min(a.length, b.length);\n  if (edge === \"start\") {\n    while (n < max && a[n] === b[n] && /\\s/.test(a[n])) n++;\n    return a.slice(0, n);\n  }\n  while (n < max && a[a.length - 1 - n] === b[b.length - 1 - n] && /\\s/.test(a[a.length - 1 - n])) n++;\n  return n ? a.slice(a.length - n) : \"\";\n}\n\nfunction mergeEquals(segments: RedlineSegment[]) {\n  const out: RedlineSegment[] = [];\n  for (const s of segments) {\n    const last = out[out.length - 1];\n    if (last && last.kind === \"equal\" && s.kind === \"equal\") last.text += s.text;\n    else if (s.text) out.push({ ...s });\n  }\n  return out;\n}\n\nexport type Decision = \"accept\" | \"reject\";\n\n/**\n * The text after deciding each change: accepted changes take their insert, rejected ones\n * keep the original. Undecided changes follow `pending` (default `reject`: the original stands).\n */\nexport function resolveRedline(segments: readonly RedlineSegment[], decisions: Record<string, Decision | undefined>, { pending = \"reject\" }: { pending?: Decision } = {}): string {\n  let out = \"\";\n  for (const s of segments) {\n    if (s.kind === \"equal\") out += s.text;\n    else {\n      const d = (s.changeId && decisions[s.changeId]) || pending;\n      if ((s.kind === \"insert\" && d === \"accept\") || (s.kind === \"delete\" && d === \"reject\")) out += s.text;\n    }\n  }\n  return out;\n}\n\nexport type BlockDiff = {\n  id: string;\n  status: \"same\" | \"changed\" | \"added\" | \"removed\";\n  before?: DocBlock;\n  after?: DocBlock;\n  /** For changed blocks (and added/removed ones, as one insert/delete). */\n  redline: Redline;\n};\n\n/**\n * Two versions of a document compared block by block (matched by id): the order of `after`,\n * with removed blocks kept where they were. Change ids are prefixed by block id, so they are\n * unique across the document.\n */\nexport function diffDocs(before: Doc, after: Doc, options: Omit<RedlineOptions, \"idPrefix\"> = {}) {\n  const old = new Map(before.blocks.map((b, i) => [b.id, { b, i }]));\n  const kept = new Set(after.blocks.map((b) => b.id));\n  const out: BlockDiff[] = [];\n  const emitRemovedBefore = (limit: number, from: { i: number }) => {\n    for (; from.i < limit; from.i++) {\n      const b = before.blocks[from.i];\n      if (!kept.has(b.id)) out.push({ id: b.id, status: \"removed\", before: b, redline: redline(b.text, \"\", { ...options, idPrefix: `${b.id}:` }) });\n    }\n  };\n  const cursor = { i: 0 };\n  for (const b of after.blocks) {\n    const prev = old.get(b.id);\n    if (prev) emitRemovedBefore(prev.i, cursor);\n    if (prev && cursor.i <= prev.i) cursor.i = prev.i + 1;\n    const r = redline(prev?.b.text ?? \"\", b.text, { ...options, idPrefix: `${b.id}:` });\n    out.push({ id: b.id, status: !prev ? \"added\" : r.changes.length || prev.b.title !== b.title ? \"changed\" : \"same\", before: prev?.b, after: b, redline: r });\n  }\n  emitRemovedBefore(before.blocks.length, cursor);\n  const stats = out.reduce((s, d) => ({ inserted: s.inserted + d.redline.stats.inserted, deleted: s.deleted + d.redline.stats.deleted, changes: s.changes + d.redline.stats.changes }), {\n    inserted: 0,\n    deleted: 0,\n    changes: 0,\n  });\n  return { blocks: out, stats, changed: out.filter((d) => d.status !== \"same\").length };\n}\n\n/**\n * Line hunks between two texts, shaped exactly like code-core's `DiffHunk` (`oldStart`,\n * `oldLines`, `newStart`, `newLines`, `lines: { kind, text }[]`), so a Diff Block can render a\n * document version diff without doc-core depending on code-core.\n */\nexport function lineHunks(before: string, after: string, { context = 3 }: { context?: number } = {}) {\n  const patch = structuredPatch(\"before\", \"after\", before, after, undefined, undefined, { context });\n  return patch.hunks.map((h) => ({\n    oldStart: h.oldStart,\n    oldLines: h.oldLines,\n    newStart: h.newStart,\n    newLines: h.newLines,\n    lines: h.lines\n      .filter((l) => !l.startsWith(\"\\\\\"))\n      .map((l) => ({ kind: l[0] === \"+\" ? (\"add\" as const) : l[0] === \"-\" ? (\"remove\" as const) : (\"context\" as const), text: l.slice(1) })),\n  }));\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/doc-core/redline.ts"
    },
    {
      "path": "src/registry/eased/_primitives/doc-core/types.ts",
      "content": "export type DocBlockKind = \"heading\" | \"paragraph\" | \"clause\";\nexport type Risk = \"low\" | \"medium\" | \"high\";\n\n/** One block of a document. Offsets in anchors and redlines are into `text`. */\nexport type DocBlock = {\n  /** Stable across versions: `diffDocs` matches blocks by it. */\n  id: string;\n  kind: DocBlockKind;\n  text: string;\n  /** Clause number, \"4.2\" (see `numberClauses`). */\n  number?: string;\n  /** A clause's caption, \"Limitation of liability\". */\n  title?: string;\n  /** Heading level (1–3) or clause depth (1 = \"4\", 2 = \"4.2\", 3 = \"4.2.1\"). Default 1. */\n  level?: number;\n  risk?: Risk;\n};\n\nexport type Doc = { id: string; title: string; version?: string; blocks: DocBlock[] };\n\n/** Assigns \"1\", \"1.1\", \"1.1.1\" … to clauses by `level`, restarting sub-numbers under each parent. Headings and paragraphs are skipped. Immutable. */\nexport function numberClauses(blocks: readonly DocBlock[]): DocBlock[] {\n  const counters: number[] = [];\n  return blocks.map((b) => {\n    if (b.kind !== \"clause\") return b;\n    const level = Math.max(1, b.level ?? 1);\n    counters.length = level;\n    for (let i = 0; i < level; i++) counters[i] ??= 0;\n    counters[level - 1]++;\n    return { ...b, number: counters.slice(0, level).join(\".\") };\n  });\n}\n\n/**\n * A document from plain text: `# Title` and `## Heading` lines, numbered clauses (\"4.\", \"4.2\",\n * \"4.2.1\" with an optional \"Title.\" caption), and blank-line paragraphs. Ids are `b1`, `b2` …\n * in order, so parse two versions with matching structure and they diff block by block.\n */\nexport function docFromText(text: string, { id = \"doc\" }: { id?: string } = {}): Doc {\n  let title = \"\";\n  const blocks: DocBlock[] = [];\n  let n = 0;\n  for (const chunk of text.replace(/\\r\\n?/g, \"\\n\").split(/\\n\\s*\\n/)) {\n    const raw = chunk.trim();\n    if (!raw) continue;\n    const heading = /^(#{1,3})\\s+([\\s\\S]*)$/.exec(raw);\n    if (heading) {\n      if (heading[1].length === 1 && !title) {\n        title = heading[2].trim();\n        continue;\n      }\n      blocks.push({ id: `b${++n}`, kind: \"heading\", level: heading[1].length - 1 || 1, text: heading[2].trim() });\n      continue;\n    }\n    const clause = /^(\\d+(?:\\.\\d+)*)\\.?\\s+(?:([A-Z][A-Za-z'’\\- ]{1,48})\\.\\s+)?([\\s\\S]*)$/.exec(raw);\n    if (clause) {\n      // A caption is short (\"Limitation of liability.\"); a longer first sentence is body text.\n      const caption = clause[2] && clause[2].trim().split(/\\s+/).length <= 6 ? clause[2].trim() : undefined;\n      const body = caption ? clause[3] : raw.slice(clause[1].length).replace(/^\\.?\\s+/, \"\");\n      blocks.push({ id: `b${++n}`, kind: \"clause\", number: clause[1], level: clause[1].split(\".\").length, title: caption, text: body.replace(/\\s*\\n\\s*/g, \" \").trim() });\n      continue;\n    }\n    blocks.push({ id: `b${++n}`, kind: \"paragraph\", text: raw.replace(/\\s*\\n\\s*/g, \" \") });\n  }\n  return { id, title, blocks };\n}\n\nexport const blockById = (doc: Doc, id: string) => doc.blocks.find((b) => b.id === id);\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/doc-core/types.ts"
    }
  ],
  "type": "registry:lib"
}