{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-source",
  "title": "Text source adapter",
  "description": "Bridges the document-source model to text viewers: textAnchorToTarget and sourceToTextHighlight for text_span anchors.",
  "registryDependencies": [
    "@retab/document-source"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/text-source.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { Source, SourceAnchor } from \"@/lib/document-source\";\nimport type { TextLineRange } from \"@/components/ui/text-viewer-ranges\";\n\ninterface TextSourceHandle {\n  scrollToLineRange: (range: TextLineRange, options?: ScrollToOptions) => void;\n}\n\nexport interface SourceTarget {\n  scrollTo?: (source: Source, options: { behavior: ScrollBehavior }) => void;\n}\n\n/** A text_span anchor → a 1-based inclusive line range. */\nexport function textAnchorToTarget(\n  anchor: SourceAnchor,\n): TextLineRange | undefined {\n  if (anchor.kind === \"text_span\") {\n    if (\n      !Number.isInteger(anchor.line_start) ||\n      !Number.isInteger(anchor.line_end) ||\n      anchor.line_start < 1 ||\n      anchor.line_end < anchor.line_start ||\n      !isValidOptionalRange(anchor.char_start, anchor.char_end)\n    ) {\n      return undefined;\n    }\n    return { start: anchor.line_start, end: anchor.line_end };\n  }\n  return undefined;\n}\n\nfunction isValidOptionalRange(start?: number, end?: number) {\n  if (start == null && end == null) return true;\n  if (start == null || end == null) return false;\n  return (\n    Number.isInteger(start) &&\n    Number.isInteger(end) &&\n    start >= 0 &&\n    end >= start\n  );\n}\n\n/** A stable source target over a text-like viewer ref. */\nexport function useTextSourceTarget(\n  viewerRef: React.RefObject<TextSourceHandle | null>,\n): SourceTarget {\n  return React.useMemo<SourceTarget>(\n    () => ({\n      scrollTo: (source: Source, options) => {\n        const target = textAnchorToTarget(source.anchor);\n        if (target) viewerRef.current?.scrollToLineRange(target, options);\n      },\n    }),\n    [viewerRef],\n  );\n}\n\n/**\n * The line-range highlight prop derived from a source.\n */\nexport function sourceToTextHighlight(\n  source: Source | undefined,\n): TextLineRange | null {\n  return source ? textAnchorToTarget(source.anchor) || null : null;\n}\n",
      "type": "registry:ui",
      "target": "@ui/text-source.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/text-viewer-ranges.ts",
      "content": "export interface TextLineRange {\n  start: number;\n  end: number;\n}\n\nexport interface NormalizedTextLineRange extends TextLineRange {\n  readonly normalized: true;\n}\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\n\nexport function normalizeTextLineRange(\n  range: TextLineRange | null | undefined,\n  lineCount: number,\n): NormalizedTextLineRange | null {\n  if (\n    !range ||\n    !Number.isFinite(range.start) ||\n    !Number.isFinite(range.end) ||\n    !Number.isFinite(lineCount) ||\n    lineCount <= 0\n  ) {\n    return null;\n  }\n\n  const maxLine = Math.floor(lineCount);\n  const rawStart = Math.trunc(range.start);\n  const rawEnd = Math.trunc(range.end);\n  const start = Math.min(rawStart, rawEnd);\n  const end = Math.max(rawStart, rawEnd);\n\n  if (end < 1 || start > maxLine) return null;\n\n  return {\n    start: clamp(start, 1, maxLine),\n    end: clamp(end, 1, maxLine),\n    normalized: true,\n  };\n}\n\nexport function isLineInRange(\n  lineNumber: number,\n  range: NormalizedTextLineRange | null,\n) {\n  return range != null && lineNumber >= range.start && lineNumber <= range.end;\n}\n",
      "type": "registry:ui",
      "target": "@ui/text-viewer-ranges.ts"
    }
  ],
  "type": "registry:ui"
}