{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "source-anchor",
  "title": "Source anchor",
  "description": "Pure conversion from document-source anchors to document viewer anchors.",
  "registryDependencies": [
    "@retab/document-anchor",
    "@retab/document-source"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/source-anchor.ts",
      "content": "import type { Source, SourceAnchor } from \"@/lib/document-source\";\n\nimport type { DocumentAnchor } from \"./document-anchor\";\n\nexport function sourceToDocumentAnchor(source: Source): DocumentAnchor | null {\n  switch (source.anchor.kind) {\n    case \"pdf_bbox\":\n      return pdfBboxAnchorToDocumentAnchor(source.anchor);\n    case \"image_bbox\":\n      return imageBboxAnchorToDocumentAnchor(source.anchor);\n    case \"csv_cell\":\n      return csvCellAnchorToDocumentAnchor(source.anchor);\n    case \"spreadsheet_cell\":\n      return spreadsheetCellAnchorToDocumentAnchor(source.anchor);\n    case \"docx_text_span\":\n      return docxTextSpanAnchorToDocumentAnchor(source.content, source.anchor);\n    case \"docx_table_cell\":\n      return docxTableCellAnchorToDocumentAnchor(source.anchor);\n    case \"text_span\":\n      return textSpanAnchorToDocumentAnchor(source.anchor);\n  }\n}\n\nfunction pdfBboxAnchorToDocumentAnchor(\n  anchor: Extract<SourceAnchor, { kind: \"pdf_bbox\" }>,\n): DocumentAnchor | null {\n  if (!isPositiveInteger(anchor.page) || !isValidNormalizedBox(anchor)) {\n    return null;\n  }\n  return {\n    kind: \"pdf-area\",\n    pageNumber: anchor.page,\n    left: toPercent(anchor.left),\n    top: toPercent(anchor.top),\n    width: toPercent(anchor.width),\n    height: toPercent(anchor.height),\n  };\n}\n\nfunction imageBboxAnchorToDocumentAnchor(\n  anchor: Extract<SourceAnchor, { kind: \"image_bbox\" }>,\n): DocumentAnchor | null {\n  const frameNumber = anchor.page ?? 1;\n  if (!isPositiveInteger(frameNumber) || !isValidNormalizedBox(anchor)) {\n    return null;\n  }\n  return {\n    kind: \"image-area\",\n    frameNumber,\n    left: toPercent(anchor.left),\n    top: toPercent(anchor.top),\n    width: toPercent(anchor.width),\n    height: toPercent(anchor.height),\n  };\n}\n\nfunction csvCellAnchorToDocumentAnchor(\n  anchor: Extract<SourceAnchor, { kind: \"csv_cell\" }>,\n): DocumentAnchor | null {\n  const columnIndex = columnLetterToIndex(anchor.column);\n  if (columnIndex == null || !Number.isInteger(anchor.row) || anchor.row < 1) {\n    return null;\n  }\n  return {\n    kind: \"csv-cell\",\n    rowIndex: anchor.row - 1,\n    columnIndex,\n  };\n}\n\nfunction spreadsheetCellAnchorToDocumentAnchor(\n  anchor: Extract<SourceAnchor, { kind: \"spreadsheet_cell\" }>,\n): DocumentAnchor | null {\n  const columnIndex = columnLetterToIndex(anchor.column);\n  if (\n    columnIndex == null ||\n    !Number.isSafeInteger(anchor.sheet_index) ||\n    anchor.sheet_index < 0 ||\n    !Number.isSafeInteger(anchor.row) ||\n    anchor.row < 1\n  ) {\n    return null;\n  }\n  return {\n    kind: \"xlsx-cell\",\n    sheetIndex: anchor.sheet_index,\n    rowIndex: anchor.row - 1,\n    columnIndex,\n  };\n}\n\nfunction docxTextSpanAnchorToDocumentAnchor(\n  content: string,\n  anchor: Extract<SourceAnchor, { kind: \"docx_text_span\" }>,\n): DocumentAnchor | null {\n  if (\n    !isNonNegativeInteger(anchor.paragraph) ||\n    !isValidOptionalRange(anchor.char_start, anchor.char_end)\n  ) {\n    return null;\n  }\n  const text = content.trim();\n  return text\n    ? {\n        kind: \"docx-target\",\n        target: { kind: \"text\", text },\n      }\n    : null;\n}\n\nfunction docxTableCellAnchorToDocumentAnchor(\n  anchor: Extract<SourceAnchor, { kind: \"docx_table_cell\" }>,\n): DocumentAnchor | null {\n  if (\n    !isNonNegativeInteger(anchor.table) ||\n    !isNonNegativeInteger(anchor.row) ||\n    !isNonNegativeInteger(anchor.column) ||\n    !isValidOptionalRange(anchor.char_start, anchor.char_end)\n  ) {\n    return null;\n  }\n  return {\n    kind: \"docx-target\",\n    target: {\n      kind: \"cell\",\n      table: anchor.table,\n      row: anchor.row,\n      column: anchor.column,\n    },\n  };\n}\n\nfunction textSpanAnchorToDocumentAnchor(\n  anchor: Extract<SourceAnchor, { kind: \"text_span\" }>,\n): DocumentAnchor | null {\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 null;\n  }\n  return {\n    kind: \"text-range\",\n    startLine: anchor.line_start,\n    endLine: anchor.line_end,\n  };\n}\n\nfunction columnLetterToIndex(letter: string): number | null {\n  if (!/^[A-Za-z]+$/.test(letter)) return null;\n  let index = 0;\n  for (const character of letter.toUpperCase()) {\n    index = index * 26 + (character.charCodeAt(0) - 64);\n    if (!Number.isSafeInteger(index)) return null;\n  }\n  return index - 1;\n}\n\nfunction isPositiveInteger(value: number): boolean {\n  return Number.isInteger(value) && value >= 1;\n}\n\nfunction isNonNegativeInteger(value: number) {\n  return Number.isInteger(value) && value >= 0;\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\nfunction isValidNormalizedBox({\n  left,\n  top,\n  width,\n  height,\n}: {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n}) {\n  return (\n    Number.isFinite(left) &&\n    Number.isFinite(top) &&\n    Number.isFinite(width) &&\n    Number.isFinite(height) &&\n    left >= 0 &&\n    top >= 0 &&\n    width > 0 &&\n    height > 0 &&\n    left + width <= 1 &&\n    top + height <= 1\n  );\n}\n\nfunction toPercent(value: number): number {\n  return value * 100;\n}\n",
      "type": "registry:ui",
      "target": "@ui/source-anchor.ts"
    }
  ],
  "type": "registry:ui"
}