{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "docx-viewer",
  "title": "DOCX Viewer",
  "description": "A DOCX viewer with upload, zoom, theme controls, and read-only rendering.",
  "dependencies": [
    "@extend-ai/react-docx@^0.9.1",
    "@tanstack/react-virtual@^3.13.12",
    "@base-ui/react@^1.4.1"
  ],
  "registryDependencies": [
    "@extend/document-viewer-sidebar",
    "@extend/file-thumbnail",
    "utils",
    "button",
    "dropdown-menu",
    "input",
    "scroll-area",
    "select",
    "separator",
    "tooltip",
    "@extend/docx-editor",
    "badge",
    "card"
  ],
  "files": [
    {
      "path": "components/extend/docx-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\nimport {\n  DocxEditorViewer,\n  useDocxComments,\n  useDocxEditor,\n  useDocxPageLayout,\n  useDocxTrackChanges,\n  useDocxViewerThumbnails,\n  type DocxDocumentTheme,\n  type DocxEditorController,\n  type DocxPageThumbnailItem,\n} from \"@extend-ai/react-docx\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  ScrollArea as InlineScrollArea,\n  ScrollBar,\n} from \"@/components/ui/scroll-area\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport {\n  DocumentViewerThumbnailSidebar,\n  useElementWidth,\n  useInlineThumbnailSidebar,\n} from \"@/components/extend/document-viewer-sidebar\"\nimport {\n  createDocxCommentCardRenderer,\n  createDocxTrackedChangeCardRenderer,\n} from \"@/components/extend/docx-annotation-card\"\nimport { FileThumbnail } from \"@/components/extend/file-thumbnail\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\nconst DOCX_MIME_TYPE =\n  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\nconst DOCX_LOADING_INDICATOR_DELAY_MS = 300\nconst DOCX_THUMBNAIL_WIDTH = 92\nconst DOCX_THUMBNAIL_LIST_PADDING = 16\nconst DOCX_THUMBNAIL_ROW_ESTIMATE = 172\nconst DEFAULT_ZOOM = 50\nconst ZOOM_OPTIONS = [10, 25, 50, 75, 100, 125, 150, 175, 200, 400] as const\nconst DOCX_PADDING_WARNING_TEXT = \"a style property during rerender\"\nconst DOCX_THUMBNAIL_FOCUS_RING_CLASS =\n  \"group-focus-visible/docx-thumbnail-sidebar:ring-2 group-focus-visible/docx-thumbnail-sidebar:ring-ring group-focus-visible/docx-thumbnail-sidebar:ring-offset-1 group-focus-visible/docx-thumbnail-sidebar:ring-offset-background\"\nconst DOCX_THUMBNAIL_PREFETCH_ROWS = 4\ntype UploadedDocxFile = {\n  file: File\n  identity: string\n  sourceUrl: string | undefined\n}\ntype DocxActivePageStore = {\n  getSnapshot: () => number\n  setActivePage: React.Dispatch<React.SetStateAction<number>>\n  subscribe: (listener: () => void) => () => void\n}\ntype DocxThumbnailRenderWindowState = {\n  visiblePageIndexes: number[]\n  prefetchPageIndexes: number[]\n}\nfunction createDocxActivePageStore(): DocxActivePageStore {\n  let activePage = 1\n  const listeners = new Set<() => void>()\n  return {\n    getSnapshot: () => activePage,\n    setActivePage: (nextPage) => {\n      const value =\n        typeof nextPage === \"function\" ? nextPage(activePage) : nextPage\n      const normalizedValue = Math.max(1, Math.round(value || 1))\n      if (normalizedValue === activePage) return\n      activePage = normalizedValue\n      listeners.forEach((listener) => listener())\n    },\n    subscribe: (listener) => {\n      listeners.add(listener)\n      return () => listeners.delete(listener)\n    },\n  }\n}\nfunction useDocxActivePage(activePageStore: DocxActivePageStore) {\n  return React.useSyncExternalStore(\n    activePageStore.subscribe,\n    activePageStore.getSnapshot,\n    activePageStore.getSnapshot\n  )\n}\nfunction areNumberArraysEqual(left: number[], right: number[]) {\n  return (\n    left.length === right.length &&\n    left.every((value, index) => value === right[index])\n  )\n}\nasync function loadDocxFile(\n  url: string,\n  displayFileName: string\n): Promise<File> {\n  const response = await fetch(url)\n  if (!response.ok) {\n    throw new Error(`Failed to fetch DOCX (${response.status})`)\n  }\n  const blob = await response.blob()\n  return new File([blob], displayFileName, {\n    type: blob.type || DOCX_MIME_TYPE,\n  })\n}\nfunction formatDocumentName(fileName: string | undefined, url: string) {\n  if (fileName?.trim()) return fileName\n  const pathname = url.split(\"?\")[0] ?? \"\"\n  const rawName = pathname.split(\"/\").pop() ?? \"document.docx\"\n  try {\n    return decodeURIComponent(rawName)\n  } catch {\n    return rawName\n  }\n}\nfunction ensureDocxExtension(fileName: string) {\n  return fileName.toLowerCase().endsWith(\".docx\")\n    ? fileName\n    : `${fileName}.docx`\n}\nfunction downloadBlob(blob: Blob, fileName: string) {\n  const url = URL.createObjectURL(blob)\n  const anchor = document.createElement(\"a\")\n  anchor.href = url\n  anchor.download = fileName\n  anchor.rel = \"noopener\"\n  document.body.append(anchor)\n  anchor.click()\n  anchor.remove()\n  window.setTimeout(() => URL.revokeObjectURL(url), 0)\n}\nasync function downloadDocxFile({\n  file,\n  fileName,\n  url,\n}: {\n  file?: File\n  fileName: string\n  url?: string\n}) {\n  if (file) {\n    downloadBlob(file, ensureDocxExtension(fileName))\n    return\n  }\n  if (!url) return\n  const response = await fetch(url)\n  if (!response.ok) {\n    throw new Error(`Failed to download DOCX (${response.status})`)\n  }\n  downloadBlob(await response.blob(), ensureDocxExtension(fileName))\n}\nfunction getNextZoomScale(currentZoomScale: number, direction: 1 | -1) {\n  const currentIndex = ZOOM_OPTIONS.indexOf(\n    currentZoomScale as (typeof ZOOM_OPTIONS)[number]\n  )\n  let fallbackIndex = -1\n  if (direction > 0) {\n    fallbackIndex = ZOOM_OPTIONS.findIndex((value) => value > currentZoomScale)\n  } else {\n    for (let index = ZOOM_OPTIONS.length - 1; index >= 0; index -= 1) {\n      if (ZOOM_OPTIONS[index] < currentZoomScale) {\n        fallbackIndex = index\n        break\n      }\n    }\n  }\n  const resolvedIndex = currentIndex >= 0 ? currentIndex : fallbackIndex\n  if (resolvedIndex < 0) return currentZoomScale\n  const nextIndex = Math.min(\n    Math.max(resolvedIndex + direction, 0),\n    ZOOM_OPTIONS.length - 1\n  )\n  return ZOOM_OPTIONS[nextIndex] ?? currentZoomScale\n}\nfunction normalizeDocxZoomScale(value: number | undefined): number {\n  return typeof value === \"number\" &&\n    ZOOM_OPTIONS.includes(value as (typeof ZOOM_OPTIONS)[number])\n    ? value\n    : DEFAULT_ZOOM\n}\nfunction useDelayedLoadingIndicator(isLoading: boolean, delayMs: number) {\n  const [showSpinner, setShowSpinner] = React.useState(false)\n  const [previousIsLoading, setPreviousIsLoading] = React.useState(isLoading)\n  if (previousIsLoading !== isLoading) {\n    setPreviousIsLoading(isLoading)\n    setShowSpinner(false)\n  }\n  React.useEffect(() => {\n    if (!isLoading) return\n    const timeoutId = window.setTimeout(() => {\n      setShowSpinner(true)\n    }, delayMs)\n    return () => window.clearTimeout(timeoutId)\n  }, [delayMs, isLoading])\n  return isLoading && showSpinner\n}\nfunction isDocxPaddingWarning(args: unknown[]) {\n  return (\n    typeof args[0] === \"string\" &&\n    args[0].includes(DOCX_PADDING_WARNING_TEXT) &&\n    args.some((arg) => String(arg).includes(\"padding\"))\n  )\n}\nfunction useSuppressDocxPaddingWarning(enabled: boolean) {\n  React.useEffect(() => {\n    if (!enabled) return\n    const originalConsoleError = console.error\n    console.error = (...args: unknown[]) => {\n      if (isDocxPaddingWarning(args)) return\n      originalConsoleError(...args)\n    }\n    return () => {\n      console.error = originalConsoleError\n    }\n  }, [enabled])\n}\nfunction isInteractiveViewerTarget(target: EventTarget | null) {\n  return (\n    target instanceof Element &&\n    Boolean(\n      target.closest(\n        'a[href], button, input, select, textarea, [contenteditable=\"true\"], [role=\"button\"], [role=\"link\"]'\n      )\n    )\n  )\n}\nfunction ToolbarTooltip({\n  label,\n  children,\n}: {\n  label: string\n  children: React.ReactNode\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={<span className=\"inline-flex\">{children}</span>}\n      ></TooltipTrigger>\n      <TooltipContent side=\"bottom\">{label}</TooltipContent>\n    </Tooltip>\n  )\n}\nfunction ViewerLoadingSurface({\n  showSpinner = true,\n}: {\n  showSpinner?: boolean\n}) {\n  return (\n    <div className=\"grid h-full min-h-52 place-items-center bg-transparent\">\n      {showSpinner ? <InlineSpinner className=\"size-4\" /> : null}\n    </div>\n  )\n}\nfunction DocxFileActionsMenu({\n  controlsDisabled,\n  downloadDisabled,\n  isPreparingDownload,\n  isDark,\n  onDownload,\n  onShowCommentsChange,\n  onShowTrackedChangesChange,\n  onIsDarkChange,\n  onUploadClick,\n  showComments,\n  showDownloadButton,\n  showNightRenderToggle,\n  showTrackedChanges,\n  showUploadButton,\n}: {\n  controlsDisabled: boolean\n  downloadDisabled: boolean\n  isPreparingDownload: boolean\n  isDark: boolean\n  onDownload: () => void\n  onShowCommentsChange: (checked: boolean) => void\n  onShowTrackedChangesChange: (checked: boolean) => void\n  onIsDarkChange: (checked: boolean) => void\n  onUploadClick: () => void\n  showComments: boolean\n  showDownloadButton: boolean\n  showNightRenderToggle: boolean\n  showTrackedChanges: boolean\n  showUploadButton: boolean\n}) {\n  const showFileActions = showDownloadButton || showUploadButton\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label=\"Open DOCX actions\"\n          >\n            <IconPlaceholder\n              lucide=\"Ellipsis\"\n              tabler=\"IconDots\"\n              hugeicons=\"MoreHorizontalIcon\"\n              phosphor=\"DotsThreeIcon\"\n              remixicon=\"RiMoreLine\"\n              className=\"size-4\"\n            />\n          </Button>\n        }\n      ></DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-52\">\n        {showNightRenderToggle ? (\n          <>\n            <DropdownMenuCheckboxItem\n              checked={isDark}\n              disabled={controlsDisabled}\n              onCheckedChange={(checked) => onIsDarkChange(checked === true)}\n              className={cn(\"justify-between\")}\n            >\n              <span className=\"flex min-w-0 items-center gap-2\">\n                <IconPlaceholder\n                  lucide=\"Moon\"\n                  tabler=\"IconMoon\"\n                  hugeicons=\"Moon02Icon\"\n                  phosphor=\"MoonIcon\"\n                  remixicon=\"RiMoonLine\"\n                  className=\"size-4\"\n                />\n                Dark mode\n              </span>\n            </DropdownMenuCheckboxItem>\n            <DropdownMenuSeparator />\n          </>\n        ) : null}\n        <DropdownMenuCheckboxItem\n          checked={showComments}\n          disabled={controlsDisabled}\n          onCheckedChange={(checked) => onShowCommentsChange(checked === true)}\n          className={cn(\"justify-between\")}\n        >\n          <span className=\"flex min-w-0 items-center gap-2\">\n            <IconPlaceholder\n              lucide=\"MessageSquare\"\n              tabler=\"IconMessage\"\n              hugeicons=\"Comment01Icon\"\n              phosphor=\"ChatIcon\"\n              remixicon=\"RiChat1Line\"\n              className=\"size-4\"\n            />\n            Comments\n          </span>\n        </DropdownMenuCheckboxItem>\n        <DropdownMenuCheckboxItem\n          checked={showTrackedChanges}\n          disabled={controlsDisabled}\n          onCheckedChange={(checked) =>\n            onShowTrackedChangesChange(checked === true)\n          }\n          className={cn(\"justify-between\")}\n        >\n          <span className=\"flex min-w-0 items-center gap-2\">\n            <IconPlaceholder\n              lucide=\"FileDiff\"\n              tabler=\"IconFileDiff\"\n              hugeicons=\"FileDiffIcon\"\n              phosphor=\"GitDiffIcon\"\n              remixicon=\"RiFileHistoryLine\"\n              className=\"size-4\"\n            />\n            Edits\n          </span>\n        </DropdownMenuCheckboxItem>\n        {showFileActions ? <DropdownMenuSeparator /> : null}\n        {showDownloadButton ? (\n          <DropdownMenuItem disabled={downloadDisabled} onClick={onDownload}>\n            {isPreparingDownload ? (\n              <InlineSpinner className=\"size-4\" />\n            ) : (\n              <IconPlaceholder\n                lucide=\"Download\"\n                tabler=\"IconDownload\"\n                hugeicons=\"Download01Icon\"\n                phosphor=\"DownloadSimpleIcon\"\n                remixicon=\"RiDownload2Line\"\n                className=\"size-4\"\n              />\n            )}\n            Download\n          </DropdownMenuItem>\n        ) : null}\n        {showUploadButton ? (\n          <DropdownMenuItem onClick={onUploadClick}>\n            <IconPlaceholder\n              lucide=\"Upload\"\n              tabler=\"IconUpload\"\n              hugeicons=\"Upload01Icon\"\n              phosphor=\"UploadSimpleIcon\"\n              remixicon=\"RiUpload2Line\"\n              className=\"size-4\"\n            />\n            Upload\n          </DropdownMenuItem>\n        ) : null}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\nfunction DocxPageNumberControl({\n  activePageStore,\n  controlsDisabled,\n  onPageChange,\n  pageCount,\n}: {\n  activePageStore: DocxActivePageStore\n  controlsDisabled: boolean\n  onPageChange: (pageNumber: number) => void\n  pageCount: number\n}) {\n  const activePage = useDocxActivePage(activePageStore)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const displayPage = pageCount ? activePage : 1\n  const [isEditing, setIsEditing] = React.useState(false)\n  const [draftPage, setDraftPage] = React.useState(() => String(displayPage))\n  React.useEffect(() => {\n    if (!isEditing) return\n    inputRef.current?.focus()\n    inputRef.current?.select()\n  }, [isEditing])\n  const applyPageDraft = React.useCallback(\n    (value: string) => {\n      const trimmedValue = value.trim()\n      if (!trimmedValue) return\n      const parsedPage = Number(trimmedValue)\n      if (!Number.isInteger(parsedPage)) return\n      onPageChange(Math.min(Math.max(parsedPage, 1), Math.max(pageCount, 1)))\n    },\n    [onPageChange, pageCount]\n  )\n  return (\n    <div className=\"flex items-center text-sm whitespace-nowrap text-primary\">\n      <span>Page</span>\n      {isEditing ? (\n        <Input\n          ref={inputRef}\n          aria-label=\"Page number\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          value={draftPage}\n          onBlur={() => setIsEditing(false)}\n          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {\n            const nextValue = event.target.value\n            setDraftPage(nextValue)\n            applyPageDraft(nextValue)\n          }}\n          onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {\n            if (event.key === \"Enter\" || event.key === \"Escape\") {\n              event.currentTarget.blur()\n            }\n          }}\n          className={cn(\n            \"h-8 px-2.5\",\n            \"mx-1 w-14 min-w-14 rounded-md [&_[data-slot=input]]:text-center\"\n          )}\n        />\n      ) : (\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"font-normal\"\n          aria-label={`Current page ${displayPage}. Edit page number`}\n          disabled={controlsDisabled || !pageCount}\n          onClick={() => {\n            setDraftPage(String(displayPage))\n            setIsEditing(true)\n          }}\n        >\n          {displayPage}\n        </Button>\n      )}\n      <span>of {pageCount || \"-\"}</span>\n    </div>\n  )\n}\nfunction DocxToolbar({\n  activePageStore,\n  controlsDisabled,\n  isDark,\n  isPreparingDownload,\n  onDownload,\n  onIsDarkChange,\n  onPageChange,\n  onShowCommentsChange,\n  onShowTrackedChangesChange,\n  onToggleSidebar,\n  onUploadClick,\n  pageCount,\n  setZoomScale,\n  showComments,\n  showDownloadButton = true,\n  showNightRenderToggle,\n  showTrackedChanges,\n  showUploadButton = true,\n  toolbarActions,\n  zoomScale,\n}: {\n  activePageStore: DocxActivePageStore\n  controlsDisabled: boolean\n  isDark: boolean\n  isPreparingDownload: boolean\n  onDownload: () => void\n  onIsDarkChange: (checked: boolean) => void\n  onPageChange: (pageNumber: number) => void\n  onShowCommentsChange: (checked: boolean) => void\n  onShowTrackedChangesChange: (checked: boolean) => void\n  onToggleSidebar: () => void\n  onUploadClick: () => void\n  pageCount: number\n  setZoomScale: React.Dispatch<React.SetStateAction<number>>\n  showComments: boolean\n  showDownloadButton?: boolean\n  showNightRenderToggle: boolean\n  showTrackedChanges: boolean\n  showUploadButton?: boolean\n  toolbarActions?: React.ReactNode\n  zoomScale: number\n}) {\n  const canZoomIn = zoomScale < ZOOM_OPTIONS[ZOOM_OPTIONS.length - 1]\n  const canZoomOut = zoomScale > ZOOM_OPTIONS[0]\n  return (\n    <div className=\"flex min-h-12 flex-wrap items-center justify-between gap-2 border-b bg-background px-3 py-2\">\n      <TooltipProvider>\n        <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n          <ToolbarTooltip label=\"Toggle thumbnails\">\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label=\"Toggle thumbnails\"\n              disabled={controlsDisabled}\n              onClick={onToggleSidebar}\n            >\n              <IconPlaceholder\n                lucide=\"PanelLeft\"\n                tabler=\"IconLayoutSidebar\"\n                hugeicons=\"SidebarLeftIcon\"\n                phosphor=\"SidebarIcon\"\n                remixicon=\"RiLayoutLeftLine\"\n                className=\"size-4\"\n              />\n            </Button>\n          </ToolbarTooltip>\n          <DocxPageNumberControl\n            activePageStore={activePageStore}\n            controlsDisabled={controlsDisabled}\n            onPageChange={onPageChange}\n            pageCount={pageCount}\n          />\n        </div>\n        <div className=\"ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1\">\n          <div className=\"flex flex-none items-center gap-1\">\n            <ToolbarTooltip label=\"Zoom out\">\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                disabled={controlsDisabled || !canZoomOut}\n                aria-label=\"Zoom out\"\n                onClick={() =>\n                  setZoomScale((currentZoomScale) =>\n                    getNextZoomScale(currentZoomScale, -1)\n                  )\n                }\n              >\n                <IconPlaceholder\n                  lucide=\"CircleMinus\"\n                  tabler=\"IconCircleMinus\"\n                  hugeicons=\"MinusSignCircleIcon\"\n                  phosphor=\"MinusCircleIcon\"\n                  remixicon=\"RiIndeterminateCircleLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </ToolbarTooltip>\n            <Select\n              value={zoomScale.toString()}\n              onValueChange={(value) => setZoomScale(Number(value))}\n              disabled={controlsDisabled}\n              modal={false}\n            >\n              <SelectTrigger\n                size=\"sm\"\n                className=\"w-[84px] min-w-[84px]\"\n                aria-label=\"Zoom level\"\n              >\n                <SelectValue>{Math.round(zoomScale)}%</SelectValue>\n              </SelectTrigger>\n              <SelectContent align=\"end\" alignItemWithTrigger={false}>\n                {ZOOM_OPTIONS.map((value) => (\n                  <SelectItem key={value} value={value.toString()}>\n                    {value}%\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n            <ToolbarTooltip label=\"Zoom in\">\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                disabled={controlsDisabled || !canZoomIn}\n                aria-label=\"Zoom in\"\n                onClick={() =>\n                  setZoomScale((currentZoomScale) =>\n                    getNextZoomScale(currentZoomScale, 1)\n                  )\n                }\n              >\n                <IconPlaceholder\n                  lucide=\"CirclePlusIcon\"\n                  tabler=\"IconCirclePlusFilled\"\n                  hugeicons=\"PlusSignCircleIcon\"\n                  phosphor=\"PlusCircleIcon\"\n                  remixicon=\"RiAddCircleFill\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </ToolbarTooltip>\n          </div>\n          {toolbarActions ? (\n            <>\n              <Separator\n                orientation=\"vertical\"\n                className=\"mx-1 h-4 self-center\"\n              />\n              {toolbarActions}\n            </>\n          ) : null}\n          <Separator orientation=\"vertical\" className=\"mx-1 h-4 self-center\" />\n          <DocxFileActionsMenu\n            controlsDisabled={controlsDisabled}\n            downloadDisabled={controlsDisabled || isPreparingDownload}\n            isPreparingDownload={isPreparingDownload}\n            isDark={isDark}\n            onDownload={onDownload}\n            onIsDarkChange={onIsDarkChange}\n            onShowCommentsChange={onShowCommentsChange}\n            onShowTrackedChangesChange={onShowTrackedChangesChange}\n            onUploadClick={onUploadClick}\n            showComments={showComments}\n            showDownloadButton={showDownloadButton}\n            showNightRenderToggle={showNightRenderToggle}\n            showTrackedChanges={showTrackedChanges}\n            showUploadButton={showUploadButton}\n          />\n        </div>\n      </TooltipProvider>\n    </div>\n  )\n}\nfunction DocxSidebarThumbnail({\n  canvasRef,\n  displayFileName,\n  hasError,\n  isActive,\n  isLoading,\n  pageNumber,\n  pixelHeightPx,\n  pixelWidthPx,\n  previewAspectRatio,\n}: {\n  canvasRef: React.RefCallback<HTMLCanvasElement>\n  displayFileName: string\n  hasError: boolean\n  isActive: boolean\n  isLoading: boolean\n  pageNumber: number\n  pixelHeightPx: number\n  pixelWidthPx: number\n  previewAspectRatio: number\n}) {\n  return (\n    <FileThumbnail\n      file={{\n        name: `${displayFileName} page ${pageNumber}`,\n        type: DOCX_MIME_TYPE,\n      }}\n      previewAspectRatio={previewAspectRatio}\n      previewClassName=\"rounded-md bg-white\"\n      previewContent={\n        <canvas\n          ref={canvasRef}\n          width={pixelWidthPx}\n          height={pixelHeightPx}\n          className=\"!size-full bg-white object-cover object-top\"\n        />\n      }\n      isLoading={isLoading}\n      hasError={hasError}\n      className={cn(\n        \"w-[92px] rounded-md border-0 shadow-xs ring-0 transition-shadow duration-150\",\n        isActive && \"shadow-sm\"\n      )}\n    />\n  )\n}\nfunction DocxThumbnailSidebarList({\n  activePage,\n  displayFileName,\n  isLoadingDocument,\n  onSelectPage,\n  onThumbnailRenderWindowChange,\n  pageCount,\n  sidebarOpen,\n  thumbnails,\n}: {\n  activePage: number\n  displayFileName: string\n  isLoadingDocument: boolean\n  onSelectPage: (pageNumber: number) => void\n  onThumbnailRenderWindowChange: (\n    renderWindow: DocxThumbnailRenderWindowState\n  ) => void\n  pageCount: number\n  sidebarOpen: boolean\n  thumbnails: DocxPageThumbnailItem[]\n}) {\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const thumbnailListboxId = React.useId()\n  const visibleThumbnails = React.useMemo(\n    () => thumbnails.slice(0, pageCount || 0),\n    [pageCount, thumbnails]\n  )\n  const activeDescendantId =\n    activePage > 0 && visibleThumbnails.length\n      ? `${thumbnailListboxId}-page-${activePage}`\n      : undefined\n  const virtualizer = useVirtualizer({\n    count: visibleThumbnails.length,\n    estimateSize: () => DOCX_THUMBNAIL_ROW_ESTIMATE,\n    getItemKey: (index) => visibleThumbnails[index]?.pageIndex ?? index,\n    getScrollElement: () => viewportRef.current,\n    overscan: 3,\n  })\n  const virtualItems = virtualizer.getVirtualItems()\n  const renderWindowSignature = virtualItems\n    .map((virtualRow) => virtualRow.index)\n    .join(\",\")\n  React.useEffect(() => {\n    if (!sidebarOpen || isLoadingDocument || !visibleThumbnails.length) {\n      onThumbnailRenderWindowChange({\n        prefetchPageIndexes: [],\n        visiblePageIndexes: [],\n      })\n      return\n    }\n    const visiblePageIndexes = virtualItems\n      .map((virtualRow) => visibleThumbnails[virtualRow.index]?.pageIndex)\n      .filter((pageIndex): pageIndex is number => pageIndex !== undefined)\n    const firstVirtualIndex = virtualItems[0]?.index ?? 0\n    const lastVirtualIndex =\n      virtualItems[virtualItems.length - 1]?.index ?? firstVirtualIndex\n    const firstPrefetchIndex = Math.max(\n      0,\n      firstVirtualIndex - DOCX_THUMBNAIL_PREFETCH_ROWS\n    )\n    const lastPrefetchIndex = Math.min(\n      visibleThumbnails.length - 1,\n      lastVirtualIndex + DOCX_THUMBNAIL_PREFETCH_ROWS\n    )\n    const visiblePageIndexSet = new Set(visiblePageIndexes)\n    const prefetchPageIndexes: number[] = []\n    for (\n      let index = firstPrefetchIndex;\n      index <= lastPrefetchIndex;\n      index += 1\n    ) {\n      const pageIndex = visibleThumbnails[index]?.pageIndex\n      if (pageIndex !== undefined && !visiblePageIndexSet.has(pageIndex)) {\n        prefetchPageIndexes.push(pageIndex)\n      }\n    }\n    onThumbnailRenderWindowChange({\n      prefetchPageIndexes,\n      visiblePageIndexes,\n    })\n  }, [\n    isLoadingDocument,\n    onThumbnailRenderWindowChange,\n    renderWindowSignature,\n    sidebarOpen,\n    visibleThumbnails,\n    virtualItems,\n  ])\n  React.useEffect(() => {\n    if (!sidebarOpen || activePage < 1 || !visibleThumbnails.length) return\n    virtualizer.scrollToIndex(\n      Math.min(activePage - 1, visibleThumbnails.length - 1),\n      { align: \"auto\" }\n    )\n  }, [activePage, sidebarOpen, virtualizer, visibleThumbnails.length])\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (pageCount < 1) return\n      const currentPage = activePage > 0 ? activePage : 1\n      let nextPage: number | null = null\n      if (event.key === \"ArrowDown\") {\n        nextPage = Math.min(pageCount, currentPage + 1)\n      } else if (event.key === \"ArrowUp\") {\n        nextPage = Math.max(1, currentPage - 1)\n      } else if (event.key === \"Home\") {\n        nextPage = 1\n      } else if (event.key === \"End\") {\n        nextPage = pageCount\n      }\n      if (nextPage === null) return\n      event.preventDefault()\n      onSelectPage(nextPage)\n    },\n    [activePage, onSelectPage, pageCount]\n  )\n  return (\n    <InlineScrollArea2\n      className=\"h-full\"\n      scrollFade\n      viewportClassName=\"group/docx-thumbnail-sidebar focus-visible:ring-0 focus-visible:ring-offset-0\"\n      viewportProps={{\n        \"aria-activedescendant\": activeDescendantId,\n        \"aria-busy\": isLoadingDocument || undefined,\n        \"aria-label\": \"DOCX pages\",\n        onKeyDown: handleKeyDown,\n        onMouseDown: (event) => {\n          event.currentTarget.focus({ preventScroll: true })\n        },\n        role: \"listbox\",\n        tabIndex: 0,\n      }}\n      viewportRef={viewportRef}\n    >\n      {isLoadingDocument ? (\n        <div className=\"p-4\">\n          <div className=\"mx-auto h-28 w-20 overflow-hidden rounded-md bg-background shadow-xs\">\n            <div className=\"h-full animate-pulse bg-muted\" />\n          </div>\n          <div className=\"mx-auto mt-3 h-3 w-10 rounded-full bg-muted\" />\n        </div>\n      ) : visibleThumbnails.length ? (\n        <div\n          className=\"relative\"\n          style={{\n            height:\n              virtualizer.getTotalSize() + DOCX_THUMBNAIL_LIST_PADDING * 2,\n          }}\n        >\n          {virtualItems.map((virtualRow) => {\n            const thumbnail = visibleThumbnails[virtualRow.index]\n            if (!thumbnail) return null\n            return (\n              <div\n                key={virtualRow.key}\n                ref={virtualizer.measureElement}\n                data-index={virtualRow.index}\n                className={cn(\n                  \"absolute top-0 right-3 left-3 pb-3 [contain:layout]\",\n                  thumbnail.pageNumber === activePage && \"z-10\"\n                )}\n                style={{\n                  transform: `translateY(${virtualRow.start + DOCX_THUMBNAIL_LIST_PADDING}px)`,\n                }}\n              >\n                <div\n                  id={`${thumbnailListboxId}-page-${thumbnail.pageNumber}`}\n                  role=\"option\"\n                  aria-current={\n                    thumbnail.pageNumber === activePage ? \"page\" : undefined\n                  }\n                  aria-label={`Page ${thumbnail.pageNumber}`}\n                  aria-posinset={thumbnail.pageNumber}\n                  aria-selected={thumbnail.pageNumber === activePage}\n                  aria-setsize={pageCount}\n                  data-docx-viewer-thumbnail-option={thumbnail.pageNumber}\n                  className={cn(\n                    \"flex h-auto w-full cursor-default flex-col items-center gap-2 rounded-md p-2 text-xs transition-shadow outline-none select-none hover:bg-sidebar-accent\",\n                    thumbnail.pageNumber === activePage &&\n                      \"bg-sidebar-accent text-foreground\",\n                    thumbnail.pageNumber !== activePage &&\n                      \"text-muted-foreground\",\n                    thumbnail.pageNumber === activePage &&\n                      DOCX_THUMBNAIL_FOCUS_RING_CLASS\n                  )}\n                  onClick={() => onSelectPage(thumbnail.pageNumber)}\n                >\n                  <DocxSidebarThumbnail\n                    canvasRef={thumbnail.canvasRef}\n                    displayFileName={displayFileName}\n                    hasError={thumbnail.status === \"error\"}\n                    isActive={thumbnail.pageNumber === activePage}\n                    isLoading={\n                      thumbnail.status !== \"ready\" &&\n                      thumbnail.status !== \"error\"\n                    }\n                    pageNumber={thumbnail.pageNumber}\n                    pixelHeightPx={thumbnail.pixelHeightPx}\n                    pixelWidthPx={thumbnail.pixelWidthPx}\n                    previewAspectRatio={thumbnail.aspectRatio}\n                  />\n                  {thumbnail.pageNumber}\n                </div>\n              </div>\n            )\n          })}\n        </div>\n      ) : null}\n    </InlineScrollArea2>\n  )\n}\nfunction DocxThumbnailSidebarContent({\n  activePageStore,\n  displayFileName,\n  editor,\n  isLoadingDocument,\n  onSelectPage,\n  pageCount,\n  reportedPageCount,\n  sidebarOpen,\n}: {\n  activePageStore: DocxActivePageStore\n  displayFileName: string\n  editor: DocxEditorController\n  isLoadingDocument: boolean\n  onSelectPage: (pageNumber: number) => void\n  pageCount: number\n  reportedPageCount: number\n  sidebarOpen: boolean\n}) {\n  const [thumbnailRenderWindow, setThumbnailRenderWindow] =\n    React.useState<DocxThumbnailRenderWindowState>({\n      prefetchPageIndexes: [],\n      visiblePageIndexes: [],\n    })\n  const thumbnailEditor = React.useMemo<DocxEditorController>(\n    () => ({\n      ...editor,\n      totalPages: Math.max(editor.totalPages, reportedPageCount),\n    }),\n    [editor, reportedPageCount]\n  )\n  const thumbnailOptions = React.useMemo(\n    () => ({\n      // Detached thumbnail rendering handles offscreen pages; keep the raster\n      // queue dormant while the sidebar is closed.\n      disabled: !sidebarOpen,\n      pixelRatio: 2,\n      renderWindow: thumbnailRenderWindow,\n      resolution: {\n        maxHeight: DOCX_THUMBNAIL_WIDTH * 1.35,\n        maxWidth: DOCX_THUMBNAIL_WIDTH,\n      },\n    }),\n    [sidebarOpen, thumbnailRenderWindow]\n  )\n  const { thumbnails } = useDocxViewerThumbnails(\n    thumbnailEditor,\n    thumbnailOptions\n  )\n  const activePage = useDocxActivePage(activePageStore)\n  const handleThumbnailRenderWindowChange = React.useCallback(\n    (nextRenderWindow: DocxThumbnailRenderWindowState) => {\n      setThumbnailRenderWindow((currentRenderWindow) => {\n        if (\n          areNumberArraysEqual(\n            currentRenderWindow.visiblePageIndexes,\n            nextRenderWindow.visiblePageIndexes\n          ) &&\n          areNumberArraysEqual(\n            currentRenderWindow.prefetchPageIndexes,\n            nextRenderWindow.prefetchPageIndexes\n          )\n        ) {\n          return currentRenderWindow\n        }\n        return nextRenderWindow\n      })\n    },\n    []\n  )\n  if (!sidebarOpen) return null\n  return (\n    <DocxThumbnailSidebarList\n      activePage={activePage}\n      displayFileName={displayFileName}\n      isLoadingDocument={isLoadingDocument}\n      onSelectPage={onSelectPage}\n      onThumbnailRenderWindowChange={handleThumbnailRenderWindowChange}\n      pageCount={pageCount}\n      sidebarOpen={sidebarOpen}\n      thumbnails={thumbnails}\n    />\n  )\n}\nexport function DocxViewerPreview({\n  className,\n  defaultZoom = DEFAULT_ZOOM,\n  fileName,\n  isDark,\n  onIsDarkChange,\n  showDownload = true,\n  showToolbar = true,\n  showUpload = true,\n  src,\n  toolbarActions,\n}: {\n  className?: string\n  defaultZoom?: number\n  fileName?: string\n  isDark: boolean\n  onIsDarkChange: (isDark: boolean) => void\n  showDownload?: boolean\n  showToolbar?: boolean\n  showUpload?: boolean\n  src?: string\n  toolbarActions?: React.ReactNode\n}) {\n  return (\n    <DocxViewerContent\n      className={className}\n      defaultZoom={defaultZoom}\n      effectiveIsDark={isDark}\n      fileName={fileName}\n      setNightRenderEnabled={onIsDarkChange}\n      shouldRenderNightMode\n      showDownload={showDownload}\n      showToolbar={showToolbar}\n      showUpload={showUpload}\n      toolbarActions={toolbarActions}\n      url={src}\n    />\n  )\n}\nfunction DocxViewerContent({\n  className,\n  defaultZoom,\n  effectiveIsDark,\n  fileName,\n  setNightRenderEnabled,\n  shouldRenderNightMode,\n  showDownload,\n  showToolbar = true,\n  showUpload,\n  toolbarActions,\n  url,\n}: {\n  className?: string\n  defaultZoom?: number\n  effectiveIsDark: boolean\n  fileName?: string\n  setNightRenderEnabled: (checked: boolean) => void\n  shouldRenderNightMode: boolean\n  showDownload: boolean\n  showToolbar?: boolean\n  showUpload: boolean\n  toolbarActions?: React.ReactNode\n  url?: string\n}) {\n  const fileInputRef = React.useRef<HTMLInputElement>(null)\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const [viewportElement, setViewportElement] =\n    React.useState<HTMLDivElement | null>(null)\n  const [viewerShellRef, viewerShellWidth] = useElementWidth<HTMLDivElement>()\n  const [uploadedDocxFile, setUploadedDocxFile] =\n    React.useState<UploadedDocxFile | null>(null)\n  const [sidebarOpen, setSidebarOpen] = React.useState(false)\n  const activePageStore = React.useMemo(() => createDocxActivePageStore(), [])\n  const resolvedDefaultZoomScale = normalizeDocxZoomScale(defaultZoom)\n  const activeUploadedDocxFile =\n    uploadedDocxFile?.sourceUrl === url ? uploadedDocxFile : null\n  const documentKey = activeUploadedDocxFile?.identity ?? url ?? \"\"\n  const setActivePage = activePageStore.setActivePage\n  const sidebarInline = useInlineThumbnailSidebar(viewerShellWidth)\n  const viewerBackgroundColor =\n    \"color-mix(in oklab, var(--muted) 40%, transparent)\"\n  const displayFileName = React.useMemo(\n    () =>\n      activeUploadedDocxFile?.file.name ??\n      (url ? formatDocumentName(fileName, url) : (fileName ?? \"document.docx\")),\n    [activeUploadedDocxFile?.file.name, fileName, url]\n  )\n  const [initialDocumentTheme] = React.useState<DocxDocumentTheme>(() =>\n    effectiveIsDark ? \"dark\" : \"light\"\n  )\n  const editorOptions = React.useMemo(\n    () => ({\n      initialDocumentTheme,\n      initialFileName: displayFileName,\n    }),\n    [displayFileName, initialDocumentTheme]\n  )\n  const editor = useDocxEditor(editorOptions)\n  const { layout: pageLayout } = useDocxPageLayout(editor)\n  const { importDocxFile, setDocumentTheme, status } = editor\n  const { showComments, setShowComments } = useDocxComments(editor)\n  const { showTrackedChanges, setShowTrackedChanges } =\n    useDocxTrackChanges(editor)\n  const [reportedPageCount, setReportedPageCount] = React.useState(0)\n  const [zoomScaleState, setZoomScaleState] = React.useState({\n    documentKey: \"\",\n    value: resolvedDefaultZoomScale,\n  })\n  const zoomScale =\n    zoomScaleState.documentKey === documentKey\n      ? zoomScaleState.value\n      : resolvedDefaultZoomScale\n  const setZoomScale = React.useCallback<\n    React.Dispatch<React.SetStateAction<number>>\n  >(\n    (nextZoomScale) => {\n      setZoomScaleState((currentState) => {\n        const currentZoomScale =\n          currentState.documentKey === documentKey\n            ? currentState.value\n            : resolvedDefaultZoomScale\n        const value =\n          typeof nextZoomScale === \"function\"\n            ? nextZoomScale(currentZoomScale)\n            : nextZoomScale\n        return { documentKey, value }\n      })\n    },\n    [documentKey, resolvedDefaultZoomScale]\n  )\n  const [loadError, setLoadError] = React.useState<string>()\n  const [isLoadingDocument, setIsLoadingDocument] = React.useState(true)\n  const [isPreparingDownload, setIsPreparingDownload] = React.useState(false)\n  const shouldShowDocumentSpinner = useDelayedLoadingIndicator(\n    isLoadingDocument,\n    DOCX_LOADING_INDICATOR_DELAY_MS\n  )\n  const loadingState = (\n    <ViewerLoadingSurface showSpinner={shouldShowDocumentSpinner} />\n  )\n  const documentTheme = effectiveIsDark ? \"dark\" : \"light\"\n  const renderTrackedChangeCard = React.useMemo(\n    () => createDocxTrackedChangeCardRenderer(documentTheme),\n    [documentTheme]\n  )\n  const renderCommentCard = React.useMemo(\n    () => createDocxCommentCardRenderer(documentTheme),\n    [documentTheme]\n  )\n  const hasDocument = Boolean(url || activeUploadedDocxFile)\n  const pageCount =\n    hasDocument && !isLoadingDocument && !loadError\n      ? Math.max(1, reportedPageCount || editor.totalPages)\n      : 0\n  const thumbnailSidebarVisible = Boolean(\n    sidebarOpen && (pageCount || isLoadingDocument)\n  )\n  const controlsDisabled =\n    !hasDocument || isLoadingDocument || Boolean(loadError)\n  const handlePageCountChange = React.useCallback((nextPageCount: number) => {\n    setReportedPageCount(Math.max(1, Math.round(nextPageCount || 1)))\n  }, [])\n  const setViewportRef = React.useCallback((element: HTMLDivElement | null) => {\n    viewportRef.current = element\n    setViewportElement(element)\n  }, [])\n  const pageVirtualization = React.useMemo(\n    () => ({\n      enabled: true,\n      overscan: 1,\n      scrollElement: viewportElement,\n      zoomScale: zoomScale / 100,\n    }),\n    [viewportElement, zoomScale]\n  )\n  const handleDownload = React.useCallback(async () => {\n    if (isPreparingDownload) return\n    if (!activeUploadedDocxFile && !url) return\n    setIsPreparingDownload(true)\n    try {\n      await downloadDocxFile({\n        file: activeUploadedDocxFile?.file,\n        fileName: displayFileName,\n        url,\n      })\n    } catch (error) {\n      console.error(error)\n    } finally {\n      setIsPreparingDownload(false)\n    }\n  }, [activeUploadedDocxFile, displayFileName, isPreparingDownload, url])\n  useSuppressDocxPaddingWarning(!isLoadingDocument && !loadError)\n  React.useEffect(() => {\n    setActivePage(1)\n    viewportRef.current?.scrollTo({ top: 0, left: 0 })\n  }, [documentKey, setActivePage])\n  React.useEffect(() => {\n    setDocumentTheme(effectiveIsDark ? \"dark\" : \"light\")\n  }, [effectiveIsDark, setDocumentTheme])\n  React.useEffect(() => {\n    if (\n      status.startsWith(\"Failed to load file\") ||\n      status === \"Only .docx files are supported\"\n    ) {\n      const frame = window.requestAnimationFrame(() => {\n        setLoadError(status)\n        setIsLoadingDocument(false)\n      })\n      return () => window.cancelAnimationFrame(frame)\n    }\n  }, [status])\n  // Imports mutate the shared editor instance; concurrent calls (effect\n  // re-runs, StrictMode double-invoke) race inside the parser and surface as\n  // bogus \"Invalid DOCX ZIP\" errors, so every import is chained through here.\n  const importQueueRef = React.useRef<Promise<void>>(Promise.resolve())\n  React.useEffect(() => {\n    let isCurrent = true\n    async function load() {\n      // Superseded while queued — let the newest import run instead.\n      if (!isCurrent) return\n      if (!activeUploadedDocxFile && !url) {\n        setIsLoadingDocument(false)\n        setLoadError(undefined)\n        setReportedPageCount(0)\n        return\n      }\n      setIsLoadingDocument(true)\n      setLoadError(undefined)\n      setReportedPageCount(0)\n      try {\n        const docxFile =\n          activeUploadedDocxFile?.file ??\n          (url ? await loadDocxFile(url, displayFileName) : null)\n        if (!docxFile) return\n        await importDocxFile(docxFile)\n        if (isCurrent) {\n          setIsLoadingDocument(false)\n          setActivePage(1)\n          viewportRef.current?.scrollTo({ top: 0, left: 0 })\n        }\n      } catch (error) {\n        if (isCurrent) {\n          setLoadError(\n            error instanceof Error ? error.message : \"Unknown DOCX load error\"\n          )\n          setIsLoadingDocument(false)\n        }\n      }\n    }\n    importQueueRef.current = importQueueRef.current.then(load)\n    return () => {\n      isCurrent = false\n    }\n  }, [\n    activeUploadedDocxFile,\n    displayFileName,\n    importDocxFile,\n    setActivePage,\n    url,\n  ])\n  const updateActivePageFromViewport = React.useCallback(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !pageCount) return\n    const viewportRect = viewport.getBoundingClientRect()\n    const viewportCenter = viewportRect.top + viewportRect.height / 2\n    let closestPage = 1\n    let closestDistance = Number.POSITIVE_INFINITY\n    viewport\n      .querySelectorAll<HTMLElement>(\n        '[data-docx-page-wrapper=\"true\"][data-index]'\n      )\n      .forEach((page) => {\n        const pageIndex = Number(page.dataset.index)\n        if (!Number.isFinite(pageIndex)) return\n        const pageRect = page.getBoundingClientRect()\n        const pageCenter = pageRect.top + pageRect.height / 2\n        const distance = Math.abs(pageCenter - viewportCenter)\n        if (distance < closestDistance) {\n          closestDistance = distance\n          closestPage = pageIndex + 1\n        }\n      })\n    activePageStore.setActivePage((currentPage) =>\n      currentPage === closestPage ? currentPage : closestPage\n    )\n  }, [activePageStore, pageCount])\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !pageCount) return\n    let frameId = 0\n    const handleScroll = () => {\n      window.cancelAnimationFrame(frameId)\n      frameId = window.requestAnimationFrame(updateActivePageFromViewport)\n    }\n    frameId = window.requestAnimationFrame(updateActivePageFromViewport)\n    viewport.addEventListener(\"scroll\", handleScroll, { passive: true })\n    return () => {\n      window.cancelAnimationFrame(frameId)\n      viewport.removeEventListener(\"scroll\", handleScroll)\n    }\n  }, [pageCount, updateActivePageFromViewport])\n  const scrollToPage = React.useCallback(\n    (pageNumber: number) => {\n      const viewport = viewportRef.current\n      const targetPageIndex = pageNumber - 1\n      const page = viewport?.querySelector<HTMLElement>(\n        `[data-docx-page-wrapper=\"true\"][data-index=\"${targetPageIndex}\"]`\n      )\n      setActivePage(pageNumber)\n      if (!viewport) return\n      if (!page) {\n        const pageStridePx =\n          (pageLayout.pageHeightPx + pageLayout.viewportDefaults.pageGapPx) *\n          (zoomScale / 100)\n        viewport.scrollTo({\n          top: Math.max(0, targetPageIndex * pageStridePx - 24),\n          behavior: \"auto\",\n        })\n        return\n      }\n      viewport.scrollTo({\n        top:\n          page.getBoundingClientRect().top -\n          viewport.getBoundingClientRect().top +\n          viewport.scrollTop -\n          24,\n        behavior: \"auto\",\n      })\n    },\n    [\n      pageLayout.pageHeightPx,\n      pageLayout.viewportDefaults.pageGapPx,\n      setActivePage,\n      zoomScale,\n    ]\n  )\n  async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {\n    const file = event.target.files?.[0]\n    event.target.value = \"\"\n    if (!file) return\n    setZoomScale(resolvedDefaultZoomScale)\n    setActivePage(1)\n    setReportedPageCount(0)\n    setUploadedDocxFile({\n      file,\n      identity: `${file.name}-${file.size}-${file.lastModified}`,\n      sourceUrl: url,\n    })\n  }\n  return (\n    <div\n      className={cn(\n        \"flex h-[640px] min-h-0 flex-col overflow-hidden bg-background\",\n        className\n      )}\n    >\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept=\".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n        className=\"hidden\"\n        onChange={handleUpload}\n      />\n      {showToolbar ? (\n        <DocxToolbar\n          activePageStore={activePageStore}\n          controlsDisabled={controlsDisabled}\n          isDark={effectiveIsDark}\n          isPreparingDownload={isPreparingDownload}\n          onDownload={handleDownload}\n          onIsDarkChange={setNightRenderEnabled}\n          onPageChange={scrollToPage}\n          onShowCommentsChange={setShowComments}\n          onShowTrackedChangesChange={setShowTrackedChanges}\n          onToggleSidebar={() => setSidebarOpen((open) => !open)}\n          onUploadClick={() => fileInputRef.current?.click()}\n          pageCount={pageCount}\n          setZoomScale={setZoomScale}\n          showComments={showComments}\n          showDownloadButton={showDownload}\n          showNightRenderToggle={shouldRenderNightMode}\n          showTrackedChanges={showTrackedChanges}\n          showUploadButton={showUpload}\n          toolbarActions={toolbarActions}\n          zoomScale={zoomScale}\n        />\n      ) : null}\n      <div\n        ref={viewerShellRef}\n        className=\"relative flex min-h-0 flex-1 overflow-hidden bg-muted/30\"\n      >\n        <DocumentViewerThumbnailSidebar\n          inline={sidebarInline}\n          open={thumbnailSidebarVisible}\n        >\n          <DocxThumbnailSidebarContent\n            activePageStore={activePageStore}\n            displayFileName={displayFileName}\n            editor={editor}\n            isLoadingDocument={isLoadingDocument}\n            onSelectPage={scrollToPage}\n            pageCount={pageCount}\n            reportedPageCount={reportedPageCount}\n            sidebarOpen={thumbnailSidebarVisible}\n          />\n        </DocumentViewerThumbnailSidebar>\n        <InlineScrollArea2\n          className=\"min-h-0 flex-1\"\n          style={{ backgroundColor: viewerBackgroundColor }}\n          viewportClassName=\"px-4 py-6\"\n          viewportProps={{\n            \"aria-label\": \"DOCX document\",\n            onMouseDown: (event) => {\n              if (isInteractiveViewerTarget(event.target)) return\n              event.currentTarget.focus({ preventScroll: true })\n            },\n            tabIndex: 0,\n          }}\n          viewportRef={setViewportRef}\n        >\n          {!url && !activeUploadedDocxFile ? (\n            <div className=\"grid h-full min-h-96 place-items-center p-6 text-center\">\n              <div className=\"max-w-md rounded-lg border bg-background p-4 text-sm shadow-xs\">\n                <div className=\"font-medium\">\n                  Upload a Word document to preview\n                </div>\n                <div className=\"mt-1 text-muted-foreground\">\n                  Pass a DOCX URL with the <code>src</code> prop or upload a\n                  file.\n                </div>\n                <div className=\"mt-1 text-muted-foreground\">\n                  Legacy <code>.doc</code> support is limited and experimental;\n                  convert to DOCX for best fidelity.\n                </div>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"mt-4\"\n                  onClick={() => fileInputRef.current?.click()}\n                >\n                  <IconPlaceholder\n                    lucide=\"Upload\"\n                    tabler=\"IconUpload\"\n                    hugeicons=\"Upload01Icon\"\n                    phosphor=\"UploadSimpleIcon\"\n                    remixicon=\"RiUpload2Line\"\n                    className=\"size-4\"\n                  />\n                  Upload Word document\n                </Button>\n              </div>\n            </div>\n          ) : loadError ? (\n            <div className=\"grid h-full min-h-96 place-items-center p-6 text-center\">\n              <div className=\"max-w-md rounded-lg border bg-background p-4 text-sm text-destructive shadow-xs\">\n                <div className=\"font-medium\">Unable to display DOCX</div>\n                <div className=\"mt-1 text-muted-foreground\">{loadError}</div>\n              </div>\n            </div>\n          ) : isLoadingDocument ? (\n            loadingState\n          ) : (\n            <div className=\"flex min-h-full w-max min-w-full justify-center\">\n              <div\n                className={cn(\n                  \"origin-top\",\n                  effectiveIsDark && \"docx-night-reader-shell\"\n                )}\n                style={{ zoom: zoomScale / 100 }}\n              >\n                <DocxEditorViewer\n                  editor={editor}\n                  mode=\"read-only\"\n                  showTrackedChanges={showTrackedChanges}\n                  renderTrackedChangeCard={renderTrackedChangeCard}\n                  showComments={showComments}\n                  renderCommentCard={renderCommentCard}\n                  loadingState={loadingState}\n                  pageBackgroundColor={effectiveIsDark ? \"#0a0a0a\" : undefined}\n                  pageGapBackgroundColor={viewerBackgroundColor}\n                  pageVirtualization={pageVirtualization}\n                  deferInitialPaginationPaint={false}\n                  onPageCountChange={handlePageCountChange}\n                />\n              </div>\n            </div>\n          )}\n        </InlineScrollArea2>\n      </div>\n    </div>\n  )\n}\nfunction InlineScrollArea2({\n  className,\n  children,\n  orientation = \"both\",\n  scrollFade = false,\n  scrollbarGutter = false,\n  scrollbarOverflowOnly = false,\n  viewportClassName,\n  viewportProps,\n  viewportRef,\n  ...props\n}: InlineScrollAreaProps) {\n  const {\n    className: viewportPropsClassName,\n    ref: viewportPropsRef,\n    ...resolvedViewportProps\n  } = viewportProps ?? {}\n  const composedViewportRef = React.useMemo(\n    () => InlineComposeRefs(viewportPropsRef, viewportRef),\n    [viewportPropsRef, viewportRef]\n  )\n\n  if (\n    !viewportProps &&\n    !viewportRef &&\n    !viewportClassName &&\n    !scrollFade &&\n    !scrollbarGutter &&\n    !scrollbarOverflowOnly\n  ) {\n    return (\n      <InlineScrollArea\n        {...props}\n        className={cn(\n          \"size-full min-h-0\",\n          orientation === \"horizontal\" &&\n            \"[&>[data-orientation=vertical]]:hidden\",\n          className\n        )}\n      >\n        {children}\n        {orientation !== \"vertical\" ? (\n          <ScrollBar orientation=\"horizontal\" />\n        ) : null}\n      </InlineScrollArea>\n    )\n  }\n\n  return (\n    <ScrollAreaPrimitive.Root\n      className={cn(\n        \"size-full min-h-0\",\n        scrollbarOverflowOnly &&\n          \"[&:not(:has([data-slot=scroll-area-viewport][data-has-overflow-x]))_[data-orientation=horizontal]]:hidden [&:not(:has([data-slot=scroll-area-viewport][data-has-overflow-y]))_[data-orientation=vertical]]:hidden\",\n        className\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        {...resolvedViewportProps}\n        ref={composedViewportRef}\n        className={cn(\n          \"h-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          scrollFade &&\n            \"mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] [--fade-size:1.5rem]\",\n          scrollbarGutter && orientation !== \"vertical\" && \"pb-3.5\",\n          scrollbarGutter && orientation !== \"horizontal\" && \"pe-3.5\",\n          viewportPropsClassName,\n          viewportClassName\n        )}\n        data-slot=\"scroll-area-viewport\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      {orientation !== \"horizontal\" ? (\n        <ScrollBar orientation=\"vertical\" />\n      ) : null}\n      {orientation !== \"vertical\" ? (\n        <ScrollBar orientation=\"horizontal\" />\n      ) : null}\n      {orientation === \"both\" ? <ScrollAreaPrimitive.Corner /> : null}\n    </ScrollAreaPrimitive.Root>\n  )\n}\ntype InlineScrollAreaProps = ScrollAreaPrimitive.Root.Props & {\n  orientation?: \"vertical\" | \"horizontal\" | \"both\"\n  scrollFade?: boolean\n  scrollbarGutter?: boolean\n  scrollbarOverflowOnly?: boolean\n  viewportClassName?: string\n  viewportProps?: ScrollAreaPrimitive.Viewport.Props\n  viewportRef?: React.Ref<HTMLDivElement>\n}\nfunction InlineComposeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (!ref) continue\n      if (typeof ref === \"function\") ref(node)\n      else ref.current = node\n    }\n  }\n}\nfunction InlineSpinner({ className, ...props }: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LoaderCircle\"\n      tabler=\"IconLoader2\"\n      hugeicons=\"Loading03Icon\"\n      phosphor=\"CircleNotchIcon\"\n      remixicon=\"RiLoader4Line\"\n      role=\"status\"\n      aria-label=\"Loading\"\n      className={cn(\"size-4 animate-spin\", className)}\n      {...props}\n    />\n  )\n}\ntype InlineRegistryIconProps = Omit<\n  React.ComponentProps<\"svg\">,\n  \"children\" | \"strokeWidth\"\n> & { strokeWidth?: number }\n",
      "type": "registry:ui",
      "target": "@components/extend/docx-viewer.tsx"
    },
    {
      "path": "components/extend/docx-annotation-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport type {\n  DocxCommentCardRenderProps,\n  DocxDocumentTheme,\n  DocxTrackedChangeCardRenderProps,\n} from \"@extend-ai/react-docx\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Card } from \"@/components/ui/card\"\n\ntype AnnotationBadgeVariant =\n  | \"outline\"\n  | \"secondary\"\n  | \"success\"\n  | \"error\"\n  | \"warning\"\n  | \"info\"\nfunction trackedChangeBadgeVariant(\n  kind: DocxTrackedChangeCardRenderProps[\"change\"][\"kind\"]\n): AnnotationBadgeVariant {\n  switch (kind) {\n    case \"insertion\":\n    case \"move-to\":\n      return \"success\"\n    case \"deletion\":\n    case \"move-from\":\n      return \"error\"\n    default:\n      return \"warning\"\n  }\n}\nfunction trackedChangeBadgeLabel({\n  change,\n  kindLabel,\n}: Pick<DocxTrackedChangeCardRenderProps, \"change\" | \"kindLabel\">) {\n  switch (change.kind) {\n    case \"insertion\":\n      return \"Inserted\"\n    case \"deletion\":\n      return \"Removed\"\n    case \"move-from\":\n      return \"Moved from\"\n    case \"move-to\":\n      return \"Moved to\"\n    default:\n      return kindLabel\n  }\n}\nfunction DocxAnnotationCard({\n  anchorText,\n  badge,\n  badgeVariant = \"outline\",\n  date,\n  meta,\n  documentTheme,\n  snippet,\n  style,\n}: {\n  anchorText?: string\n  badge: string\n  badgeVariant?: AnnotationBadgeVariant\n  date?: string\n  documentTheme: DocxDocumentTheme\n  meta: string\n  snippet: string\n  style: React.CSSProperties\n}) {\n  const isDarkDocument = documentTheme === \"dark\"\n  const cardStyle: React.CSSProperties = {\n    ...style,\n    backgroundColor: isDarkDocument\n      ? \"rgb(24 24 27 / 0.95)\"\n      : \"rgb(255 255 255 / 0.95)\",\n    color: isDarkDocument ? \"#f4f4f5\" : \"#18181b\",\n  }\n  const mutedTextColor = isDarkDocument ? \"#a1a1aa\" : \"#71717a\"\n  const anchorStyle: React.CSSProperties = {\n    backgroundColor: isDarkDocument\n      ? \"rgb(63 63 70 / 0.55)\"\n      : \"rgb(244 244 245 / 0.75)\",\n    color: mutedTextColor,\n  }\n  return (\n    <Card\n      style={cardStyle}\n      className=\"pointer-events-auto box-border gap-2 rounded-lg p-2 shadow-sm before:rounded-[7px]\"\n    >\n      <div className=\"flex min-w-0 items-start justify-between gap-2\">\n        <div\n          className=\"min-w-0 text-[11px] leading-tight font-medium\"\n          style={{ color: mutedTextColor }}\n        >\n          <div className=\"truncate\">{meta}</div>\n          {date ? <div className=\"mt-0.5 truncate\">{date}</div> : null}\n        </div>\n        <Badge\n          variant={badgeVariant === \"outline\" ? \"outline\" : \"secondary\"}\n          className={cn(\n            \"h-4 px-1 text-[10px]\",\n            cn(\n              \"max-w-[92px] truncate\",\n              badgeVariant === \"success\" &&\n                \"bg-green-500/10 text-green-700 dark:text-green-300\",\n              badgeVariant === \"error\" && \"bg-destructive/10 text-destructive\",\n              badgeVariant === \"warning\" &&\n                \"bg-amber-500/10 text-amber-700 dark:text-amber-300\",\n              badgeVariant === \"info\" &&\n                \"bg-blue-500/10 text-blue-700 dark:text-blue-300\"\n            )\n          )}\n        >\n          {badge}\n        </Badge>\n      </div>\n      {anchorText ? (\n        <div\n          className=\"rounded-md px-2 py-1 text-[11px] leading-snug italic\"\n          style={anchorStyle}\n        >\n          {anchorText}\n        </div>\n      ) : null}\n      <div className=\"text-xs leading-snug break-words\">{snippet}</div>\n    </Card>\n  )\n}\nexport function createDocxTrackedChangeCardRenderer(\n  documentTheme: DocxDocumentTheme\n) {\n  return function renderDocxTrackedChangeCard({\n    change,\n    formattedDate,\n    kindLabel,\n    snippet,\n    style,\n  }: DocxTrackedChangeCardRenderProps) {\n    return (\n      <DocxAnnotationCard\n        badge={trackedChangeBadgeLabel({ change, kindLabel })}\n        badgeVariant={trackedChangeBadgeVariant(change.kind)}\n        date={formattedDate}\n        documentTheme={documentTheme}\n        meta={change.author?.trim() || \"Unknown author\"}\n        snippet={snippet}\n        style={style}\n      />\n    )\n  }\n}\nexport function createDocxCommentCardRenderer(\n  documentTheme: DocxDocumentTheme\n) {\n  return function renderDocxCommentCard({\n    comment,\n    formattedDate,\n    snippet,\n    style,\n  }: DocxCommentCardRenderProps) {\n    const badge = comment.resolved\n      ? \"Resolved\"\n      : comment.parentId !== undefined\n        ? \"Reply\"\n        : \"Comment\"\n    return (\n      <DocxAnnotationCard\n        anchorText={comment.anchorText}\n        badge={badge}\n        badgeVariant={comment.resolved ? \"secondary\" : \"info\"}\n        date={formattedDate}\n        documentTheme={documentTheme}\n        meta={comment.author?.trim() || \"Unknown author\"}\n        snippet={snippet}\n        style={style}\n      />\n    )\n  }\n}\n",
      "type": "registry:ui",
      "target": "@components/extend/docx-annotation-card.tsx"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui"
}
