{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "parse-viewer-block",
  "title": "Parse Viewer",
  "description": "The source document beside its extracted markdown, synced by page, with a Rendered/Text toggle.",
  "dependencies": [
    "lucide-react",
    "react-markdown",
    "remark-gfm",
    "rehype-raw",
    "hast-util-to-jsx-runtime@^2.3.6",
    "remark-parse@^11.0.0",
    "remark-rehype@^11.1.2",
    "unified@^11.0.5"
  ],
  "registryDependencies": [
    "@retab/file-viewer",
    "@retab/pdf-viewer",
    "button",
    "dropdown-menu",
    "@retab/scroll-area",
    "@retab/resizable",
    "separator",
    "@retab/spinner",
    "tabs"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/parse-viewer-block.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport {\n  FileViewer,\n  FileViewerContent,\n  FileViewerProvider,\n  FileViewerInset,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport {\n  PdfViewerPages,\n  PdfViewerProvider,\n  type PdfViewerHandle,\n} from \"@/components/ui/pdf-viewer\";\nimport {\n  ResizableHandle,\n  ResizablePanel,\n  ResizablePanelGroup,\n} from \"@/components/ui/resizable\";\nimport { ViewerBody, ViewerRoot, ViewerSurface } from \"@/components/ui/viewer\";\nimport type { ParseResponse } from \"@/components/viewers/lib/parse-types\";\nimport {\n  ParseViewerHeader,\n  ParseViewerMarkdown,\n  ParseViewerProvider,\n  useParseViewerDocument,\n} from \"@/components/viewers/parse/parse-viewer\";\nimport parseSample from \"@/components/viewers/sample-data/parse.json\";\n\nconst PDF_URL = \"/samples/bank-statement-x4uhhi7t.pdf\";\n\n// A parse of the bank-statement sample: per-page, LLM-ready markdown with the\n// transactions reconstructed as a table.\nconst PARSE_RESULT: ParseResponse = {\n  output: parseSample.output as ParseResponse[\"output\"],\n  usage: parseSample.usage as ParseResponse[\"usage\"],\n};\n\n/**\n * Parse viewer block — the source document beside its extracted markdown, kept\n * in sync by page. `ParseViewer` owns the markdown pane (Rendered/Text toggle,\n * page controls); the document surface here is FileViewer + PDF pages.\n */\nexport function ParseViewerBlock() {\n  return (\n    <div className=\"bg-background flex h-full min-h-[680px] flex-col\">\n      <ParseViewerProvider result={PARSE_RESULT}>\n        <ViewerRoot className=\"bg-background h-full flex-1\">\n          <ParseViewerHeader />\n          <ViewerBody>\n            <ResizablePanelGroup\n              orientation=\"horizontal\"\n              className=\"min-h-0 flex-1\"\n            >\n              <ResizablePanel defaultSize={52} minSize={28}>\n                <ViewerSurface className=\"h-full\">\n                  <ParseSourceDocument />\n                </ViewerSurface>\n              </ResizablePanel>\n              <ResizableHandle withHandle />\n              <ResizablePanel defaultSize={48} minSize={28}>\n                <ViewerSurface className=\"h-full\">\n                  <ParseViewerMarkdown />\n                </ViewerSurface>\n              </ResizablePanel>\n            </ResizablePanelGroup>\n          </ViewerBody>\n        </ViewerRoot>\n      </ParseViewerProvider>\n    </div>\n  );\n}\n\nfunction ParseSourceDocument() {\n  const document = useParseViewerDocument();\n  const viewerRef = React.useRef<PdfViewerHandle | null>(null);\n  const documentKey = useObjectDependencyKey(document);\n\n  useKeyedMountEffect(documentKey, () => {\n    document.setDocumentHandle({\n      scrollToPage: (pageNumber, options) => {\n        viewerRef.current?.scrollToPage(pageNumber, options);\n      },\n    });\n    return () => document.setDocumentHandle(null);\n  });\n\n  return (\n    <FileViewerProvider\n      source={{\n        kind: \"url\",\n        url: PDF_URL,\n        fileName: \"bank-statement.pdf\",\n      }}\n    >\n      <FileViewer>\n        <PdfViewerProvider>\n          <FileViewerContent>\n            <FileViewerInset>\n              <FileViewerViewport>\n                <PdfViewerPages\n                  ref={viewerRef}\n                  bare\n                  onVisiblePageChange={document.onCurrentPageChange}\n                  onScrollProgressChange={document.onScrollProgressChange}\n                  className=\"h-full\"\n                />\n              </FileViewerViewport>\n            </FileViewerInset>\n          </FileViewerContent>\n        </PdfViewerProvider>\n      </FileViewer>\n    </FileViewerProvider>\n  );\n}\n\nfunction useObjectDependencyKey(value: object): string {\n  const keyRef = React.useRef(0);\n  const valueRef = React.useRef<object | null>(null);\n\n  if (valueRef.current !== value) {\n    valueRef.current = value;\n    keyRef.current += 1;\n  }\n\n  return `object:${keyRef.current}`;\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/parse-viewer-block.tsx"
    },
    {
      "path": "components/viewers/parse/parse-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { ViewerBody, ViewerRoot, ViewerSurface } from \"@/components/ui/viewer\";\nimport { type ParseResponse } from \"@/components/viewers/lib/parse-types\";\nimport { type PageMarkdownDocumentState } from \"@/components/viewers/page-markdown/page-markdown-types\";\nimport {\n  PageMarkdownViewerContent,\n  PageMarkdownViewerHeader,\n  PageMarkdownViewerProvider,\n  usePageMarkdownViewerDocument,\n} from \"@/components/viewers/page-markdown/page-markdown-viewer\";\n\nexport interface ParseViewerProviderProps {\n  result: ParseResponse | null;\n  isProcessing?: boolean;\n  onVisiblePageChange?: (pageNumber: number) => void;\n  children: React.ReactNode;\n}\n\nexport interface ParseViewerProps {\n  result: ParseResponse | null;\n  isProcessing?: boolean;\n  onVisiblePageChange?: (pageNumber: number) => void;\n}\n\nexport type ParseDocumentState = PageMarkdownDocumentState;\n\nexport function useParseViewerDocument(): ParseDocumentState {\n  return usePageMarkdownViewerDocument();\n}\n\nexport function ParseViewerProvider({\n  result,\n  isProcessing = false,\n  onVisiblePageChange,\n  children,\n}: ParseViewerProviderProps) {\n  const pages = result?.output?.pages ?? [];\n  const text = result?.output?.text ?? undefined;\n  const resetKey = result?.document?.id;\n\n  return (\n    <PageMarkdownViewerProvider\n      pages={pages}\n      text={text}\n      isProcessing={isProcessing}\n      onVisiblePageChange={onVisiblePageChange}\n      resetKey={resetKey}\n      fileName=\"parse-output.md\"\n      processingLabel=\"Parsing document...\"\n    >\n      {children}\n    </PageMarkdownViewerProvider>\n  );\n}\n\nexport function ParseViewerMarkdown() {\n  return <PageMarkdownViewerContent />;\n}\n\nexport function ParseViewerHeader({ className }: { className?: string }) {\n  return <PageMarkdownViewerHeader className={className} />;\n}\n\nexport function ParseViewer({\n  result,\n  isProcessing = false,\n  onVisiblePageChange,\n}: ParseViewerProps) {\n  return (\n    <ParseViewerProvider\n      result={result}\n      isProcessing={isProcessing}\n      onVisiblePageChange={onVisiblePageChange}\n    >\n      <ViewerRoot className=\"bg-background h-full flex-1\">\n        <ParseViewerHeader />\n        <ViewerBody>\n          <ViewerSurface>\n            <ParseViewerMarkdown />\n          </ViewerSurface>\n        </ViewerBody>\n      </ViewerRoot>\n    </ParseViewerProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/parse/parse-viewer.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport {\n  ViewerBody,\n  ViewerHeader,\n  ViewerRoot,\n  ViewerSurface,\n} from \"@/components/ui/viewer\";\nimport { PageMarkdownEmptyState } from \"@/components/viewers/page-markdown/page-markdown-empty-state\";\nimport { PAGE_MARKDOWN_PAGE_WIDTH } from \"@/components/viewers/page-markdown/page-markdown-layout\";\nimport { joinMarkdownPages } from \"@/components/viewers/page-markdown/page-markdown-model\";\nimport {\n  PageMarkdownPane,\n  type PageMarkdownPaneHandle,\n} from \"@/components/viewers/page-markdown/page-markdown-pane\";\nimport {\n  usePageMarkdownScale,\n  zoomPageScale,\n} from \"@/components/viewers/page-markdown/page-markdown-scale\";\nimport { usePageMarkdownSync } from \"@/components/viewers/page-markdown/page-markdown-sync\";\nimport { PageMarkdownControls } from \"@/components/viewers/page-markdown/page-markdown-controls\";\nimport {\n  type PageMarkdownDocumentHandle,\n  type PageMarkdownDocumentState,\n  type PageMarkdownViewerProps,\n  type PageMarkdownViewMode,\n} from \"@/components/viewers/page-markdown/page-markdown-types\";\n\ntype PageMarkdownViewerContextValue = {\n  content: PageMarkdownViewerContentState;\n  document: PageMarkdownDocumentState;\n  header: PageMarkdownViewerHeaderState;\n};\n\ntype PageMarkdownViewerContentState = {\n  hasPages: boolean;\n  isMarkdownScaleReady: boolean;\n  isProcessing: boolean;\n  markdownPaneRef: React.RefObject<PageMarkdownPaneHandle | null>;\n  mode: PageMarkdownViewMode;\n  onMarkdownVisiblePageChange: (pageNumber: number) => void;\n  pages: string[];\n  processingLabel: string;\n  resetKey?: string;\n  scale: number;\n  setMarkdownContainerWidth: (width: number | null) => void;\n  text: string;\n};\n\ntype PageMarkdownViewerHeaderState = {\n  currentPage: number;\n  fileName: string;\n  fitWidth: () => void;\n  mode: PageMarkdownViewMode;\n  pageCount: number;\n  scale: number;\n  setMode: (mode: PageMarkdownViewMode) => void;\n  setViewerScale: (scale: number | null) => void;\n  text: string;\n};\n\nconst PageMarkdownViewerContext =\n  React.createContext<PageMarkdownViewerContextValue | null>(null);\n\nfunction usePageMarkdownViewerContext(): PageMarkdownViewerContextValue {\n  const context = React.useContext(PageMarkdownViewerContext);\n  if (!context) {\n    throw new Error(\n      \"usePageMarkdownViewer must be used within PageMarkdownViewerProvider.\",\n    );\n  }\n  return context;\n}\n\nfunction usePageMarkdownViewerContent(): PageMarkdownViewerContentState {\n  return usePageMarkdownViewerContext().content;\n}\n\nexport function usePageMarkdownViewerDocument(): PageMarkdownDocumentState {\n  return usePageMarkdownViewerContext().document;\n}\n\nfunction usePageMarkdownViewerHeader(): PageMarkdownViewerHeaderState {\n  return usePageMarkdownViewerContext().header;\n}\n\nexport function PageMarkdownViewerProvider({\n  children,\n  pages,\n  text = joinMarkdownPages(pages),\n  isProcessing = false,\n  onVisiblePageChange,\n  fileName = \"document.md\",\n  resetKey,\n  processingLabel = \"Preparing document...\",\n}: PageMarkdownViewerProps & { children: React.ReactNode }) {\n  const hasPages = pages.length > 0;\n  const [mode, setMode] = React.useState<PageMarkdownViewMode>(\"rendered\");\n  const [markdownContainerWidth, setMarkdownContainerWidth] = React.useState<\n    number | null\n  >(null);\n  const markdownPaneRef = React.useRef<PageMarkdownPaneHandle | null>(null);\n  const documentHandleRef = React.useRef<PageMarkdownDocumentHandle | null>(\n    null,\n  );\n  const pagePaneResetKey = hasPages ? `pages:${resetKey ?? \"\"}` : \"empty\";\n  const { currentPage, reportDocumentPage, reportMarkdownPage } =\n    usePageMarkdownSync({\n      onMarkdownPageChange: onVisiblePageChange,\n      pageCount: pages.length,\n      resetKey: pagePaneResetKey,\n    });\n\n  useKeyedMountEffect(`page-markdown-viewer-mode:${resetKey ?? \"\"}`, () => {\n    setMode(\"rendered\");\n  });\n\n  const handleDocumentPageChange = React.useCallback(\n    (pageNumber: number) => {\n      const normalizedPage = Number.isFinite(pageNumber)\n        ? Math.floor(pageNumber)\n        : 1;\n      const pageCount = Math.max(1, pages.length);\n      const target = reportDocumentPage(\n        Math.min(pageCount, Math.max(1, normalizedPage)),\n      );\n      if (target?.pane === \"markdown\") {\n        markdownPaneRef.current?.scrollToPage(target.pageNumber);\n      }\n    },\n    [pages.length, reportDocumentPage],\n  );\n\n  const handleDocumentScrollProgressChange = React.useCallback(\n    (progress: number) => {\n      void progress;\n    },\n    [],\n  );\n\n  const handleMarkdownPageChange = React.useCallback(\n    (pageNumber: number) => {\n      const target = reportMarkdownPage(pageNumber);\n      if (target?.pane === \"document\") {\n        documentHandleRef.current?.scrollToPage(target.pageNumber);\n      }\n    },\n    [reportMarkdownPage],\n  );\n\n  const setDocumentHandle = React.useCallback(\n    (handle: PageMarkdownDocumentHandle | null) => {\n      documentHandleRef.current = handle;\n    },\n    [],\n  );\n\n  const { fitWidth, scale, setViewerScale } = usePageMarkdownScale({\n    containerWidth: markdownContainerWidth,\n    pageWidth: PAGE_MARKDOWN_PAGE_WIDTH,\n    resetKey,\n  });\n  const isMarkdownScaleReady = markdownContainerWidth !== null;\n\n  const document = React.useMemo<PageMarkdownDocumentState>(\n    () => ({\n      onCurrentPageChange: handleDocumentPageChange,\n      onScrollProgressChange: handleDocumentScrollProgressChange,\n      setDocumentHandle,\n    }),\n    [\n      handleDocumentPageChange,\n      handleDocumentScrollProgressChange,\n      setDocumentHandle,\n    ],\n  );\n\n  const content = React.useMemo<PageMarkdownViewerContentState>(\n    () => ({\n      hasPages,\n      isMarkdownScaleReady,\n      isProcessing,\n      markdownPaneRef,\n      mode,\n      onMarkdownVisiblePageChange: handleMarkdownPageChange,\n      pages,\n      processingLabel,\n      resetKey,\n      scale,\n      setMarkdownContainerWidth,\n      text,\n    }),\n    [\n      handleMarkdownPageChange,\n      hasPages,\n      isMarkdownScaleReady,\n      isProcessing,\n      markdownPaneRef,\n      mode,\n      pages,\n      processingLabel,\n      resetKey,\n      scale,\n      setMarkdownContainerWidth,\n      text,\n    ],\n  );\n\n  const header = React.useMemo<PageMarkdownViewerHeaderState>(\n    () => ({\n      currentPage,\n      fileName,\n      fitWidth,\n      mode,\n      pageCount: pages.length,\n      scale,\n      setMode,\n      setViewerScale,\n      text,\n    }),\n    [\n      currentPage,\n      fileName,\n      fitWidth,\n      mode,\n      pages.length,\n      scale,\n      setMode,\n      setViewerScale,\n      text,\n    ],\n  );\n\n  const value = React.useMemo<PageMarkdownViewerContextValue>(\n    () => ({\n      content,\n      document,\n      header,\n    }),\n    [content, document, header],\n  );\n\n  return (\n    <PageMarkdownViewerContext.Provider value={value}>\n      {children}\n    </PageMarkdownViewerContext.Provider>\n  );\n}\n\nexport function PageMarkdownViewerContent() {\n  const {\n    hasPages,\n    isMarkdownScaleReady,\n    isProcessing,\n    markdownPaneRef,\n    mode,\n    onMarkdownVisiblePageChange,\n    pages,\n    processingLabel,\n    resetKey,\n    scale,\n    setMarkdownContainerWidth,\n    text,\n  } = usePageMarkdownViewerContent();\n\n  if (!hasPages) {\n    return (\n      <PageMarkdownEmptyState\n        isProcessing={isProcessing}\n        processingLabel={processingLabel}\n      />\n    );\n  }\n\n  return (\n    <PageMarkdownPane\n      ref={markdownPaneRef}\n      pages={pages}\n      text={text}\n      mode={mode}\n      scale={scale}\n      isScaleReady={isMarkdownScaleReady}\n      resetKey={resetKey}\n      onContainerWidthChange={setMarkdownContainerWidth}\n      onVisiblePageChange={onMarkdownVisiblePageChange}\n    />\n  );\n}\n\nexport function PageMarkdownViewerHeader({\n  className,\n}: {\n  className?: string;\n}) {\n  const {\n    currentPage,\n    fileName,\n    fitWidth,\n    mode,\n    pageCount,\n    scale,\n    setMode,\n    setViewerScale,\n    text,\n  } = usePageMarkdownViewerHeader();\n\n  return (\n    <ViewerHeader className={className}>\n      <PageMarkdownControls\n        className=\"border-b-0\"\n        currentPage={Math.min(currentPage, pageCount)}\n        pageCount={pageCount}\n        mode={mode}\n        scale={scale}\n        text={text}\n        fileName={fileName}\n        onModeChange={setMode}\n        onZoom={(factor) => setViewerScale(zoomPageScale(scale, factor))}\n        onFitWidth={fitWidth}\n      />\n    </ViewerHeader>\n  );\n}\n\nexport function PageMarkdownViewer(props: PageMarkdownViewerProps) {\n  return (\n    <PageMarkdownViewerProvider {...props}>\n      <ViewerRoot className=\"bg-background h-full flex-1\">\n        <PageMarkdownViewerHeader />\n        <ViewerBody>\n          <ViewerSurface>\n            <PageMarkdownViewerContent />\n          </ViewerSurface>\n        </ViewerBody>\n      </ViewerRoot>\n    </PageMarkdownViewerProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-viewer.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-pane.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\nimport { useElementWidth } from \"@/hooks/use-element-width\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport {\n  createPageMarkdownLayout,\n  createPageMeasurementKey,\n  findPageMarkdownPageByOffset,\n  getPageMarkdownPageLayout,\n  getPageMarkdownVisiblePageNumbers,\n} from \"@/components/viewers/page-markdown/page-markdown-layout\";\nimport {\n  usePageMarkdownMeasurements,\n  usePageMarkdownScrollAnchor,\n} from \"@/components/viewers/page-markdown/page-markdown-measurements\";\nimport { PageMarkdownPageFrame } from \"@/components/viewers/page-markdown/page-markdown-page-frame\";\nimport { usePageMarkdownScroll } from \"@/components/viewers/page-markdown/page-markdown-scroll\";\nimport { type PageMarkdownViewMode } from \"@/components/viewers/page-markdown/page-markdown-types\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport interface PageMarkdownPaneHandle {\n  scrollToPage: (pageNumber: number, options?: ScrollToOptions) => void;\n}\n\nexport const PageMarkdownPane = React.forwardRef<\n  PageMarkdownPaneHandle,\n  {\n    pages: string[];\n    text: string;\n    mode: PageMarkdownViewMode;\n    scale: number;\n    isScaleReady: boolean;\n    resetKey?: string;\n    onContainerWidthChange: (width: number | null) => void;\n    onVisiblePageChange: (pageNumber: number) => void;\n  }\n>(function PageMarkdownPane(\n  {\n    pages,\n    text,\n    mode,\n    scale,\n    isScaleReady,\n    resetKey,\n    onContainerWidthChange,\n    onVisiblePageChange,\n  },\n  ref,\n) {\n  const [viewportWidthRef, viewportWidth] = useElementWidth();\n  const canvasRef = React.useRef<HTMLDivElement | null>(null);\n  const projectionFrameRef = React.useRef<number | null>(null);\n  const projectPagesRef = React.useRef<() => void>(() => {});\n  const projectionCacheRef = React.useRef<PageMarkdownProjectionCache>({\n    resetKey: null,\n    slots: new Map(),\n  });\n  const pagesSignature = React.useMemo(\n    () => `${resetKey ?? \"\"}\\u0000${pages.join(\"\\u0000\")}`,\n    [pages, resetKey],\n  );\n  const pageRenderKeys = React.useMemo(\n    () =>\n      pages.map((markdown) =>\n        createPageMeasurementKey({\n          markdown,\n          mode,\n          scale,\n        }),\n      ),\n    [mode, pages, scale],\n  );\n  const { measuredHeightByPageNumber, setPageHeight } =\n    usePageMarkdownMeasurements({ mode, pages, scale });\n  const layout = React.useMemo(\n    () =>\n      createPageMarkdownLayout({\n        measuredHeightByPageNumber,\n        mode,\n        pages,\n        scale,\n      }),\n    [measuredHeightByPageNumber, mode, pages, scale],\n  );\n  const {\n    handleScroll,\n    measureScroll,\n    scrollToPage,\n    getViewportElement,\n    setViewportElement,\n    viewportElement,\n  } = usePageMarkdownScroll({\n    layout,\n    onVisiblePageChange,\n    pageCount: pages.length,\n    resetKey: pagesSignature,\n  });\n\n  useKeyedLayoutEffect(\n    joinEffectKey([onContainerWidthChange, viewportWidth]),\n    () => {\n      onContainerWidthChange(viewportWidth);\n    },\n  );\n\n  const { captureScrollAnchor } = usePageMarkdownScrollAnchor({\n    layout,\n    onRestore: () => projectPagesRef.current(),\n    viewportElement,\n  });\n  const captureScrollAnchorBeforePageResize = React.useCallback(\n    (pageNumber: number) => {\n      const viewportElement = getViewportElement();\n      if (!viewportElement) return;\n\n      const anchorPageNumber = findPageMarkdownPageByOffset(\n        layout,\n        viewportElement.scrollTop,\n      );\n      if (pageNumber < anchorPageNumber) captureScrollAnchor();\n    },\n    [captureScrollAnchor, getViewportElement, layout],\n  );\n  const handlePageSize = React.useCallback(\n    (pageNumber: number, height: number) => {\n      setPageHeight(pageNumber, height, () =>\n        captureScrollAnchorBeforePageResize(pageNumber),\n      );\n    },\n    [captureScrollAnchorBeforePageResize, setPageHeight],\n  );\n\n  const projectPages = React.useCallback(() => {\n    projectionFrameRef.current = null;\n    projectPageMarkdownPages({\n      cache: projectionCacheRef.current,\n      canvas: canvasRef.current,\n      layout,\n      mode,\n      onSize: handlePageSize,\n      pageRenderKeys,\n      pages,\n      resetKey: pagesSignature,\n      scale,\n      viewportElement: getViewportElement(),\n    });\n  }, [\n    getViewportElement,\n    handlePageSize,\n    layout,\n    mode,\n    pages,\n    pageRenderKeys,\n    pagesSignature,\n    scale,\n  ]);\n  useKeyedLayoutEffect(joinEffectKey([projectPages]), () => {\n    projectPagesRef.current = projectPages;\n  });\n\n  const schedulePageProjection = React.useCallback(() => {\n    if (projectionFrameRef.current !== null) return;\n    if (typeof requestAnimationFrame !== \"function\") {\n      projectPages();\n      return;\n    }\n    projectionFrameRef.current = requestAnimationFrame(projectPages);\n  }, [projectPages]);\n\n  React.useImperativeHandle(\n    ref ?? null,\n    () => ({\n      scrollToPage: (pageNumber, options) => {\n        scrollToPage(pageNumber, options);\n        measureScroll();\n        projectPages();\n      },\n    }),\n    [measureScroll, projectPages, scrollToPage],\n  );\n\n  useKeyedLayoutEffect(joinEffectKey([projectPages, viewportElement]), () => {\n    projectPages();\n  });\n\n  useMountEffect(() => () => {\n    if (\n      projectionFrameRef.current !== null &&\n      typeof cancelAnimationFrame === \"function\"\n    ) {\n      cancelAnimationFrame(projectionFrameRef.current);\n    }\n    disposePageMarkdownProjectionCache(projectionCacheRef.current);\n  });\n\n  const handleViewportScroll = React.useCallback(() => {\n    handleScroll();\n    schedulePageProjection();\n  }, [handleScroll, schedulePageProjection]);\n\n  const handleViewportKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (\n        event.defaultPrevented ||\n        event.altKey ||\n        event.ctrlKey ||\n        event.metaKey\n      ) {\n        return;\n      }\n\n      const viewportElement = event.currentTarget;\n      const maxScrollTop = Math.max(\n        0,\n        viewportElement.scrollHeight - viewportElement.clientHeight,\n      );\n      let nextScrollTop: number | null = null;\n\n      switch (event.key) {\n        case \"ArrowDown\":\n          nextScrollTop = viewportElement.scrollTop + 40;\n          break;\n        case \"ArrowUp\":\n          nextScrollTop = viewportElement.scrollTop - 40;\n          break;\n        case \"PageDown\":\n          nextScrollTop =\n            viewportElement.scrollTop + viewportElement.clientHeight * 0.85;\n          break;\n        case \"PageUp\":\n          nextScrollTop =\n            viewportElement.scrollTop - viewportElement.clientHeight * 0.85;\n          break;\n        case \"Home\":\n          nextScrollTop = 0;\n          break;\n        case \"End\":\n          nextScrollTop = maxScrollTop;\n          break;\n        default:\n          return;\n      }\n\n      event.preventDefault();\n      viewportElement.scrollTop = Math.min(\n        maxScrollTop,\n        Math.max(0, nextScrollTop),\n      );\n      measureScroll();\n      projectPages();\n    },\n    [measureScroll, projectPages],\n  );\n\n  if (!isScaleReady) {\n    return (\n      <div className=\"bg-muted/20 flex h-full min-h-0 min-w-0 flex-col\">\n        <ScrollArea className=\"min-h-0 flex-1\">\n          <div ref={viewportWidthRef} className=\"h-full w-full min-w-0\" />\n        </ScrollArea>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"bg-muted/20 flex h-full min-h-0 min-w-0 flex-col\">\n      <ScrollArea\n        nativeScrollbar\n        viewportRef={setViewportElement}\n        viewportProps={{\n          \"aria-label\": \"Markdown pages\",\n          onKeyDown: handleViewportKeyDown,\n          onScroll: handleViewportScroll,\n          tabIndex: 0,\n        }}\n        className=\"min-h-0 flex-1\"\n      >\n        <div ref={viewportWidthRef} className=\"w-full min-w-0\">\n          <div\n            ref={canvasRef}\n            className=\"relative mx-auto\"\n            style={{\n              height: layout.totalHeight,\n              minWidth: layout.width,\n            }}\n          />\n        </div>\n      </ScrollArea>\n    </div>\n  );\n});\n\ntype PageMarkdownProjectionCache = {\n  resetKey: unknown;\n  slots: Map<number, PageMarkdownProjectedSlot>;\n};\n\ntype PageMarkdownProjectedSlot = {\n  renderKey: string;\n  root: Root;\n  slot: HTMLElement;\n};\n\nfunction projectPageMarkdownPages({\n  cache,\n  canvas,\n  layout,\n  mode,\n  onSize,\n  pageRenderKeys,\n  pages,\n  resetKey,\n  scale,\n  viewportElement,\n}: {\n  cache: PageMarkdownProjectionCache;\n  canvas: HTMLDivElement | null;\n  layout: ReturnType<typeof createPageMarkdownLayout>;\n  mode: PageMarkdownViewMode;\n  onSize: (pageNumber: number, height: number) => void;\n  pageRenderKeys: readonly string[];\n  pages: readonly string[];\n  resetKey: unknown;\n  scale: number;\n  viewportElement: HTMLDivElement | null;\n}) {\n  if (!canvas) return;\n\n  canvas.style.height = `${layout.totalHeight}px`;\n  canvas.style.minWidth = `${layout.width}px`;\n\n  if (!Object.is(cache.resetKey, resetKey)) {\n    disposePageMarkdownProjectionCache(cache);\n    cache.resetKey = resetKey;\n  }\n\n  const visiblePageNumbers = getPageMarkdownVisiblePageNumbers({\n    layout,\n    scrollTop: viewportElement?.scrollTop ?? 0,\n    viewportHeight: getPageMarkdownViewportHeight(viewportElement),\n  });\n  const visiblePageNumberSet = new Set(visiblePageNumbers);\n\n  for (const [pageNumber, projectedSlot] of cache.slots) {\n    if (visiblePageNumberSet.has(pageNumber)) continue;\n    disposePageMarkdownProjectedSlot(projectedSlot);\n    cache.slots.delete(pageNumber);\n  }\n\n  for (const pageNumber of visiblePageNumbers) {\n    const pageLayout = getPageMarkdownPageLayout(layout, pageNumber);\n    const markdown = pages[pageNumber - 1];\n    const pageRenderKey = pageRenderKeys[pageNumber - 1];\n    if (!pageLayout || markdown == null) continue;\n\n    const projectedSlot =\n      cache.slots.get(pageNumber) ??\n      createPageMarkdownProjectedSlot(pageNumber);\n    patchPageMarkdownProjectedSlot(projectedSlot.slot, pageLayout);\n    renderPageMarkdownProjectedSlot({\n      markdown,\n      mode,\n      onSize,\n      pageLayout,\n      pageNumber,\n      pageRenderKey,\n      projectedSlot,\n      scale,\n    });\n    cache.slots.set(pageNumber, projectedSlot);\n    canvas.append(projectedSlot.slot);\n  }\n}\n\nfunction createPageMarkdownProjectedSlot(pageNumber: number) {\n  const slot = document.createElement(\"div\");\n  slot.dataset.pageNumber = String(pageNumber);\n  slot.dataset.slot = \"page-markdown-page-slot\";\n  slot.className = \"absolute left-1/2 -translate-x-1/2\";\n  return {\n    renderKey: \"\",\n    root: createRoot(slot),\n    slot,\n  };\n}\n\nfunction patchPageMarkdownProjectedSlot(\n  slot: HTMLElement,\n  pageLayout: NonNullable<ReturnType<typeof getPageMarkdownPageLayout>>,\n) {\n  slot.style.top = `${pageLayout.offsetTop}px`;\n  slot.style.width = `${pageLayout.width}px`;\n  slot.style.minHeight = `${pageLayout.height}px`;\n}\n\nfunction renderPageMarkdownProjectedSlot({\n  markdown,\n  mode,\n  onSize,\n  pageLayout,\n  pageNumber,\n  pageRenderKey,\n  projectedSlot,\n  scale,\n}: {\n  markdown: string;\n  mode: PageMarkdownViewMode;\n  onSize: (pageNumber: number, height: number) => void;\n  pageLayout: NonNullable<ReturnType<typeof getPageMarkdownPageLayout>>;\n  pageNumber: number;\n  pageRenderKey?: string;\n  projectedSlot: PageMarkdownProjectedSlot;\n  scale: number;\n}) {\n  const renderKey = [\n    pageNumber,\n    mode,\n    scale,\n    pageLayout.height,\n    pageLayout.width,\n    pageRenderKey ?? markdown,\n  ].join(\"\\u0000\");\n  if (projectedSlot.renderKey === renderKey) return;\n\n  projectedSlot.renderKey = renderKey;\n  projectedSlot.root.render(\n    <PageMarkdownPageFrame\n      estimatedHeight={pageLayout.height}\n      markdown={markdown}\n      mode={mode}\n      onSize={onSize}\n      pageNumber={pageNumber}\n      scale={scale}\n    />,\n  );\n}\n\nfunction getPageMarkdownViewportHeight(viewportElement: HTMLElement | null) {\n  return (\n    viewportElement?.clientHeight ||\n    viewportElement?.getBoundingClientRect().height ||\n    0\n  );\n}\n\nfunction disposePageMarkdownProjectionCache(\n  cache: PageMarkdownProjectionCache,\n) {\n  for (const projectedSlot of cache.slots.values()) {\n    disposePageMarkdownProjectedSlot(projectedSlot);\n  }\n  cache.slots.clear();\n}\n\nfunction disposePageMarkdownProjectedSlot(\n  projectedSlot: PageMarkdownProjectedSlot,\n) {\n  deferPageMarkdownRootUnmount(projectedSlot.root);\n  projectedSlot.slot.remove();\n}\n\nfunction deferPageMarkdownRootUnmount(root: Root) {\n  const unmount = () => root.unmount();\n  if (typeof queueMicrotask === \"function\") {\n    queueMicrotask(unmount);\n    return;\n  }\n  window.setTimeout(unmount, 0);\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-pane.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-empty-state.tsx",
      "content": "\"use client\";\n\nimport { ScanText } from \"lucide-react\";\n\nimport { Spinner } from \"@/components/ui/spinner\";\n\nexport function PageMarkdownEmptyState({\n  isProcessing,\n  processingLabel,\n}: {\n  isProcessing: boolean;\n  processingLabel: string;\n}) {\n  return (\n    <div className=\"bg-muted/30 text-muted-foreground flex min-h-0 flex-1 flex-col items-center justify-center gap-4 px-8\">\n      {isProcessing ? (\n        <>\n          <Spinner className=\"text-primary size-8\" />\n          <p className=\"text-sm\">{processingLabel}</p>\n        </>\n      ) : (\n        <>\n          <ScanText className=\"size-12 opacity-60\" />\n          <div className=\"space-y-1 text-center\">\n            <p className=\"text-foreground text-sm font-medium\">\n              No markdown pages yet\n            </p>\n            <p className=\"max-w-xs text-xs\">\n              Provide page-by-page markdown to see the rendered document here.\n            </p>\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-empty-state.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-controls.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useElementWidth } from \"@/hooks/use-element-width\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { ViewerControls } from \"@/components/ui/viewer-controls\";\nimport {\n  MarkdownActionButtons,\n  MarkdownActionsMenu,\n} from \"@/components/viewers/page-markdown/page-markdown-actions\";\nimport { PAGE_MARKDOWN_COMPACT_ACTIONS_WIDTH } from \"@/components/viewers/page-markdown/page-markdown-model\";\nimport { type PageMarkdownViewMode } from \"@/components/viewers/page-markdown/page-markdown-types\";\n\nexport function PageMarkdownControls({\n  className,\n  currentPage,\n  pageCount,\n  mode,\n  scale,\n  text,\n  fileName,\n  onModeChange,\n  onZoom,\n  onFitWidth,\n}: {\n  className?: string;\n  currentPage: number;\n  pageCount: number;\n  mode: PageMarkdownViewMode;\n  scale: number;\n  text: string;\n  fileName: string;\n  onModeChange: (mode: PageMarkdownViewMode) => void;\n  onZoom: (factor: number) => void;\n  onFitWidth: () => void;\n}) {\n  const [controlsRef, controlsWidth] = useElementWidth();\n  const isCompact =\n    controlsWidth !== null &&\n    controlsWidth < PAGE_MARKDOWN_COMPACT_ACTIONS_WIDTH;\n\n  return (\n    <div ref={controlsRef} className=\"shrink-0\">\n      <ViewerControls\n        className={className}\n        title={\n          <div className=\"flex min-w-0 items-center\">\n            <ModeTabs mode={mode} onChange={onModeChange} />\n          </div>\n        }\n        position={{ kind: \"page\", current: currentPage, total: pageCount }}\n        zoom={{\n          scale,\n          onZoomOut: () => onZoom(1 / 1.2),\n          onZoomIn: () => onZoom(1.2),\n          onFit: onFitWidth,\n        }}\n        extra={\n          isCompact ? (\n            <MarkdownActionsMenu text={text} fileName={fileName} />\n          ) : (\n            <MarkdownActionButtons text={text} fileName={fileName} />\n          )\n        }\n      />\n    </div>\n  );\n}\n\nfunction ModeTabs({\n  mode,\n  onChange,\n}: {\n  mode: PageMarkdownViewMode;\n  onChange: (mode: PageMarkdownViewMode) => void;\n}) {\n  return (\n    <Tabs\n      value={mode}\n      onValueChange={(value) => onChange(value as PageMarkdownViewMode)}\n    >\n      <TabsList variant=\"line\" className=\"py-0\">\n        <TabsTrigger value=\"rendered\" className=\"h-8 text-xs sm:text-xs\">\n          Rendered\n        </TabsTrigger>\n        <TabsTrigger value=\"text\" className=\"h-8 text-xs sm:text-xs\">\n          Text\n        </TabsTrigger>\n      </TabsList>\n    </Tabs>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-controls.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-actions.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Check, Copy, Download, MoreHorizontal } from \"lucide-react\";\n\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\nimport { createTextDownloadAction } from \"@/lib/viewer-download-actions\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport {\n  useViewerDownloadTrigger,\n  type ViewerDownloadErrorHandler,\n} from \"@/components/ui/viewer-download\";\n\ntype CopyStatus = \"idle\" | \"copied\" | \"failed\";\n\nfunction scheduleCopyStatusReset(\n  timeoutRef: React.MutableRefObject<number | null>,\n  setStatus: React.Dispatch<React.SetStateAction<CopyStatus>>,\n) {\n  timeoutRef.current = window.setTimeout(() => {\n    timeoutRef.current = null;\n    setStatus(\"idle\");\n  }, 1200);\n}\n\nfunction clearCopyStatusReset(\n  timeoutRef: React.MutableRefObject<number | null>,\n) {\n  if (timeoutRef.current === null) return;\n  window.clearTimeout(timeoutRef.current);\n  timeoutRef.current = null;\n}\n\nexport function MarkdownActionButtons({\n  text,\n  fileName,\n  onDownloadError,\n}: {\n  text: string;\n  fileName: string;\n  onDownloadError?: ViewerDownloadErrorHandler;\n}) {\n  return (\n    <>\n      <CopyMarkdownButton text={text} />\n      <DownloadMarkdownButton\n        text={text}\n        fileName={fileName}\n        onDownloadError={onDownloadError}\n      />\n    </>\n  );\n}\n\nexport function MarkdownActionsMenu({\n  text,\n  fileName,\n  onDownloadError,\n}: {\n  text: string;\n  fileName: string;\n  onDownloadError?: ViewerDownloadErrorHandler;\n}) {\n  const copy = useCopyMarkdown(text);\n  const download = useDownloadMarkdown(text, fileName, onDownloadError);\n\n  return (\n    <DropdownMenu modal={false}>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"iconSm\"\n          className=\"size-7\"\n          aria-label=\"More markdown actions\"\n          title=\"More markdown actions\"\n        >\n          <MoreHorizontal />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-44\">\n        <DropdownMenuItem onClick={copy.write}>\n          <Copy />\n          {copy.status === \"copied\"\n            ? \"Copied\"\n            : copy.status === \"failed\"\n              ? \"Copy failed\"\n              : \"Copy markdown\"}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          disabled={download.isPending}\n          onClick={download.trigger}\n        >\n          <Download />\n          Download markdown\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction CopyMarkdownButton({ text }: { text: string }) {\n  const copy = useCopyMarkdown(text);\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"iconSm\"\n      className=\"size-7\"\n      aria-label={copy.status === \"failed\" ? \"Copy failed\" : \"Copy markdown\"}\n      title={copy.status === \"failed\" ? \"Copy failed\" : \"Copy all markdown\"}\n      onClick={copy.write}\n    >\n      {copy.status === \"copied\" ? (\n        <Check className=\"text-emerald-600\" />\n      ) : (\n        <Copy className={copy.status === \"failed\" ? \"text-destructive\" : \"\"} />\n      )}\n    </Button>\n  );\n}\n\nfunction DownloadMarkdownButton({\n  text,\n  fileName,\n  onDownloadError,\n}: {\n  text: string;\n  fileName: string;\n  onDownloadError?: ViewerDownloadErrorHandler;\n}) {\n  const download = useDownloadMarkdown(text, fileName, onDownloadError);\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"iconSm\"\n      className=\"size-7\"\n      aria-label=\"Download markdown\"\n      title=\"Download markdown\"\n      disabled={download.isPending}\n      onClick={download.trigger}\n    >\n      {download.isPending ? (\n        <Spinner className=\"size-4 animate-spin\" />\n      ) : (\n        <Download />\n      )}\n    </Button>\n  );\n}\n\nfunction useCopyMarkdown(text: string) {\n  const [status, setStatus] = React.useState<CopyStatus>(\"idle\");\n  const timeoutRef = React.useRef<number | null>(null);\n  const isMountedRef = React.useRef(true);\n  const copyAttemptRef = React.useRef(0);\n\n  useMountEffect(() => {\n    isMountedRef.current = true;\n    return () => {\n      isMountedRef.current = false;\n      clearCopyStatusReset(timeoutRef);\n    };\n  });\n\n  const write = React.useCallback(() => {\n    clearCopyStatusReset(timeoutRef);\n    const copyAttempt = copyAttemptRef.current + 1;\n    copyAttemptRef.current = copyAttempt;\n\n    const isCurrentCopyAttempt = () =>\n      isMountedRef.current && copyAttemptRef.current === copyAttempt;\n\n    try {\n      const clipboard = navigator.clipboard;\n      const writeText = clipboard?.writeText;\n      if (typeof writeText !== \"function\") {\n        setStatus(\"failed\");\n        scheduleCopyStatusReset(timeoutRef, setStatus);\n        return;\n      }\n\n      Promise.resolve(writeText.call(clipboard, text)).then(\n        () => {\n          if (!isCurrentCopyAttempt()) return;\n          setStatus(\"copied\");\n          scheduleCopyStatusReset(timeoutRef, setStatus);\n        },\n        () => {\n          if (!isCurrentCopyAttempt()) return;\n          setStatus(\"failed\");\n          scheduleCopyStatusReset(timeoutRef, setStatus);\n        },\n      );\n    } catch {\n      setStatus(\"failed\");\n      scheduleCopyStatusReset(timeoutRef, setStatus);\n    }\n  }, [text]);\n\n  return { status, write };\n}\n\nfunction useDownloadMarkdown(\n  text: string,\n  fileName: string | undefined,\n  onError: ViewerDownloadErrorHandler | undefined,\n) {\n  const action = React.useMemo(\n    () => downloadMarkdownAction(text, fileName),\n    [fileName, text],\n  );\n  const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({\n    onError,\n    resetKey: action,\n  });\n  const trigger = React.useCallback(() => {\n    triggerDownload(action);\n  }, [action, triggerDownload]);\n\n  return {\n    isPending: pendingActionId === action.id,\n    trigger,\n  };\n}\n\nexport function normalizeMarkdownFileName(fileName?: string): string {\n  const trimmed = fileName?.trim();\n  if (!trimmed) return \"document.md\";\n  if (/\\.(?:md|markdown)$/i.test(trimmed)) return trimmed;\n\n  const slashIndex = Math.max(\n    trimmed.lastIndexOf(\"/\"),\n    trimmed.lastIndexOf(\"\\\\\"),\n  );\n  const dotIndex = trimmed.lastIndexOf(\".\");\n  if (dotIndex > slashIndex + 1) return `${trimmed.slice(0, dotIndex)}.md`;\n\n  return `${trimmed}.md`;\n}\n\nexport function downloadMarkdownAction(text: string, fileName?: string) {\n  return createTextDownloadAction({\n    id: \"download-markdown\",\n    label: \"Download markdown\",\n    text,\n    fileName: normalizeMarkdownFileName(fileName),\n    mimeType: \"text/markdown;charset=utf-8\",\n    origin: \"derived\",\n  });\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-actions.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-page-frame.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { PageMarkdownContent } from \"@/components/viewers/page-markdown/page-markdown-content\";\nimport {\n  PAGE_MARKDOWN_PAGE_PADDING_X,\n  PAGE_MARKDOWN_PAGE_PADDING_Y,\n  PAGE_MARKDOWN_PAGE_WIDTH,\n} from \"@/components/viewers/page-markdown/page-markdown-layout\";\nimport { type PageMarkdownViewMode } from \"@/components/viewers/page-markdown/page-markdown-types\";\n\nexport const PageMarkdownPageFrame = React.memo(function PageMarkdownPageFrame({\n  estimatedHeight,\n  pageNumber,\n  markdown,\n  mode,\n  onSize,\n  scale,\n}: {\n  estimatedHeight: number;\n  markdown: string;\n  mode: PageMarkdownViewMode;\n  onSize: (pageNumber: number, height: number) => void;\n  pageNumber: number;\n  scale: number;\n}) {\n  const pageElementRef = React.useRef<HTMLDivElement | null>(null);\n  const reportSize = React.useCallback(() => {\n    const pageElement = pageElementRef.current;\n    if (!pageElement) return;\n\n    const height = pageElement.offsetHeight;\n    if (height > 0) onSize(pageNumber, height);\n  }, [onSize, pageNumber]);\n  const pageRef = React.useCallback(\n    (pageElement: HTMLDivElement | null) => {\n      pageElementRef.current = pageElement;\n      if (!pageElement) return;\n\n      reportSize();\n\n      if (typeof ResizeObserver !== \"function\") return;\n\n      const observer = new ResizeObserver(reportSize);\n      observer.observe(pageElement);\n      return () => observer.disconnect();\n    },\n    [reportSize],\n  );\n\n  const pageWidth = PAGE_MARKDOWN_PAGE_WIDTH * scale;\n  const pagePaddingX = PAGE_MARKDOWN_PAGE_PADDING_X * scale;\n  const pagePaddingY = PAGE_MARKDOWN_PAGE_PADDING_Y * scale;\n\n  return (\n    <div\n      ref={pageRef}\n      data-slot=\"page-markdown-page\"\n      data-page={pageNumber}\n      className=\"bg-card ring-border relative w-full max-w-3xl shadow-sm ring-1\"\n      style={{\n        minHeight: estimatedHeight,\n        width: `${pageWidth}px`,\n        maxWidth: scale <= 1 ? \"100%\" : \"none\",\n        paddingInline: `${pagePaddingX}px`,\n        paddingBlock: `${pagePaddingY}px`,\n      }}\n    >\n      <PageMarkdownContent\n        markdown={markdown}\n        mode={mode}\n        onRenderedContentChange={reportSize}\n        scale={scale}\n      />\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-page-frame.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-content.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { usePageMarkdownProjection } from \"@/components/viewers/page-markdown/page-markdown-projection\";\nimport { type PageMarkdownViewMode } from \"@/components/viewers/page-markdown/page-markdown-types\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport function PageMarkdownContent({\n  markdown,\n  mode,\n  onRenderedContentChange,\n  scale,\n}: {\n  markdown: string;\n  mode: PageMarkdownViewMode;\n  onRenderedContentChange?: () => void;\n  scale: number;\n}) {\n  if (mode === \"text\") {\n    return (\n      <pre\n        className=\"text-foreground/90 font-mono leading-relaxed whitespace-pre-wrap\"\n        style={{ fontSize: `${12 * scale}px` }}\n      >\n        <PageMarkdownText markdown={markdown} />\n      </pre>\n    );\n  }\n\n  return (\n    <div className=\"leading-relaxed\" style={{ fontSize: `${14 * scale}px` }}>\n      <PageMarkdownRenderedProjection\n        markdown={markdown}\n        onProjectionChange={onRenderedContentChange}\n      />\n    </div>\n  );\n}\n\nconst PageMarkdownText = React.memo(function PageMarkdownText({\n  markdown,\n}: {\n  markdown: string;\n}) {\n  return markdown;\n});\n\nconst PageMarkdownRenderedProjection = React.memo(\n  function PageMarkdownRenderedProjection({\n    markdown,\n    onProjectionChange,\n  }: {\n    markdown: string;\n    onProjectionChange?: () => void;\n  }) {\n    const projection = usePageMarkdownProjection(markdown);\n    useKeyedLayoutEffect(\n      projection\n        ? joinEffectKey([\n            \"page-markdown-projection\",\n            projection,\n            onProjectionChange,\n          ])\n        : null,\n      () => {\n        onProjectionChange?.();\n      },\n    );\n    return projection;\n  },\n);\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-content.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-components.tsx",
      "content": "import { type Components } from \"react-markdown\";\n\nexport const markdownComponents: Components = {\n  h1: ({ node: _node, ...props }) => (\n    <h1\n      className=\"mt-4 mb-2 text-[1.285em] font-semibold first:mt-0\"\n      {...props}\n    />\n  ),\n  h2: ({ node: _node, ...props }) => (\n    <h2\n      className=\"mt-4 mb-2 text-[1.14em] font-semibold first:mt-0\"\n      {...props}\n    />\n  ),\n  h3: ({ node: _node, ...props }) => (\n    <h3\n      className=\"mt-3 mb-1.5 text-[1em] font-semibold first:mt-0\"\n      {...props}\n    />\n  ),\n  h4: ({ node: _node, ...props }) => (\n    <h4 className=\"mt-3 mb-1.5 text-[1em] font-medium first:mt-0\" {...props} />\n  ),\n  p: ({ node: _node, ...props }) => (\n    <p className=\"my-2 leading-relaxed\" {...props} />\n  ),\n  ul: ({ node: _node, ...props }) => (\n    <ul className=\"my-2 ml-5 list-disc space-y-1\" {...props} />\n  ),\n  ol: ({ node: _node, ...props }) => (\n    <ol className=\"my-2 ml-5 list-decimal space-y-1\" {...props} />\n  ),\n  li: ({ node: _node, ...props }) => (\n    <li className=\"leading-relaxed\" {...props} />\n  ),\n  a: ({ node: _node, href, children, ...props }) => {\n    const safeHref = sanitizePageMarkdownUrl(href ?? \"\");\n    return safeHref ? (\n      <a\n        className=\"text-primary font-medium underline underline-offset-2\"\n        {...props}\n        href={safeHref}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n      >\n        {children}\n      </a>\n    ) : (\n      <span>{children}</span>\n    );\n  },\n  img: ({ node: _node, src, alt, ...props }) => {\n    const safeSrc = sanitizePageMarkdownUrl(typeof src === \"string\" ? src : \"\");\n    return safeSrc ? (\n      <img {...props} src={safeSrc} alt={alt} />\n    ) : (\n      <span>{alt}</span>\n    );\n  },\n  strong: ({ node: _node, ...props }) => (\n    <strong className=\"font-semibold\" {...props} />\n  ),\n  hr: ({ node: _node, ...props }) => (\n    <hr className=\"border-border my-4\" {...props} />\n  ),\n  blockquote: ({ node: _node, ...props }) => (\n    <blockquote\n      className=\"border-border text-muted-foreground my-3 border-l-2 pl-3 italic\"\n      {...props}\n    />\n  ),\n  code: ({ node: _node, ...props }) => (\n    <code\n      className=\"bg-muted rounded px-1 py-0.5 font-mono text-[0.85em]\"\n      {...props}\n    />\n  ),\n  pre: ({ node: _node, ...props }) => (\n    <pre\n      className=\"bg-muted/50 my-3 overflow-x-auto rounded-lg border p-3 font-mono text-[0.85em]\"\n      {...props}\n    />\n  ),\n  table: ({ node: _node, ...props }) => (\n    <div className=\"my-3 overflow-x-auto rounded-lg border\">\n      <table className=\"w-full border-collapse text-[0.85em]\" {...props} />\n    </div>\n  ),\n  thead: ({ node: _node, ...props }) => (\n    <thead className=\"bg-muted/60\" {...props} />\n  ),\n  th: ({ node: _node, ...props }) => (\n    <th\n      className=\"border-border border-b px-3 py-1.5 text-left font-medium [&[align=right]]:text-right\"\n      {...props}\n    />\n  ),\n  td: ({ node: _node, ...props }) => (\n    <td\n      className=\"border-border border-b px-3 py-1.5 tabular-nums [&[align=right]]:text-right\"\n      {...props}\n    />\n  ),\n};\n\nfunction sanitizePageMarkdownUrl(value: string): string {\n  const colon = value.indexOf(\":\");\n  const questionMark = value.indexOf(\"?\");\n  const numberSign = value.indexOf(\"#\");\n  const slash = value.indexOf(\"/\");\n\n  if (\n    colon === -1 ||\n    (slash !== -1 && colon > slash) ||\n    (questionMark !== -1 && colon > questionMark) ||\n    (numberSign !== -1 && colon > numberSign) ||\n    /^(https?|ircs?|mailto|xmpp)$/i.test(value.slice(0, colon))\n  ) {\n    return value;\n  }\n\n  return \"\";\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-components.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-layout.ts",
      "content": "import { type PageMarkdownViewMode } from \"./page-markdown-types\";\n\nexport const PAGE_MARKDOWN_PAGE_WIDTH = 768;\nexport const PAGE_MARKDOWN_PAGE_PADDING_X = 36;\nexport const PAGE_MARKDOWN_PAGE_PADDING_Y = 28;\nexport const PAGE_MARKDOWN_PAGE_GAP = 16;\nexport const PAGE_MARKDOWN_PAGE_PADDING = 16;\n\nconst PAGE_MARKDOWN_ESTIMATE_MIN = 180;\nconst PAGE_MARKDOWN_ESTIMATE_MAX = 1800;\nconst PAGE_MARKDOWN_TEXT_LINE_HEIGHT = 22;\nconst PAGE_MARKDOWN_RENDERED_LINE_HEIGHT = 26;\nconst PAGE_MARKDOWN_ESTIMATE_VERTICAL_PADDING = 80;\nconst PAGE_MARKDOWN_ESTIMATE_CHARS_PER_LINE = 82;\n\nexport type PageMarkdownMeasuredPageLayout = {\n  height: number;\n  heightDelta: number;\n  pageNumber: number;\n};\n\nexport type PageMarkdownPageLayout = {\n  height: number;\n  offsetTop: number;\n  pageNumber: number;\n  width: number;\n};\n\nexport type PageMarkdownLayoutModel = {\n  estimatedHeights: readonly number[];\n  measuredPageByNumber: ReadonlyMap<number, PageMarkdownMeasuredPageLayout>;\n  measuredPages: readonly PageMarkdownMeasuredPageLayout[];\n  pageCount: number;\n  prefixEstimatedHeights: readonly number[];\n  prefixHeightDeltas: readonly number[];\n  totalHeight: number;\n  width: number;\n};\n\nexport function createPageMarkdownLayout({\n  measuredHeightByPageNumber,\n  mode,\n  pages,\n  scale,\n  width = PAGE_MARKDOWN_PAGE_WIDTH,\n}: {\n  measuredHeightByPageNumber: ReadonlyMap<number, number>;\n  mode: PageMarkdownViewMode;\n  pages: readonly string[];\n  scale: number;\n  width?: number;\n}): PageMarkdownLayoutModel {\n  const safeScale = safePageScale(scale);\n  const pageCount = pages.length;\n  const estimatedHeights = pages.map((markdown) =>\n    estimateMarkdownPageHeight(markdown, safeScale, mode),\n  );\n  const prefixEstimatedHeights: number[] = [];\n  let estimatedTotalHeight = 0;\n\n  for (const height of estimatedHeights) {\n    prefixEstimatedHeights.push(estimatedTotalHeight);\n    estimatedTotalHeight += height;\n  }\n\n  const measuredPages = Array.from(measuredHeightByPageNumber.entries())\n    .filter(([pageNumber, height]) => {\n      return (\n        pageNumber >= 1 &&\n        pageNumber <= pageCount &&\n        Number.isFinite(height) &&\n        height > 0\n      );\n    })\n    .map(([pageNumber, height]) => ({\n      height,\n      heightDelta: height - estimatedHeights[pageNumber - 1]!,\n      pageNumber,\n    }))\n    .filter((measuredPage) => measuredPage.heightDelta !== 0)\n    .sort((a, b) => a.pageNumber - b.pageNumber);\n\n  const measuredPageByNumber = new Map(\n    measuredPages.map((measuredPage) => [\n      measuredPage.pageNumber,\n      measuredPage,\n    ]),\n  );\n  const prefixHeightDeltas: number[] = [];\n  let totalHeightDelta = 0;\n  for (const measuredPage of measuredPages) {\n    totalHeightDelta += measuredPage.heightDelta;\n    prefixHeightDeltas.push(totalHeightDelta);\n  }\n\n  return {\n    estimatedHeights,\n    measuredPageByNumber,\n    measuredPages,\n    pageCount,\n    prefixEstimatedHeights,\n    prefixHeightDeltas,\n    totalHeight:\n      pageCount === 0\n        ? 0\n        : PAGE_MARKDOWN_PAGE_PADDING * 2 +\n          estimatedTotalHeight +\n          (pageCount - 1) * PAGE_MARKDOWN_PAGE_GAP +\n          totalHeightDelta,\n    width: Math.max(1, width * safeScale),\n  };\n}\n\nexport function getPageMarkdownPageLayout(\n  layout: PageMarkdownLayoutModel,\n  pageNumber: number,\n): PageMarkdownPageLayout | undefined {\n  if (pageNumber < 1 || pageNumber > layout.pageCount) return undefined;\n\n  const measuredPage = layout.measuredPageByNumber.get(pageNumber);\n  return {\n    height: measuredPage?.height ?? layout.estimatedHeights[pageNumber - 1]!,\n    offsetTop: getPageMarkdownPageOffsetTop(layout, pageNumber),\n    pageNumber,\n    width: layout.width,\n  };\n}\n\nexport function findPageMarkdownPageByOffset(\n  layout: PageMarkdownLayoutModel,\n  offset: number,\n) {\n  if (layout.pageCount === 0) return 1;\n\n  let low = 1;\n  let high = layout.pageCount;\n  let match = 1;\n\n  while (low <= high) {\n    const mid = Math.floor((low + high) / 2);\n    if (getPageMarkdownPageOffsetTop(layout, mid) <= offset) {\n      match = mid;\n      low = mid + 1;\n    } else {\n      high = mid - 1;\n    }\n  }\n\n  return match;\n}\n\nexport function getPageMarkdownVisiblePageNumbers({\n  layout,\n  overscanPages = 2,\n  scrollTop,\n  viewportHeight,\n}: {\n  layout: PageMarkdownLayoutModel;\n  overscanPages?: number;\n  scrollTop: number;\n  viewportHeight: number;\n}) {\n  if (layout.pageCount === 0) return [];\n\n  const safeViewportHeight = Math.max(0, viewportHeight);\n  const startOffset = Math.max(0, scrollTop - safeViewportHeight);\n  const endOffset = scrollTop + safeViewportHeight * 2;\n  const firstVisiblePage = findPageMarkdownPageByOffset(layout, startOffset);\n  const lastVisiblePage = findPageMarkdownPageByOffset(layout, endOffset);\n  const firstPage = Math.max(1, firstVisiblePage - overscanPages);\n  const lastPage = Math.min(layout.pageCount, lastVisiblePage + overscanPages);\n\n  return Array.from(\n    { length: lastPage - firstPage + 1 },\n    (_, index) => firstPage + index,\n  );\n}\n\nexport function estimateMarkdownPageHeight(\n  markdown: string,\n  scale: number,\n  mode: PageMarkdownViewMode = \"rendered\",\n): number {\n  const safeScale = safePageScale(scale);\n  const estimatedContentHeight =\n    mode === \"text\"\n      ? estimateTextContentHeight(markdown)\n      : estimateRenderedContentHeight(markdown);\n\n  return Math.min(\n    PAGE_MARKDOWN_ESTIMATE_MAX * safeScale,\n    Math.max(\n      PAGE_MARKDOWN_ESTIMATE_MIN * safeScale,\n      estimatedContentHeight * safeScale +\n        PAGE_MARKDOWN_ESTIMATE_VERTICAL_PADDING * safeScale,\n    ),\n  );\n}\n\nexport function createPageMeasurementKey({\n  markdown,\n  mode,\n  scale,\n}: {\n  markdown: string;\n  mode: string;\n  scale: number;\n}): string {\n  return `${mode}:${safePageScale(scale).toFixed(3)}:${markdown.length}:${hashMarkdown(markdown)}`;\n}\n\nfunction getPageMarkdownPageOffsetTop(\n  layout: PageMarkdownLayoutModel,\n  pageNumber: number,\n) {\n  return (\n    PAGE_MARKDOWN_PAGE_PADDING +\n    layout.prefixEstimatedHeights[pageNumber - 1]! +\n    (pageNumber - 1) * PAGE_MARKDOWN_PAGE_GAP +\n    getHeightDeltaBeforePage(layout, pageNumber)\n  );\n}\n\nfunction getHeightDeltaBeforePage(\n  layout: PageMarkdownLayoutModel,\n  pageNumber: number,\n) {\n  const measuredPageIndex = getLastMeasuredPageIndexBefore(layout, pageNumber);\n  return measuredPageIndex < 0\n    ? 0\n    : layout.prefixHeightDeltas[measuredPageIndex]!;\n}\n\nfunction getLastMeasuredPageIndexBefore(\n  layout: PageMarkdownLayoutModel,\n  pageNumber: number,\n) {\n  let low = 0;\n  let high = layout.measuredPages.length - 1;\n  let match = -1;\n\n  while (low <= high) {\n    const mid = Math.floor((low + high) / 2);\n    if (layout.measuredPages[mid]!.pageNumber < pageNumber) {\n      match = mid;\n      low = mid + 1;\n    } else {\n      high = mid - 1;\n    }\n  }\n\n  return match;\n}\n\nfunction safePageScale(scale: number) {\n  return Number.isFinite(scale) && scale > 0 ? scale : 1;\n}\n\nfunction estimateTextContentHeight(markdown: string): number {\n  return (\n    Math.max(1, markdown.split(\"\\n\").length) * PAGE_MARKDOWN_TEXT_LINE_HEIGHT\n  );\n}\n\nfunction estimateRenderedContentHeight(markdown: string): number {\n  const lines = markdown.split(\"\\n\");\n  let height = 0;\n  let index = 0;\n\n  while (index < lines.length) {\n    const line = lines[index]!;\n    const trimmed = line.trim();\n\n    if (!trimmed) {\n      height += 10;\n      index += 1;\n      continue;\n    }\n\n    if (/^```|^~~~/.test(trimmed)) {\n      const fence = trimmed.slice(0, 3);\n      let codeLines = 0;\n      index += 1;\n      while (index < lines.length && !lines[index]!.trim().startsWith(fence)) {\n        codeLines += 1;\n        index += 1;\n      }\n      if (index < lines.length) index += 1;\n      height += 42 + Math.max(1, codeLines) * 22;\n      continue;\n    }\n\n    if (isMarkdownTableStart(lines, index)) {\n      let rowCount = 0;\n      while (index < lines.length && isMarkdownTableRow(lines[index]!)) {\n        rowCount += 1;\n        index += 1;\n      }\n      height += 34 + Math.max(1, rowCount) * 34;\n      continue;\n    }\n\n    const heading = /^(#{1,6})\\s+/.exec(trimmed);\n    if (heading) {\n      height += Math.max(30, 48 - heading[1]!.length * 3);\n      index += 1;\n      continue;\n    }\n\n    if (/^!\\[[^\\]]*]\\([^)]+\\)/.test(trimmed)) {\n      height += 260;\n      index += 1;\n      continue;\n    }\n\n    if (/^(?:[-*+]\\s+|\\d+\\.\\s+|>\\s*)/.test(trimmed)) {\n      height += PAGE_MARKDOWN_RENDERED_LINE_HEIGHT + 8;\n      index += 1;\n      continue;\n    }\n\n    let paragraphChars = trimmed.length;\n    index += 1;\n    while (index < lines.length && lines[index]!.trim()) {\n      const nextLine = lines[index]!.trim();\n      if (\n        /^```|^~~~/.test(nextLine) ||\n        /^#{1,6}\\s+/.test(nextLine) ||\n        /^!\\[[^\\]]*]\\([^)]+\\)/.test(nextLine) ||\n        /^(?:[-*+]\\s+|\\d+\\.\\s+|>\\s*)/.test(nextLine) ||\n        isMarkdownTableStart(lines, index)\n      ) {\n        break;\n      }\n      paragraphChars += 1 + nextLine.length;\n      index += 1;\n    }\n\n    height +=\n      Math.ceil(paragraphChars / PAGE_MARKDOWN_ESTIMATE_CHARS_PER_LINE) *\n        PAGE_MARKDOWN_RENDERED_LINE_HEIGHT +\n      14;\n  }\n\n  return height;\n}\n\nfunction isMarkdownTableStart(\n  lines: readonly string[],\n  index: number,\n): boolean {\n  return (\n    index + 1 < lines.length &&\n    isMarkdownTableRow(lines[index]!) &&\n    /^\\s*\\|?\\s*:?-{3,}:?\\s*(?:\\|\\s*:?-{3,}:?\\s*)+\\|?\\s*$/.test(\n      lines[index + 1]!,\n    )\n  );\n}\n\nfunction isMarkdownTableRow(line: string): boolean {\n  const trimmed = line.trim();\n  return trimmed.includes(\"|\") && !/^```|^~~~/.test(trimmed);\n}\n\nfunction hashMarkdown(markdown: string): string {\n  let hash = 2166136261;\n  for (let index = 0; index < markdown.length; index += 1) {\n    hash ^= markdown.charCodeAt(index);\n    hash = Math.imul(hash, 16777619);\n  }\n  return (hash >>> 0).toString(36);\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-layout.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-scale.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\n\nimport { PAGE_MARKDOWN_PAGE_WIDTH } from \"./page-markdown-layout\";\n\nexport const PAGE_MARKDOWN_SCALE_MIN = 0.35;\nexport const PAGE_MARKDOWN_SCALE_MAX = 3;\nexport const PAGE_MARKDOWN_FIT_SCALE_MAX = 1.5;\nexport const PAGE_MARKDOWN_FIT_HORIZONTAL_PADDING = 32;\n\nexport function clamp(value: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, value));\n}\n\nexport function clampPageScale(scale: number): number {\n  if (!Number.isFinite(scale)) return 1;\n  return clamp(scale, PAGE_MARKDOWN_SCALE_MIN, PAGE_MARKDOWN_SCALE_MAX);\n}\n\nexport function fitPageScale(containerWidth: number | null): number {\n  if (!containerWidth || !Number.isFinite(containerWidth)) return 1;\n  return clamp(\n    (containerWidth - PAGE_MARKDOWN_FIT_HORIZONTAL_PADDING) /\n      PAGE_MARKDOWN_PAGE_WIDTH,\n    PAGE_MARKDOWN_SCALE_MIN,\n    PAGE_MARKDOWN_FIT_SCALE_MAX,\n  );\n}\n\nexport function zoomPageScale(scale: number, factor: number): number {\n  return clampPageScale(scale * factor);\n}\n\nexport function usePageMarkdownScale({\n  containerWidth,\n  defaultScale,\n  onScaleChange,\n  pageWidth = PAGE_MARKDOWN_PAGE_WIDTH,\n  resetKey,\n  scale: controlledScale,\n}: {\n  containerWidth: number | null;\n  defaultScale?: number;\n  onScaleChange?: (scale: number | null) => void;\n  pageWidth?: number;\n  resetKey?: string;\n  scale?: number;\n}) {\n  const normalizedControlledScale = normalizeOptionalPageScale(controlledScale);\n  const isScaleControlled = controlledScale != null;\n  const normalizedDefaultScale = normalizeOptionalPageScale(defaultScale);\n  const [manualScale, setManualScale] = React.useState<number | null>(\n    normalizedDefaultScale,\n  );\n\n  useKeyedMountEffect(\n    `page-markdown-scale:${resetKey ?? \"\"}:${normalizedDefaultScale ?? \"fit\"}`,\n    () => {\n      setManualScale(normalizedDefaultScale);\n    },\n  );\n\n  const fitScale =\n    containerWidth && Number.isFinite(containerWidth)\n      ? clamp(\n          (containerWidth - PAGE_MARKDOWN_FIT_HORIZONTAL_PADDING) / pageWidth,\n          PAGE_MARKDOWN_SCALE_MIN,\n          PAGE_MARKDOWN_FIT_SCALE_MAX,\n        )\n      : 1;\n  const scale = normalizedControlledScale ?? manualScale ?? fitScale;\n\n  const setViewerScale = React.useCallback(\n    (nextScale: number | null) => {\n      const normalized =\n        nextScale == null ? null : normalizeOptionalPageScale(nextScale);\n      if (isScaleControlled) {\n        onScaleChange?.(normalized);\n        return;\n      }\n      setManualScale(normalized);\n      onScaleChange?.(normalized);\n    },\n    [isScaleControlled, onScaleChange],\n  );\n\n  const zoomIn = React.useCallback(() => {\n    setViewerScale(zoomPageScale(scale, 1.2));\n  }, [scale, setViewerScale]);\n\n  const zoomOut = React.useCallback(() => {\n    setViewerScale(zoomPageScale(scale, 1 / 1.2));\n  }, [scale, setViewerScale]);\n\n  const fitWidth = React.useCallback(() => {\n    setViewerScale(null);\n  }, [setViewerScale]);\n\n  return {\n    fitWidth,\n    scale,\n    setViewerScale,\n    zoomIn,\n    zoomOut,\n  };\n}\n\nfunction normalizeOptionalPageScale(scale: number | undefined) {\n  return scale == null ? null : clampPageScale(scale);\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-scale.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-scroll.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\n\nimport {\n  findPageMarkdownPageByOffset,\n  getPageMarkdownPageLayout,\n  type PageMarkdownLayoutModel,\n} from \"./page-markdown-layout\";\nimport { clamp } from \"./page-markdown-scale\";\n\nexport function usePageMarkdownScroll({\n  layout,\n  onScrollProgressChange,\n  onVisiblePageChange,\n  pageCount,\n  resetKey,\n}: {\n  layout: PageMarkdownLayoutModel;\n  onScrollProgressChange?: (progress: number) => void;\n  onVisiblePageChange?: (pageNumber: number) => void;\n  pageCount: number;\n  resetKey?: string;\n}) {\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null);\n  const lastReportedPageNumberRef = React.useRef(0);\n  const scrollFrameRef = React.useRef(0);\n  const viewportResetKeyRef = React.useRef<unknown>(resetKey);\n  const didMountResetEffectRef = React.useRef(false);\n  const [viewportElement, setViewportElementState] =\n    React.useState<HTMLDivElement | null>(null);\n  const [currentPageState, setCurrentPageState] = React.useState<{\n    pageNumber: number;\n    resetKey: unknown;\n  }>(() => ({ pageNumber: 1, resetKey }));\n  const currentPage = Object.is(currentPageState.resetKey, resetKey)\n    ? currentPageState.pageNumber\n    : 1;\n\n  const resetViewportForKey = React.useCallback(\n    (element: HTMLDivElement, key: unknown) => {\n      viewportResetKeyRef.current = key;\n      element.scrollTop = 0;\n      scrollViewportTo(element, 0, { behavior: \"auto\" });\n    },\n    [],\n  );\n\n  const setViewportElement = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      viewportElementRef.current = element;\n      if (element && !Object.is(viewportResetKeyRef.current, resetKey)) {\n        resetViewportForKey(element, resetKey);\n      }\n      setViewportElementState(element);\n    },\n    [resetKey, resetViewportForKey],\n  );\n\n  const measureScroll = React.useCallback(() => {\n    scrollFrameRef.current = 0;\n    const viewportElement = viewportElementRef.current;\n    if (!viewportElement) return;\n\n    const viewportHeight =\n      viewportElement.clientHeight ||\n      viewportElement.getBoundingClientRect().height;\n    const scrollable =\n      viewportElement.scrollHeight - viewportElement.clientHeight;\n    const progress =\n      scrollable > 0 ? clamp(viewportElement.scrollTop / scrollable, 0, 1) : 0;\n    onScrollProgressChange?.(progress);\n\n    const markerOffset = viewportElement.scrollTop + viewportHeight * 0.2;\n    const visiblePageNumber = findPageMarkdownPageByOffset(\n      layout,\n      markerOffset,\n    );\n\n    if (\n      visiblePageNumber >= 1 &&\n      visiblePageNumber <= pageCount &&\n      visiblePageNumber !== lastReportedPageNumberRef.current\n    ) {\n      lastReportedPageNumberRef.current = visiblePageNumber;\n      setCurrentPageState((previousState) =>\n        Object.is(previousState.resetKey, resetKey) &&\n        previousState.pageNumber === visiblePageNumber\n          ? previousState\n          : { pageNumber: visiblePageNumber, resetKey },\n      );\n      onVisiblePageChange?.(visiblePageNumber);\n    }\n  }, [\n    layout,\n    onScrollProgressChange,\n    onVisiblePageChange,\n    pageCount,\n    resetKey,\n  ]);\n  const measureScrollRef = React.useRef(measureScroll);\n  measureScrollRef.current = measureScroll;\n\n  const handleScroll = React.useCallback(() => {\n    if (scrollFrameRef.current) return;\n    if (typeof requestAnimationFrame !== \"function\") {\n      measureScrollRef.current();\n      return;\n    }\n    scrollFrameRef.current = -1;\n    const requestedFrame = requestAnimationFrame(() =>\n      measureScrollRef.current(),\n    );\n    if (scrollFrameRef.current === -1) {\n      scrollFrameRef.current = requestedFrame;\n    }\n  }, []);\n\n  useKeyedMountEffect(createPageMarkdownScrollResetEffectKey(resetKey), () => {\n    if (!didMountResetEffectRef.current) {\n      didMountResetEffectRef.current = true;\n      return;\n    }\n    lastReportedPageNumberRef.current = 0;\n    setCurrentPageState((previousState) =>\n      Object.is(previousState.resetKey, resetKey) &&\n      previousState.pageNumber === 1\n        ? previousState\n        : { pageNumber: 1, resetKey },\n    );\n    const viewportElement = viewportElementRef.current;\n    if (viewportElement) resetViewportForKey(viewportElement, resetKey);\n  });\n\n  const scrollToPage = React.useCallback(\n    (pageNumber: number, options?: ScrollToOptions) => {\n      const viewportElement = viewportElementRef.current;\n      if (!viewportElement || pageNumber < 1 || pageNumber > pageCount) return;\n\n      const pageLayout = getPageMarkdownPageLayout(layout, pageNumber);\n      if (!pageLayout) return;\n\n      scrollViewportTo(viewportElement, Math.max(0, pageLayout.offsetTop), {\n        behavior: \"smooth\",\n        ...options,\n      });\n    },\n    [layout, pageCount],\n  );\n\n  const getViewportElement = React.useCallback(\n    () => viewportElementRef.current,\n    [],\n  );\n\n  useMountEffect(() => () => {\n    if (\n      scrollFrameRef.current > 0 &&\n      typeof cancelAnimationFrame === \"function\"\n    ) {\n      cancelAnimationFrame(scrollFrameRef.current);\n    }\n  });\n\n  return {\n    currentPage,\n    getViewportElement,\n    handleScroll,\n    measureScroll,\n    scrollToPage,\n    setViewportElement,\n    viewportElement,\n  };\n}\n\nfunction createPageMarkdownScrollResetEffectKey(resetKey: string | undefined) {\n  return resetKey === undefined ? \"unset:\" : `value:${resetKey}`;\n}\n\nfunction scrollViewportTo(\n  viewportElement: HTMLDivElement,\n  top: number,\n  options?: ScrollToOptions,\n) {\n  if (typeof viewportElement.scrollTo === \"function\") {\n    viewportElement.scrollTo({ top, ...options });\n    return;\n  }\n  viewportElement.scrollTop = top;\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-scroll.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-measurements.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport {\n  createPageMeasurementKey,\n  findPageMarkdownPageByOffset,\n  getPageMarkdownPageLayout,\n  type PageMarkdownLayoutModel,\n} from \"@/components/viewers/page-markdown/page-markdown-layout\";\nimport { type PageMarkdownViewMode } from \"@/components/viewers/page-markdown/page-markdown-types\";\n\ntype PageHeightMeasurement = {\n  height: number;\n  measurementKey: string;\n};\n\ntype PageMarkdownScrollAnchor = {\n  offsetWithinPage: number;\n  pageNumber: number;\n};\n\nexport function usePageMarkdownMeasurements({\n  mode,\n  pages,\n  scale,\n}: {\n  mode: PageMarkdownViewMode;\n  pages: readonly string[];\n  scale: number;\n}) {\n  const [pageHeightMeasurements, setPageHeightMeasurements] = React.useState<\n    Map<number, PageHeightMeasurement>\n  >(() => new Map());\n\n  const measuredHeightByPageNumber = React.useMemo(() => {\n    const measuredHeightByPageNumber = new Map<number, number>();\n    for (let pageIndex = 0; pageIndex < pages.length; pageIndex += 1) {\n      const pageNumber = pageIndex + 1;\n      const measurement = pageHeightMeasurements.get(pageNumber);\n      if (!measurement) continue;\n\n      const measurementKey = createPageMeasurementKey({\n        markdown: pages[pageIndex]!,\n        mode,\n        scale,\n      });\n      if (measurement.measurementKey === measurementKey) {\n        measuredHeightByPageNumber.set(pageNumber, measurement.height);\n      }\n    }\n    return measuredHeightByPageNumber;\n  }, [mode, pageHeightMeasurements, pages, scale]);\n\n  const setPageHeight = React.useCallback(\n    (\n      pageNumber: number,\n      height: number,\n      beforeMeasurementChange?: () => void,\n    ) => {\n      const markdown = pages[pageNumber - 1];\n      if (!markdown || !Number.isFinite(height) || height <= 0) return;\n\n      const measurementKey = createPageMeasurementKey({\n        markdown,\n        mode,\n        scale,\n      });\n      setPageHeightMeasurements((current) => {\n        const currentMeasurement = current.get(pageNumber);\n        if (\n          currentMeasurement?.measurementKey === measurementKey &&\n          currentMeasurement.height === height\n        ) {\n          return current;\n        }\n\n        beforeMeasurementChange?.();\n        const next = new Map(current);\n        next.set(pageNumber, { height, measurementKey });\n        return next;\n      });\n    },\n    [mode, pages, scale],\n  );\n\n  return {\n    measuredHeightByPageNumber,\n    setPageHeight,\n  };\n}\n\nexport function usePageMarkdownScrollAnchor({\n  layout,\n  onRestore,\n  viewportElement,\n}: {\n  layout: PageMarkdownLayoutModel;\n  onRestore?: () => void;\n  viewportElement: HTMLDivElement | null;\n}) {\n  const pendingScrollAnchorRef = React.useRef<PageMarkdownScrollAnchor | null>(\n    null,\n  );\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null);\n  viewportElementRef.current = viewportElement;\n\n  const captureScrollAnchor = React.useCallback(() => {\n    const currentViewportElement = viewportElementRef.current;\n    if (!currentViewportElement) return;\n\n    const pageNumber = findPageMarkdownPageByOffset(\n      layout,\n      currentViewportElement.scrollTop,\n    );\n    const pageLayout = getPageMarkdownPageLayout(layout, pageNumber);\n    if (!pageLayout) return;\n\n    pendingScrollAnchorRef.current = {\n      offsetWithinPage: currentViewportElement.scrollTop - pageLayout.offsetTop,\n      pageNumber,\n    };\n  }, [layout]);\n\n  useKeyedMountEffect(createPageMarkdownAnchorRestoreKey(layout), () => {\n    const anchor = pendingScrollAnchorRef.current;\n    const currentViewportElement = viewportElementRef.current;\n    if (!anchor || !currentViewportElement) return;\n\n    pendingScrollAnchorRef.current = null;\n    const pageLayout = getPageMarkdownPageLayout(layout, anchor.pageNumber);\n    if (!pageLayout) return;\n\n    const nextScrollTop = Math.max(\n      0,\n      pageLayout.offsetTop + anchor.offsetWithinPage,\n    );\n    if (Math.abs(currentViewportElement.scrollTop - nextScrollTop) > 0.5) {\n      currentViewportElement.scrollTop = nextScrollTop;\n    }\n    onRestore?.();\n  });\n\n  return { captureScrollAnchor };\n}\n\nfunction createPageMarkdownAnchorRestoreKey(layout: PageMarkdownLayoutModel) {\n  return [\n    layout.pageCount,\n    layout.totalHeight,\n    layout.width,\n    ...layout.measuredPages.map(\n      (page) => `${page.pageNumber}:${page.height}:${page.heightDelta}`,\n    ),\n  ].join(\"|\");\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-measurements.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-sync.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\n\nexport type PageMarkdownSyncPane = \"markdown\" | \"document\";\n\nexport interface PageMarkdownSyncState {\n  pageNumber: number;\n  pane: PageMarkdownSyncPane;\n}\n\nexport interface PendingPageMarkdownScroll {\n  pageNumber: number;\n  pane: PageMarkdownSyncPane;\n}\n\nexport interface PageMarkdownSyncTransition {\n  confirmed: boolean;\n  pending: PendingPageMarkdownScroll | null;\n  scrollTarget: PendingPageMarkdownScroll | null;\n  state: PageMarkdownSyncState;\n}\n\nexport function initialPageMarkdownSyncState(): PageMarkdownSyncState {\n  return { pageNumber: 1, pane: \"markdown\" };\n}\n\nexport function resolvePageMarkdownSyncReport({\n  state,\n  pending,\n  pane,\n  pageNumber,\n}: {\n  state: PageMarkdownSyncState;\n  pending: PendingPageMarkdownScroll | null;\n  pane: PageMarkdownSyncPane;\n  pageNumber: number;\n}): PageMarkdownSyncTransition {\n  const nextPageNumber = Math.max(\n    1,\n    Number.isFinite(pageNumber) ? Math.floor(pageNumber) : 1,\n  );\n\n  if (pending?.pane === pane && pending.pageNumber === nextPageNumber) {\n    return {\n      confirmed: true,\n      pending: null,\n      scrollTarget: null,\n      state: {\n        pageNumber: nextPageNumber,\n        pane,\n      },\n    };\n  }\n\n  if (pending?.pane === pane) {\n    return {\n      confirmed: false,\n      pending,\n      scrollTarget: null,\n      state,\n    };\n  }\n\n  if (!pending && state.pageNumber === nextPageNumber && state.pane === pane) {\n    return {\n      confirmed: false,\n      pending: null,\n      scrollTarget: null,\n      state,\n    };\n  }\n\n  if (!pending && state.pageNumber === nextPageNumber) {\n    return {\n      confirmed: false,\n      pending: null,\n      scrollTarget: null,\n      state: {\n        pageNumber: nextPageNumber,\n        pane,\n      },\n    };\n  }\n\n  const targetPane: PageMarkdownSyncPane =\n    pane === \"markdown\" ? \"document\" : \"markdown\";\n  const nextState = {\n    pageNumber: nextPageNumber,\n    pane,\n  };\n  const nextPending = {\n    pageNumber: nextPageNumber,\n    pane: targetPane,\n  };\n\n  return {\n    confirmed: false,\n    pending: nextPending,\n    scrollTarget: nextPending,\n    state: nextState,\n  };\n}\n\nexport function usePageMarkdownSync({\n  onMarkdownPageChange,\n  pageCount,\n  resetKey,\n}: {\n  onMarkdownPageChange?: (pageNumber: number) => void;\n  pageCount?: number;\n  resetKey?: string;\n}) {\n  const [state, setState] = React.useState(initialPageMarkdownSyncState);\n  const stateRef = React.useRef(state);\n  const pendingRef = React.useRef<PendingPageMarkdownScroll | null>(null);\n  const pageCountLimit = Math.max(1, pageCount ?? 1);\n  stateRef.current = state;\n\n  useKeyedMountEffect(`page-markdown-sync:${resetKey ?? \"\"}`, () => {\n    const nextState = initialPageMarkdownSyncState();\n    stateRef.current = nextState;\n    pendingRef.current = null;\n    setState(nextState);\n  });\n\n  useKeyedMountEffect(`page-markdown-sync-page-count:${pageCountLimit}`, () => {\n    const currentState = stateRef.current;\n    const nextPageNumber = Math.min(currentState.pageNumber, pageCountLimit);\n    const nextState =\n      nextPageNumber === currentState.pageNumber\n        ? currentState\n        : {\n            ...currentState,\n            pageNumber: nextPageNumber,\n          };\n    const currentPending = pendingRef.current;\n\n    stateRef.current = nextState;\n    pendingRef.current =\n      currentPending && currentPending.pageNumber <= pageCountLimit\n        ? currentPending\n        : null;\n    setState(nextState);\n  });\n\n  const reportPage = React.useCallback(\n    (pane: PageMarkdownSyncPane, pageNumber: number) => {\n      const transition = resolvePageMarkdownSyncReport({\n        state: stateRef.current,\n        pending: pendingRef.current,\n        pane,\n        pageNumber,\n      });\n\n      stateRef.current = transition.state;\n      pendingRef.current = transition.pending;\n      setState(transition.state);\n\n      if (transition.state.pane === \"markdown\" && !transition.confirmed) {\n        onMarkdownPageChange?.(transition.state.pageNumber);\n      }\n\n      return transition.scrollTarget;\n    },\n    [onMarkdownPageChange],\n  );\n\n  return {\n    currentPage: state.pageNumber,\n    reportDocumentPage: React.useCallback(\n      (pageNumber: number) => reportPage(\"document\", pageNumber),\n      [reportPage],\n    ),\n    reportMarkdownPage: React.useCallback(\n      (pageNumber: number) => reportPage(\"markdown\", pageNumber),\n      [reportPage],\n    ),\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-sync.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-model.ts",
      "content": "export const PAGE_MARKDOWN_COMPACT_ACTIONS_WIDTH = 460;\n\nexport function joinMarkdownPages(pages: string[]): string {\n  return pages.join(\"\\n\\n\");\n}\n",
      "type": "registry:lib",
      "target": "@components/viewers/page-markdown/page-markdown-model.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-types.ts",
      "content": "export type PageMarkdownViewMode = \"rendered\" | \"text\";\n\nexport interface PageMarkdownDocumentState {\n  onCurrentPageChange: (pageNumber: number) => void;\n  onScrollProgressChange?: (progress: number) => void;\n  setDocumentHandle: (handle: PageMarkdownDocumentHandle | null) => void;\n}\n\nexport interface PageMarkdownDocumentHandle {\n  scrollToPage: (pageNumber: number, options?: ScrollToOptions) => void;\n}\n\nexport interface PageMarkdownViewerProps {\n  pages: string[];\n  text?: string;\n  isProcessing?: boolean;\n  onVisiblePageChange?: (pageNumber: number) => void;\n  fileName?: string;\n  resetKey?: string;\n  processingLabel?: string;\n}\n",
      "type": "registry:lib",
      "target": "@components/viewers/page-markdown/page-markdown-types.ts"
    },
    {
      "path": "hooks/use-element-width.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\n\nexport function useElementWidth<T extends HTMLElement = HTMLDivElement>(): [\n  React.RefCallback<T>,\n  number | null,\n] {\n  const cleanupRef = React.useRef<(() => void) | null>(null);\n  const [width, setWidth] = React.useState<number | null>(null);\n\n  const ref = React.useCallback((element: T | null) => {\n    cleanupRef.current?.();\n    cleanupRef.current = null;\n\n    if (!element) return;\n    const target = element;\n\n    let frame = 0;\n    let timeout = 0;\n    let isReadScheduled = false;\n    let lastReadWidth: number | null = null;\n\n    function scheduleRead() {\n      if (isReadScheduled) return;\n\n      isReadScheduled = true;\n      const run = () => {\n        if (!isReadScheduled) return;\n        isReadScheduled = false;\n        if (frame > 0 && typeof cancelAnimationFrame === \"function\") {\n          cancelAnimationFrame(frame);\n        }\n        if (timeout) window.clearTimeout(timeout);\n        frame = 0;\n        timeout = 0;\n        readStableWidth();\n      };\n\n      timeout = window.setTimeout(run, 50);\n      if (typeof requestAnimationFrame !== \"function\") return;\n\n      frame = -1;\n      const requestedFrame = requestAnimationFrame(run);\n      if (frame === -1) frame = requestedFrame;\n    }\n\n    function readStableWidth() {\n      const nextWidth = target.clientWidth;\n      if (lastReadWidth === nextWidth) {\n        setWidth(nextWidth);\n        return;\n      }\n      lastReadWidth = nextWidth;\n      scheduleRead();\n    }\n\n    scheduleRead();\n    const cleanupScheduledRead = () => {\n      if (frame > 0 && typeof cancelAnimationFrame === \"function\") {\n        cancelAnimationFrame(frame);\n      }\n      if (timeout) window.clearTimeout(timeout);\n    };\n    if (typeof ResizeObserver !== \"function\") {\n      cleanupRef.current = cleanupScheduledRead;\n      return;\n    }\n\n    const observer = new ResizeObserver(() => {\n      lastReadWidth = null;\n      scheduleRead();\n    });\n\n    observer.observe(element);\n    cleanupRef.current = () => {\n      cleanupScheduledRead();\n      observer.disconnect();\n    };\n  }, []);\n\n  useMountEffect(() => () => cleanupRef.current?.());\n\n  return [ref, width];\n}\n",
      "type": "registry:hook",
      "target": "@hooks/use-element-width.ts"
    },
    {
      "path": "components/viewers/lib/parse-types.ts",
      "content": "// Parse result shape consumed by the parse viewer.\n\nexport interface ParseResponse {\n  document?: { id?: string; mime_type?: string } | null;\n  output?: { pages: string[]; text: string } | null;\n  usage?: { credits: number } | null;\n}\n",
      "type": "registry:lib",
      "target": "@components/viewers/lib/parse-types.ts"
    },
    {
      "path": "components/viewers/sample-data/parse.json",
      "content": "{\n  \"output\": {\n    \"pages\": [\n      \"# Commerce Bank — Bank Statement\\n\\n| Field | Value |\\n| --- | --- |\\n| Account holder | Jane Customer |\\n| Account type | Connections Checking |\\n| Account number | 000009752 |\\n| Statement date | June 5, 2003 |\\n| Statement period | May 3, 2003 through June 5, 2003 |\\n\\n## Account Summary\\n\\n| Description | Amount |\\n| --- | ---: |\\n| Beginning Balance on May 3, 2003 | $7,126.11 |\\n| Deposits & Other Credits | +$3,615.08 |\\n| ATM Withdrawals & Debits | -$20.00 |\\n| VISA Check Card Purchases & Debits | -$0.00 |\\n| Withdrawals & Other Debits | -$0.00 |\\n| Checks Paid | -$200.00 |\\n| Ending Balance on June 5, 2003 | $10,521.19 |\\n\\n## Deposits & Other Credits\\n\\n| Description | Ref Nbr | Date Credited | Amount |\\n| --- | --- | --- | ---: |\\n| Deposit | 130012345 | 05-15 | $3,615.08 |\\n| Total Deposits & Other Credits | | | $3,615.08 |\\n\\n## ATM Withdrawals & Debits\\n\\n| Description | Tran Date | Date Paid | Amount |\\n| --- | --- | --- | ---: |\\n| ATM Withdrawal — 1000 Walnut St M119, Kansas City MO 00005678 | 05-18 | 05-19 | $20.00 |\\n| Total ATM Withdrawals & Debits | | | $20.00 |\\n\\n## Checks Paid\\n\\n| Date Paid | Check Number | Amount | Reference Number |\\n| --- | --- | ---: | --- |\\n| 05-12 | 1001 | $75.00 | 00012576589 |\\n| 05-18 | 1002 | $30.00 | 00036547854 |\\n| 05-24 | 1003 | $200.00 | 00094613547 |\\n| Total Checks Paid | | $305.00 | |\\n\"\n    ],\n    \"text\": \"# Commerce Bank — Bank Statement\\n\\n| Field | Value |\\n| --- | --- |\\n| Account holder | Jane Customer |\\n| Account type | Connections Checking |\\n| Account number | 000009752 |\\n| Statement date | June 5, 2003 |\\n| Statement period | May 3, 2003 through June 5, 2003 |\\n\\n## Account Summary\\n\\n| Description | Amount |\\n| --- | ---: |\\n| Beginning Balance on May 3, 2003 | $7,126.11 |\\n| Deposits & Other Credits | +$3,615.08 |\\n| ATM Withdrawals & Debits | -$20.00 |\\n| VISA Check Card Purchases & Debits | -$0.00 |\\n| Withdrawals & Other Debits | -$0.00 |\\n| Checks Paid | -$200.00 |\\n| Ending Balance on June 5, 2003 | $10,521.19 |\\n\\n## Deposits & Other Credits\\n\\n| Description | Ref Nbr | Date Credited | Amount |\\n| --- | --- | --- | ---: |\\n| Deposit | 130012345 | 05-15 | $3,615.08 |\\n| Total Deposits & Other Credits | | | $3,615.08 |\\n\\n## ATM Withdrawals & Debits\\n\\n| Description | Tran Date | Date Paid | Amount |\\n| --- | --- | --- | ---: |\\n| ATM Withdrawal — 1000 Walnut St M119, Kansas City MO 00005678 | 05-18 | 05-19 | $20.00 |\\n| Total ATM Withdrawals & Debits | | | $20.00 |\\n\\n## Checks Paid\\n\\n| Date Paid | Check Number | Amount | Reference Number |\\n| --- | --- | ---: | --- |\\n| 05-12 | 1001 | $75.00 | 00012576589 |\\n| 05-18 | 1002 | $30.00 | 00036547854 |\\n| 05-24 | 1003 | $200.00 | 00094613547 |\\n| Total Checks Paid | | $305.00 | |\\n\"\n  },\n  \"usage\": {\n    \"credits\": 1\n  }\n}\n",
      "type": "registry:file",
      "target": "@components/viewers/sample-data/parse.json"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-projection.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Fragment, jsx, jsxs } from \"react/jsx-runtime\";\nimport { toJsxRuntime } from \"hast-util-to-jsx-runtime\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { markdownComponents } from \"@/components/viewers/page-markdown/page-markdown-components\";\nimport { createPageMarkdownProjectionTree } from \"@/components/viewers/page-markdown/page-markdown-projection-parser\";\nimport {\n  type PageMarkdownProjectionNode,\n  type PageMarkdownProjectionWorkerRequest,\n  type PageMarkdownProjectionWorkerResponse,\n} from \"@/components/viewers/page-markdown/page-markdown-projection-protocol\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nconst PAGE_MARKDOWN_PROJECTION_CACHE_LIMIT = 160;\n\ntype PageMarkdownProjectionStatus = \"idle\" | \"pending\" | \"ready\" | \"failed\";\n\ntype PageMarkdownProjectionCacheEntry = {\n  listeners: Set<() => void>;\n  markdown: string;\n  promise: Promise<void> | null;\n  projection: React.ReactNode | null;\n  status: PageMarkdownProjectionStatus;\n};\n\nconst pageMarkdownProjectionCache = new Map<\n  string,\n  PageMarkdownProjectionCacheEntry\n>();\n\nlet pageMarkdownProjectionWorker: Worker | null = null;\nlet pageMarkdownProjectionWorkerRequestId = 0;\nconst pendingPageMarkdownProjectionWorkerRequests = new Map<\n  number,\n  {\n    reject: (error: Error) => void;\n    resolve: (projection: PageMarkdownProjectionNode) => void;\n  }\n>();\n\nexport function projectPageMarkdown(markdown: string): React.ReactNode {\n  const cached = getPageMarkdownProjectionCacheEntry(markdown);\n  if (cached?.entry.status === \"ready\") {\n    touchPageMarkdownProjectionCacheEntry(cached.cacheKey, cached.entry);\n    return cached.entry.projection;\n  }\n\n  const projection = createPageMarkdownProjection(markdown);\n  storePageMarkdownProjection(markdown, projection);\n  return projection;\n}\n\nexport function readCachedPageMarkdownProjection(\n  markdown: string,\n): React.ReactNode | null {\n  const cached = getPageMarkdownProjectionCacheEntry(markdown);\n  if (cached?.entry.status === \"ready\") {\n    touchPageMarkdownProjectionCacheEntry(cached.cacheKey, cached.entry);\n    return cached.entry.projection;\n  }\n\n  if (!isPlainPageMarkdown(markdown)) return null;\n\n  const projection = createPageMarkdownProjection(markdown);\n  storePageMarkdownProjection(markdown, projection);\n  return projection;\n}\n\nexport function preloadPageMarkdownProjection(markdown: string): Promise<void> {\n  if (isPlainPageMarkdown(markdown)) {\n    readCachedPageMarkdownProjection(markdown);\n    return Promise.resolve();\n  }\n\n  const { cacheKey, entry } =\n    getOrCreatePageMarkdownProjectionCacheEntry(markdown);\n  if (entry.status === \"ready\") return Promise.resolve();\n  if (entry.status === \"pending\" && entry.promise) return entry.promise;\n\n  entry.status = \"pending\";\n  const promise = createPageMarkdownProjectionTreeAsync(markdown)\n    .then((projectionTree) => {\n      if (pageMarkdownProjectionCache.get(cacheKey) !== entry) return;\n      entry.status = \"ready\";\n      entry.projection = createPageMarkdownProjectionFromTree(projectionTree);\n      entry.promise = null;\n      touchPageMarkdownProjectionCacheEntry(cacheKey, entry);\n      notifyPageMarkdownProjectionListeners(entry);\n    })\n    .catch(() => {\n      if (pageMarkdownProjectionCache.get(cacheKey) !== entry) return;\n      entry.status = \"failed\";\n      entry.promise = null;\n      notifyPageMarkdownProjectionListeners(entry);\n    });\n  entry.promise = promise;\n  trimPageMarkdownProjectionCache();\n  return promise;\n}\n\nexport function subscribePageMarkdownProjection(\n  markdown: string,\n  listener: () => void,\n): () => void {\n  const { entry } = getOrCreatePageMarkdownProjectionCacheEntry(markdown);\n  entry.listeners.add(listener);\n  return () => {\n    entry.listeners.delete(listener);\n    trimPageMarkdownProjectionCache();\n  };\n}\n\nexport function usePageMarkdownProjection(markdown: string) {\n  const subscribe = React.useCallback(\n    (listener: () => void) =>\n      subscribePageMarkdownProjection(markdown, listener),\n    [markdown],\n  );\n  const getSnapshot = React.useCallback(\n    () => readCachedPageMarkdownProjection(markdown),\n    [markdown],\n  );\n  const projection = React.useSyncExternalStore(\n    subscribe,\n    getSnapshot,\n    getSnapshot,\n  );\n\n  useKeyedMountEffect(\n    joinEffectKey([\"page-markdown-projection\", markdown]),\n    () => {\n      void preloadPageMarkdownProjection(markdown);\n    },\n  );\n\n  return projection;\n}\n\nexport function clearPageMarkdownProjectionCacheForTests() {\n  pageMarkdownProjectionCache.clear();\n  resetPageMarkdownProjectionWorker(\n    new Error(\"Page Markdown projection cache cleared.\"),\n  );\n}\n\nexport function isPlainPageMarkdown(markdown: string): boolean {\n  if (!markdown.trim()) return true;\n  if (\n    /[`[\\]\\\\~]|!\\[|&[#a-zA-Z]|https?:\\/\\/|www\\.|mailto:|\\S+@\\S+\\.\\S+/i.test(\n      markdown,\n    )\n  ) {\n    return false;\n  }\n  if (/[*_<>|]/.test(markdown)) return false;\n  if (/ {2,}\\n/.test(markdown)) return false;\n\n  return markdown.split(/\\r\\n|[\\n\\r\\u2028\\u2029]/).every((line) => {\n    if (/^(?: {4}|\\t)/.test(line)) return false;\n    return !/^\\s{0,3}(?:#{1,6}\\s|[-+*]\\s|\\d+[.)]\\s|>\\s|`{3,}|~{3,}|-{3,}\\s*$|={3,}\\s*$)/.test(\n      line,\n    );\n  });\n}\n\nfunction createPageMarkdownProjection(markdown: string): React.ReactNode {\n  if (isPlainPageMarkdown(markdown)) return projectPlainPageMarkdown(markdown);\n\n  return createPageMarkdownProjectionFromTree(\n    createPageMarkdownProjectionTree(markdown),\n  );\n}\n\nfunction createPageMarkdownProjectionFromTree(\n  tree: PageMarkdownProjectionNode,\n): React.ReactNode {\n  return toJsxRuntime(tree as never, {\n    Fragment,\n    components: markdownComponents as never,\n    ignoreInvalidStyle: true,\n    jsx,\n    jsxs,\n    passKeys: true,\n    passNode: true,\n  });\n}\n\nfunction projectPlainPageMarkdown(markdown: string): React.ReactNode {\n  const paragraphs = markdown\n    .split(/\\n[ \\t]*\\n/)\n    .map((paragraph) => paragraph.trim())\n    .filter(Boolean);\n\n  if (!paragraphs.length) return null;\n\n  return paragraphs.map((paragraph, index) => (\n    <p key={index} className=\"my-2 leading-relaxed\">\n      {paragraph}\n    </p>\n  ));\n}\n\nfunction storePageMarkdownProjection(\n  markdown: string,\n  projection: React.ReactNode,\n) {\n  const { cacheKey, entry } =\n    getOrCreatePageMarkdownProjectionCacheEntry(markdown);\n  entry.promise = null;\n  entry.projection = projection;\n  entry.status = \"ready\";\n  touchPageMarkdownProjectionCacheEntry(cacheKey, entry);\n}\n\nfunction getOrCreatePageMarkdownProjectionCacheEntry(markdown: string) {\n  const cacheKey = pageMarkdownProjectionCacheKey(markdown);\n  const cached = pageMarkdownProjectionCache.get(cacheKey);\n  if (cached?.markdown === markdown) {\n    touchPageMarkdownProjectionCacheEntry(cacheKey, cached);\n    return { cacheKey, entry: cached };\n  }\n\n  const entry: PageMarkdownProjectionCacheEntry = {\n    listeners: new Set(),\n    markdown,\n    promise: null,\n    projection: null,\n    status: \"idle\",\n  };\n  pageMarkdownProjectionCache.set(cacheKey, entry);\n  trimPageMarkdownProjectionCache();\n  return { cacheKey, entry };\n}\n\nfunction getPageMarkdownProjectionCacheEntry(markdown: string) {\n  const cacheKey = pageMarkdownProjectionCacheKey(markdown);\n  const entry = pageMarkdownProjectionCache.get(cacheKey);\n  if (entry?.markdown !== markdown) return null;\n  return { cacheKey, entry };\n}\n\nfunction touchPageMarkdownProjectionCacheEntry(\n  cacheKey: string,\n  entry: PageMarkdownProjectionCacheEntry,\n) {\n  pageMarkdownProjectionCache.delete(cacheKey);\n  pageMarkdownProjectionCache.set(cacheKey, entry);\n  trimPageMarkdownProjectionCache();\n}\n\nfunction trimPageMarkdownProjectionCache() {\n  if (\n    pageMarkdownProjectionCache.size <= PAGE_MARKDOWN_PROJECTION_CACHE_LIMIT\n  ) {\n    return;\n  }\n\n  for (const [cacheKey, entry] of pageMarkdownProjectionCache) {\n    if (\n      pageMarkdownProjectionCache.size <= PAGE_MARKDOWN_PROJECTION_CACHE_LIMIT\n    ) {\n      return;\n    }\n    if (entry.listeners.size > 0) continue;\n    pageMarkdownProjectionCache.delete(cacheKey);\n  }\n}\n\nfunction notifyPageMarkdownProjectionListeners(\n  entry: PageMarkdownProjectionCacheEntry,\n) {\n  for (const listener of Array.from(entry.listeners)) listener();\n}\n\nfunction createPageMarkdownProjectionTreeAsync(\n  markdown: string,\n): Promise<PageMarkdownProjectionNode> {\n  if (canUsePageMarkdownProjectionWorker()) {\n    return createPageMarkdownProjectionTreeInWorker(markdown).catch(() =>\n      createPageMarkdownProjectionTreeInIdle(markdown),\n    );\n  }\n  return createPageMarkdownProjectionTreeInIdle(markdown);\n}\n\nfunction createPageMarkdownProjectionTreeInWorker(\n  markdown: string,\n): Promise<PageMarkdownProjectionNode> {\n  const id = ++pageMarkdownProjectionWorkerRequestId;\n\n  return new Promise((resolve, reject) => {\n    pendingPageMarkdownProjectionWorkerRequests.set(id, { reject, resolve });\n    try {\n      getPageMarkdownProjectionWorker().postMessage({\n        id,\n        markdown,\n        type: \"project\",\n      } satisfies PageMarkdownProjectionWorkerRequest);\n    } catch (error) {\n      pendingPageMarkdownProjectionWorkerRequests.delete(id);\n      reject(toPageMarkdownProjectionError(error));\n    }\n  });\n}\n\nfunction createPageMarkdownProjectionTreeInIdle(\n  markdown: string,\n): Promise<PageMarkdownProjectionNode> {\n  return new Promise((resolve, reject) => {\n    const project = () => {\n      try {\n        resolve(createPageMarkdownProjectionTree(markdown));\n      } catch (error) {\n        reject(toPageMarkdownProjectionError(error));\n      }\n    };\n\n    if (\n      typeof window !== \"undefined\" &&\n      typeof window.requestIdleCallback === \"function\"\n    ) {\n      window.requestIdleCallback(project, { timeout: 500 });\n      return;\n    }\n\n    globalThis.setTimeout(project, 0);\n  });\n}\n\nfunction canUsePageMarkdownProjectionWorker() {\n  return typeof window !== \"undefined\" && typeof Worker === \"function\";\n}\n\nfunction getPageMarkdownProjectionWorker() {\n  if (pageMarkdownProjectionWorker) return pageMarkdownProjectionWorker;\n\n  const worker = new Worker(\n    new URL(\"./page-markdown-projection.worker\", import.meta.url),\n  );\n  pageMarkdownProjectionWorker = worker;\n  worker.onmessage = (\n    event: MessageEvent<PageMarkdownProjectionWorkerResponse>,\n  ) => {\n    const response = event.data;\n    const pending = pendingPageMarkdownProjectionWorkerRequests.get(\n      response.id,\n    );\n    if (!pending) return;\n    pendingPageMarkdownProjectionWorkerRequests.delete(response.id);\n    if (response.ok) {\n      pending.resolve(response.projection);\n      return;\n    }\n    pending.reject(new Error(response.error));\n  };\n  worker.onerror = (event) => {\n    resetPageMarkdownProjectionWorker(\n      event.error instanceof Error\n        ? event.error\n        : new Error(event.message || \"Page Markdown projection worker failed.\"),\n    );\n  };\n  return worker;\n}\n\nfunction resetPageMarkdownProjectionWorker(error: Error) {\n  pageMarkdownProjectionWorker?.terminate();\n  pageMarkdownProjectionWorker = null;\n  pageMarkdownProjectionWorkerRequestId = 0;\n  for (const pending of pendingPageMarkdownProjectionWorkerRequests.values()) {\n    pending.reject(error);\n  }\n  pendingPageMarkdownProjectionWorkerRequests.clear();\n}\n\nfunction toPageMarkdownProjectionError(error: unknown): Error {\n  if (error instanceof Error) return error;\n  return new Error(String(error || \"Page Markdown projection failed.\"));\n}\n\nfunction pageMarkdownProjectionCacheKey(markdown: string): string {\n  return `${markdown.length}:${hashPageMarkdown(markdown)}`;\n}\n\nfunction hashPageMarkdown(markdown: string): string {\n  let hash = 0;\n  for (let index = 0; index < markdown.length; index += 1) {\n    hash = Math.imul(hash ^ markdown.charCodeAt(index), 16777619);\n  }\n  return (hash >>> 0).toString(36);\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-projection.tsx"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-projection-parser.ts",
      "content": "import remarkGfm from \"remark-gfm\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport { unified } from \"unified\";\n\nimport { type PageMarkdownProjectionNode } from \"@/components/viewers/page-markdown/page-markdown-projection-protocol\";\n\nlet pageMarkdownProcessor: ReturnType<\n  typeof createPageMarkdownProcessor\n> | null = null;\n\nexport function createPageMarkdownProjectionTree(\n  markdown: string,\n): PageMarkdownProjectionNode {\n  const processor = getPageMarkdownProcessor();\n  const tree = processor.runSync(\n    processor.parse(markdown),\n  ) as PageMarkdownProjectionNode;\n  escapeRawHtml(tree);\n  return tree;\n}\n\nfunction getPageMarkdownProcessor() {\n  pageMarkdownProcessor ??= createPageMarkdownProcessor();\n  return pageMarkdownProcessor;\n}\n\nfunction createPageMarkdownProcessor() {\n  return unified()\n    .use(remarkParse)\n    .use(remarkGfm)\n    .use(remarkRehype, { allowDangerousHtml: true });\n}\n\nfunction escapeRawHtml(node: PageMarkdownProjectionNode) {\n  const children = node.children;\n  if (!children) return;\n\n  for (let index = 0; index < children.length; index += 1) {\n    const child = children[index]!;\n    if (child.type === \"raw\") {\n      children[index] = {\n        type: \"text\",\n        value: typeof child.value === \"string\" ? child.value : \"\",\n      };\n      continue;\n    }\n    escapeRawHtml(child);\n  }\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-projection-parser.ts"
    },
    {
      "path": "components/viewers/page-markdown/page-markdown-projection-protocol.ts",
      "content": "export type PageMarkdownProjectionNode = {\n  children?: PageMarkdownProjectionNode[];\n  properties?: Record<string, unknown>;\n  type: string;\n  value?: unknown;\n};\n\nexport type PageMarkdownProjectionWorkerRequest = {\n  id: number;\n  markdown: string;\n  type: \"project\";\n};\n\nexport type PageMarkdownProjectionWorkerResponse =\n  | {\n      id: number;\n      ok: true;\n      projection: PageMarkdownProjectionNode;\n      type: \"projected\";\n    }\n  | {\n      error: string;\n      id: number;\n      ok: false;\n      type: \"projected\";\n    };\n",
      "type": "registry:component",
      "target": "@components/viewers/page-markdown/page-markdown-projection-protocol.ts"
    }
  ],
  "categories": [
    "primitives"
  ],
  "type": "registry:block"
}