{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "xlsx-viewer",
  "title": "Excel Viewer",
  "description": "An XLSX viewer with workbook loading, thumbnails, sheet tabs, upload, zoom, and theme controls.",
  "dependencies": [
    "@extend-ai/react-xlsx@0.16.1",
    "@base-ui/react@^1.4.1"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "dropdown-menu",
    "input",
    "popover",
    "scroll-area",
    "select",
    "separator",
    "tabs",
    "tooltip"
  ],
  "files": [
    {
      "path": "components/extend/xlsx-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\nimport {\n  useXlsxViewer,\n  useXlsxViewerController,\n  useXlsxViewerThumbnails,\n  useXlsxViewerZoom,\n  XlsxViewer,\n  XlsxViewerProvider,\n  type XlsxCellAddress,\n  type XlsxScrollerRenderProps,\n  type XlsxSheetData,\n  type XlsxTableHeaderMenuRenderProps,\n  type XlsxViewerController,\n} from \"@extend-ai/react-xlsx\"\nimport { createPortal } from \"react-dom\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\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 { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\nconst XLSX_LOADING_INDICATOR_DELAY_MS = 300\nconst XLSX_DROPDOWN_Z_INDEX_CLASS = \"z-40\"\nconst XLSX_SEARCH_BATCH_ROW_COUNT = 500\nconst XLSX_SEARCH_DEBOUNCE_MS = 300\nconst XLSX_GRID_HEADER_HEIGHT = 24\nconst XLSX_GRID_ROW_HEADER_WIDTH = 40\nconst XLSX_MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024\nconst ZOOM_OPTIONS = [10, 25, 50, 75, 100, 125, 150, 175, 200, 400] as const\n// Stable reference so the thumbnails memo isn't invalidated on every render\n// (e.g. by selection changes), which would recompute every sheet thumbnail.\nconst XLSX_SHEET_TAB_THUMBNAIL_OPTIONS = {\n  resolution: {\n    maxHeight: 360,\n    maxWidth: 560,\n  },\n} as const\ntype UploadedWorkbook = {\n  buffer: ArrayBuffer\n  fileName: string\n  identity: string\n}\ntype XlsxSearchResult = {\n  cell: XlsxCellAddress\n  displayValue: string\n  formula?: string\n  sheetIndex: number\n  sheetName: string\n  workbookSheetIndex: number\n}\ntype XlsxBatchCell = {\n  col?: unknown\n  formula?: unknown\n  value?: unknown\n}\ntype XlsxBatchRow = {\n  cells?: unknown\n  index?: unknown\n}\nfunction formatWorkbookName(fileName: string | undefined, url: string) {\n  if (fileName?.trim()) return fileName\n  const pathname = url.split(\"?\")[0] ?? \"\"\n  const rawName = pathname.split(\"/\").pop() ?? \"workbook.xlsx\"\n  try {\n    return decodeURIComponent(rawName)\n  } catch {\n    return rawName\n  }\n}\nfunction ensureWorkbookExtension(fileName: string) {\n  const lowerFileName = fileName.toLowerCase()\n  return lowerFileName.endsWith(\".xlsx\") || lowerFileName.endsWith(\".xls\")\n    ? fileName\n    : `${fileName}.xlsx`\n}\nfunction downloadWorkbookBuffer(buffer: ArrayBuffer, fileName: string) {\n  const resolvedFileName = ensureWorkbookExtension(fileName)\n  const blob = new Blob([buffer], {\n    type: resolvedFileName.toLowerCase().endsWith(\".xls\")\n      ? \"application/vnd.ms-excel\"\n      : \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  })\n  const url = URL.createObjectURL(blob)\n  const anchor = document.createElement(\"a\")\n  anchor.href = url\n  anchor.download = resolvedFileName\n  anchor.rel = \"noopener\"\n  document.body.append(anchor)\n  anchor.click()\n  anchor.remove()\n  window.setTimeout(() => URL.revokeObjectURL(url), 0)\n}\nfunction normalizeSearchText(value: unknown) {\n  return typeof value === \"string\"\n    ? value\n    : value === null || value === undefined\n      ? \"\"\n      : String(value)\n}\nfunction cellValueToSearchText(value: unknown) {\n  if (!value || typeof value !== \"object\") return normalizeSearchText(value)\n  const record = value as {\n    asBoolean?: () => boolean | null\n    asError?: () => string | null\n    asNumber?: () => number | null\n    asText?: () => string | null\n    is_boolean?: boolean\n    is_empty?: boolean\n    is_error?: boolean\n    is_number?: boolean\n    is_text?: boolean\n  }\n  if (record.is_empty) return \"\"\n  if (record.is_error) return record.asError?.() ?? \"\"\n  if (record.is_text) return record.asText?.() ?? \"\"\n  if (record.is_number) return normalizeSearchText(record.asNumber?.())\n  if (record.is_boolean) return record.asBoolean?.() ? \"TRUE\" : \"FALSE\"\n  return normalizeSearchText(value)\n}\nfunction getCellSearchText(\n  controller: XlsxViewerController,\n  sheet: XlsxSheetData,\n  row: number,\n  col: number\n) {\n  const worksheet = controller.workbook?.getSheet(sheet.workbookSheetIndex)\n  if (!worksheet) return { displayValue: \"\", formula: \"\" }\n  const formula = worksheet.getFormulaAt(row, col) ?? \"\"\n  const cachedFormulaValue = formula\n    ? sheet.cachedFormulaValues[cellAddressToA1({ row, col })]\n    : undefined\n  const formatted = worksheet.getFormattedValueAt(row, col)\n  if (\n    formatted &&\n    !(formula && cachedFormulaValue !== undefined && formatted.startsWith(\"#\"))\n  ) {\n    return { displayValue: formatted, formula }\n  }\n  const calculated = worksheet.getCalculatedValueAt(row, col)\n  const displayValue =\n    formula && cachedFormulaValue !== undefined && calculated.is_error\n      ? cachedFormulaValue\n      : cellValueToSearchText(calculated)\n  return { displayValue, formula }\n}\nfunction getBatchRows(rows: unknown): XlsxBatchRow[] {\n  return Array.isArray(rows) ? (rows as XlsxBatchRow[]) : []\n}\nfunction getBatchCells(row: XlsxBatchRow): XlsxBatchCell[] {\n  return Array.isArray(row.cells) ? (row.cells as XlsxBatchCell[]) : []\n}\nfunction cellMatchesQuery(\n  displayValue: string,\n  formula: string,\n  query: string\n) {\n  return (\n    displayValue.toLowerCase().includes(query) ||\n    formula.toLowerCase().includes(query)\n  )\n}\nfunction cellAddressToA1({ col, row }: XlsxCellAddress) {\n  let columnNumber = col + 1\n  let columnName = \"\"\n  while (columnNumber > 0) {\n    const remainder = (columnNumber - 1) % 26\n    columnName = String.fromCharCode(65 + remainder) + columnName\n    columnNumber = Math.floor((columnNumber - 1) / 26)\n  }\n  return `${columnName}${row + 1}`\n}\nasync function findXlsxSearchResults(\n  controller: XlsxViewerController,\n  rawQuery: string\n) {\n  const query = rawQuery.trim().toLowerCase()\n  if (!query) return []\n  const results: XlsxSearchResult[] = []\n  for (const [sheetIndex, sheet] of controller.sheets.entries()) {\n    const startRow = Math.max(0, sheet.minUsedRow)\n    const endRow = Math.max(startRow, sheet.maxUsedRow)\n    const visibleCols = sheet.visibleCols.filter(\n      (col) => col >= sheet.minUsedCol && col <= sheet.maxUsedCol\n    )\n    const visibleRowSet = new Set(sheet.visibleRows)\n    const visibleColSet = new Set(visibleCols)\n    if (!visibleCols.length || sheet.maxUsedRow < sheet.minUsedRow) continue\n    const worksheet = controller.workbook?.getSheet(sheet.workbookSheetIndex)\n    const worksheetWithBatch = worksheet as\n      | {\n          getRowsBatch?: (\n            startRow: number,\n            rowCount: number,\n            options?: Record<string, unknown>\n          ) => unknown\n        }\n      | undefined\n    for (\n      let batchStartRow = startRow;\n      batchStartRow <= endRow;\n      batchStartRow += XLSX_SEARCH_BATCH_ROW_COUNT\n    ) {\n      const rowCount = Math.min(\n        XLSX_SEARCH_BATCH_ROW_COUNT,\n        endRow - batchStartRow + 1\n      )\n      const rows = controller.getRowsBatchAsync\n        ? await controller.getRowsBatchAsync(\n            sheet.workbookSheetIndex,\n            batchStartRow,\n            rowCount\n          )\n        : worksheetWithBatch?.getRowsBatch?.(batchStartRow, rowCount, {\n            includeFormulas: true,\n            useFormattedValues: true,\n          })\n      if (rows) {\n        for (const rowEntry of getBatchRows(rows)) {\n          const row = Number(rowEntry.index)\n          if (!Number.isInteger(row) || !visibleRowSet.has(row)) {\n            continue\n          }\n          for (const cellEntry of getBatchCells(rowEntry)) {\n            const col = Number(cellEntry.col)\n            if (!Number.isInteger(col) || !visibleColSet.has(col)) continue\n            const displayValue = normalizeSearchText(cellEntry.value)\n            const formula = normalizeSearchText(cellEntry.formula)\n            if (!cellMatchesQuery(displayValue, formula, query)) continue\n            results.push({\n              cell: { row, col },\n              displayValue,\n              formula,\n              sheetIndex,\n              sheetName: sheet.name,\n              workbookSheetIndex: sheet.workbookSheetIndex,\n            })\n          }\n        }\n        continue\n      }\n      if (!worksheet) continue\n      const batchEndRow = batchStartRow + rowCount - 1\n      for (const row of sheet.visibleRows) {\n        if (row < batchStartRow || row > batchEndRow) continue\n        for (const col of visibleCols) {\n          const { displayValue, formula } = getCellSearchText(\n            controller,\n            sheet,\n            row,\n            col\n          )\n          if (!cellMatchesQuery(displayValue, formula, query)) continue\n          results.push({\n            cell: { row, col },\n            displayValue,\n            formula,\n            sheetIndex,\n            sheetName: sheet.name,\n            workbookSheetIndex: sheet.workbookSheetIndex,\n          })\n        }\n      }\n    }\n  }\n  return results\n}\nfunction sumAxisBefore(values: number[], endIndex: number, zoomFactor: number) {\n  let total = 0\n  for (let index = 0; index < endIndex; index += 1) {\n    total += (values[index] ?? 0) * zoomFactor\n  }\n  return total\n}\nfunction scrollXlsxCellIntoView({\n  controller,\n  result,\n  viewport,\n}: {\n  controller: XlsxViewerController\n  result: XlsxSearchResult\n  viewport: HTMLDivElement | null\n}) {\n  if (!viewport) return\n  const sheet = controller.sheets[result.sheetIndex]\n  if (!sheet) return\n  const rowIndex = sheet.visibleRows.indexOf(result.cell.row)\n  const colIndex = sheet.visibleCols.indexOf(result.cell.col)\n  if (rowIndex < 0 || colIndex < 0) return\n  const zoomFactor = Math.max(0.1, controller.zoomScale / 100)\n  const headerHeight = XLSX_GRID_HEADER_HEIGHT * zoomFactor\n  const rowHeaderWidth = XLSX_GRID_ROW_HEADER_WIDTH * zoomFactor\n  const rowStart =\n    headerHeight + sumAxisBefore(sheet.rowHeights, rowIndex, zoomFactor)\n  const colStart =\n    rowHeaderWidth + sumAxisBefore(sheet.colWidths, colIndex, zoomFactor)\n  const rowHeight =\n    (sheet.rowHeights[rowIndex] ?? sheet.defaultRowHeightPx) * zoomFactor\n  const colWidth =\n    (sheet.colWidths[colIndex] ?? sheet.defaultColWidthPx) * zoomFactor\n  const rowEnd = rowStart + rowHeight\n  const colEnd = colStart + colWidth\n  let nextTop = viewport.scrollTop\n  let nextLeft = viewport.scrollLeft\n  const visibleTop = viewport.scrollTop + headerHeight\n  const visibleLeft = viewport.scrollLeft + rowHeaderWidth\n  const visibleBottom = viewport.scrollTop + viewport.clientHeight\n  const visibleRight = viewport.scrollLeft + viewport.clientWidth\n  if (rowStart < visibleTop) {\n    nextTop = rowStart - headerHeight\n  } else if (rowEnd > visibleBottom) {\n    nextTop = rowEnd - viewport.clientHeight\n  }\n  if (colStart < visibleLeft) {\n    nextLeft = colStart - rowHeaderWidth\n  } else if (colEnd > visibleRight) {\n    nextLeft = colEnd - viewport.clientWidth\n  }\n  viewport.scrollTo({\n    left: Math.max(0, nextLeft),\n    top: Math.max(0, nextTop),\n    behavior: \"auto\",\n  })\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 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 w-full min-w-full place-items-center bg-transparent\">\n      {showSpinner ? <InlineSpinner className=\"size-4\" /> : null}\n    </div>\n  )\n}\nfunction WorkbookFileActionsMenu({\n  isDark,\n  onDownload,\n  onIsDarkChange,\n  onUploadClick,\n  showDownloadButton,\n  showNightRenderToggle = false,\n  showUploadButton,\n}: {\n  isDark?: boolean\n  onDownload?: () => void\n  onIsDarkChange?: (checked: boolean) => void\n  onUploadClick: () => void\n  showDownloadButton: boolean\n  showNightRenderToggle?: boolean\n  showUploadButton: boolean\n}) {\n  const showThemeControl = showNightRenderToggle && Boolean(onIsDarkChange)\n  const showFileActions = (showDownloadButton && onDownload) || showUploadButton\n  if (!showThemeControl && !showFileActions) return null\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label=\"Open workbook 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\n        align=\"end\"\n        className={cn(\"w-52\", XLSX_DROPDOWN_Z_INDEX_CLASS)}\n      >\n        {showThemeControl ? (\n          <>\n            <DropdownMenuCheckboxItem\n              checked={Boolean(isDark)}\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            {showFileActions ? <DropdownMenuSeparator /> : null}\n          </>\n        ) : null}\n        {showDownloadButton && onDownload ? (\n          <DropdownMenuItem onClick={onDownload}>\n            <IconPlaceholder\n              lucide=\"Download\"\n              tabler=\"IconDownload\"\n              hugeicons=\"Download01Icon\"\n              phosphor=\"DownloadSimpleIcon\"\n              remixicon=\"RiDownload2Line\"\n              className=\"size-4\"\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}\nexport function renderXlsxScroller({\n  children,\n  viewportProps,\n}: XlsxScrollerRenderProps) {\n  return (\n    <InlineScrollArea2\n      className=\"h-full min-h-0 w-full min-w-0 flex-1\"\n      viewportProps={viewportProps}\n    >\n      {children}\n    </InlineScrollArea2>\n  )\n}\nexport function WorkbookTableHeaderMenu({\n  direction,\n  sortAscending,\n  sortDescending,\n  triggerIcon,\n  triggerProps,\n}: XlsxTableHeaderMenuRenderProps) {\n  const [open, setOpen] = React.useState(false)\n  return (\n    <DropdownMenu open={open} onOpenChange={setOpen}>\n      <DropdownMenuTrigger\n        render={\n          <Button\n            {...triggerProps}\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            className={cn(\"size-6 rounded-sm\", triggerProps.className)}\n            aria-label=\"Column menu\"\n          >\n            {triggerIcon ? (\n              triggerIcon\n            ) : (\n              <IconPlaceholder\n                lucide=\"Ellipsis\"\n                tabler=\"IconDots\"\n                hugeicons=\"MoreHorizontalIcon\"\n                phosphor=\"DotsThreeIcon\"\n                remixicon=\"RiMoreLine\"\n                className=\"size-3.5\"\n              />\n            )}\n          </Button>\n        }\n      ></DropdownMenuTrigger>\n      <DropdownMenuContent\n        align=\"end\"\n        className={cn(\"w-40\", XLSX_DROPDOWN_Z_INDEX_CLASS)}\n      >\n        <DropdownMenuRadioGroup\n          value={direction ?? \"\"}\n          onValueChange={(value) => {\n            if (value === \"ascending\") {\n              sortAscending()\n            } else {\n              sortDescending()\n            }\n            setOpen(false)\n          }}\n        >\n          <DropdownMenuRadioItem value=\"ascending\">\n            Sort ascending\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"descending\">\n            Sort descending\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\nfunction WorkbookSearchPopover({\n  viewportRef,\n}: {\n  viewportRef: React.RefObject<HTMLDivElement | null>\n}) {\n  const controller = useXlsxViewer()\n  const [searchDraft, setSearchDraft] = React.useState(\"\")\n  const [searchQuery, setSearchQuery] = React.useState(\"\")\n  const [searchResults, setSearchResults] = React.useState<XlsxSearchResult[]>(\n    []\n  )\n  const [activeResultIndex, setActiveResultIndex] = React.useState(0)\n  const [isSearching, setIsSearching] = React.useState(false)\n  const controllerRef = React.useRef(controller)\n  const searchRequestIdRef = React.useRef(0)\n  const appliedResultKeyRef = React.useRef(\"\")\n  const activeResult = searchResults[activeResultIndex] ?? null\n  const activeResultKey = activeResult\n    ? `${activeResult.workbookSheetIndex}:${activeResult.cell.row}:${activeResult.cell.col}`\n    : \"\"\n  const controlsDisabled =\n    controller.isLoading ||\n    Boolean(controller.error) ||\n    !controller.sheets.length\n  const hasActiveQuery = Boolean(searchQuery.trim())\n  const resultLabel = isSearching\n    ? \"Searching\"\n    : !hasActiveQuery\n      ? \"No search\"\n      : searchResults.length\n        ? `${activeResultIndex + 1} / ${searchResults.length}`\n        : \"No results\"\n  React.useEffect(() => {\n    controllerRef.current = controller\n  }, [controller])\n  const runSearch = React.useCallback((rawQuery: string) => {\n    const nextQuery = rawQuery.trim()\n    const requestId = searchRequestIdRef.current + 1\n    searchRequestIdRef.current = requestId\n    appliedResultKeyRef.current = \"\"\n    setSearchQuery(nextQuery)\n    setActiveResultIndex(0)\n    if (!nextQuery) {\n      setSearchResults([])\n      setIsSearching(false)\n      return\n    }\n    setIsSearching(true)\n    void findXlsxSearchResults(controllerRef.current, nextQuery)\n      .then((nextResults) => {\n        if (searchRequestIdRef.current !== requestId) return\n        setSearchResults(nextResults)\n      })\n      .catch(() => {\n        if (searchRequestIdRef.current !== requestId) return\n        setSearchResults([])\n      })\n      .finally(() => {\n        if (searchRequestIdRef.current !== requestId) return\n        setIsSearching(false)\n      })\n  }, [])\n  React.useEffect(() => {\n    if (!searchDraft.trim()) return\n    const timeoutId = window.setTimeout(() => {\n      runSearch(searchDraft)\n    }, XLSX_SEARCH_DEBOUNCE_MS)\n    return () => window.clearTimeout(timeoutId)\n  }, [runSearch, searchDraft])\n  const handleSearchDraftChange = (nextDraft: string) => {\n    searchRequestIdRef.current += 1\n    setSearchDraft(nextDraft)\n    if (!nextDraft.trim()) {\n      runSearch(\"\")\n      return\n    }\n    setIsSearching(true)\n  }\n  const clearSearch = React.useCallback(() => {\n    searchRequestIdRef.current += 1\n    setSearchDraft(\"\")\n    setSearchQuery(\"\")\n    setSearchResults([])\n    setActiveResultIndex(0)\n    setIsSearching(false)\n    appliedResultKeyRef.current = \"\"\n    controller.clearSelection()\n  }, [controller])\n  const goToRelativeResult = React.useCallback(\n    (direction: 1 | -1) => {\n      if (!searchResults.length) return\n      setActiveResultIndex((currentIndex) => {\n        return (\n          (currentIndex + direction + searchResults.length) %\n          searchResults.length\n        )\n      })\n    },\n    [searchResults.length]\n  )\n  React.useEffect(() => {\n    return () => {\n      searchRequestIdRef.current += 1\n    }\n  }, [])\n  React.useEffect(() => {\n    if (!activeResult) return\n    if (controller.activeSheetIndex !== activeResult.sheetIndex) {\n      appliedResultKeyRef.current = \"\"\n      controller.setActiveSheetIndex(activeResult.sheetIndex)\n      return\n    }\n    if (appliedResultKeyRef.current === activeResultKey) return\n    appliedResultKeyRef.current = activeResultKey\n    controller.selectCell(activeResult.cell)\n    const frame = window.requestAnimationFrame(() => {\n      scrollXlsxCellIntoView({\n        controller,\n        result: activeResult,\n        viewport: viewportRef.current,\n      })\n    })\n    return () => window.cancelAnimationFrame(frame)\n  }, [\n    activeResult,\n    activeResultKey,\n    controller,\n    controller.activeSheetIndex,\n    viewportRef,\n  ])\n  return (\n    <Popover>\n      <ToolbarTooltip label=\"Search workbook\">\n        <PopoverTrigger\n          render={\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label=\"Search workbook\"\n              disabled={controlsDisabled}\n            >\n              <IconPlaceholder\n                lucide=\"Search\"\n                tabler=\"IconSearch\"\n                hugeicons=\"Search01Icon\"\n                phosphor=\"MagnifyingGlassIcon\"\n                remixicon=\"RiSearchLine\"\n                className=\"size-4\"\n              />\n            </Button>\n          }\n        ></PopoverTrigger>\n      </ToolbarTooltip>\n      <PopoverContent align=\"end\" className=\"w-72\">\n        <div className=\"space-y-3\">\n          <Input\n            placeholder=\"Search workbook\"\n            value={searchDraft}\n            onChange={(event) => handleSearchDraftChange(event.target.value)}\n            onKeyDown={(event) => {\n              if (event.key !== \"Enter\") return\n              event.preventDefault()\n              if (event.shiftKey && searchResults.length) {\n                goToRelativeResult(-1)\n              } else if (searchResults.length) {\n                goToRelativeResult(1)\n              } else if (searchDraft.trim()) {\n                runSearch(searchDraft)\n              }\n            }}\n          />\n          <div className=\"flex items-center justify-between gap-2\">\n            <div className=\"min-w-0 text-xs text-muted-foreground\">\n              <div className=\"truncate\">\n                {searchResults.length ? (\n                  <>\n                    <span className=\"text-primary\">\n                      {activeResultIndex + 1}\n                    </span>\n                    {` / ${searchResults.length}`}\n                  </>\n                ) : (\n                  resultLabel\n                )}\n              </div>\n              {activeResult ? (\n                <div className=\"mt-0.5 truncate\">\n                  {activeResult.sheetName}!{cellAddressToA1(activeResult.cell)}\n                </div>\n              ) : null}\n            </div>\n            <div className=\"flex shrink-0 items-center gap-1\">\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"icon-sm\"\n                aria-label=\"Previous result\"\n                disabled={isSearching || searchResults.length === 0}\n                onClick={() => goToRelativeResult(-1)}\n              >\n                <IconPlaceholder\n                  lucide=\"ChevronLeft\"\n                  tabler=\"IconChevronLeft\"\n                  hugeicons=\"ArrowLeft01Icon\"\n                  phosphor=\"CaretLeftIcon\"\n                  remixicon=\"RiArrowLeftSLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"icon-sm\"\n                aria-label=\"Next result\"\n                disabled={isSearching || searchResults.length === 0}\n                onClick={() => goToRelativeResult(1)}\n              >\n                <IconPlaceholder\n                  lucide=\"ArrowRight\"\n                  tabler=\"IconArrowRight\"\n                  hugeicons=\"ArrowRight01Icon\"\n                  phosphor=\"ArrowRightIcon\"\n                  remixicon=\"RiArrowRightLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </div>\n          </div>\n          <div className=\"flex justify-end\">\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={clearSearch}\n            >\n              Clear\n            </Button>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\nfunction WorkbookToolbar({\n  isDark,\n  onDownload,\n  onIsDarkChange,\n  onUploadClick,\n  showDownloadButton = true,\n  showNightRenderToggle,\n  showUploadButton = true,\n  toolbarActions,\n  viewportRef,\n  workbookIdentity,\n}: {\n  isDark: boolean\n  onDownload?: () => void\n  onIsDarkChange: (checked: boolean) => void\n  onUploadClick: () => void\n  showDownloadButton?: boolean\n  showNightRenderToggle: boolean\n  showUploadButton?: boolean\n  toolbarActions?: React.ReactNode\n  viewportRef: React.RefObject<HTMLDivElement | null>\n  workbookIdentity: string\n}) {\n  const { canZoomIn, canZoomOut, setZoomScale, zoomIn, zoomOut, zoomScale } =\n    useXlsxViewerZoom()\n  const currentZoom = Math.round(zoomScale)\n  React.useEffect(() => {\n    setZoomScale(100)\n  }, [setZoomScale, workbookIdentity])\n  return (\n    <div className=\"flex min-h-12 flex-wrap items-center justify-end gap-2 border-b bg-background px-3 py-2\">\n      <TooltipProvider>\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={!canZoomOut}\n                aria-label=\"Zoom out\"\n                onClick={zoomOut}\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={currentZoom.toString()}\n              onValueChange={(value) => setZoomScale(Number(value))}\n              modal={false}\n            >\n              <SelectTrigger\n                size=\"sm\"\n                className=\"w-[84px] min-w-[84px]\"\n                aria-label=\"Zoom level\"\n              >\n                <SelectValue>{currentZoom}%</SelectValue>\n              </SelectTrigger>\n              <SelectContent\n                align=\"end\"\n                alignItemWithTrigger={false}\n                className={XLSX_DROPDOWN_Z_INDEX_CLASS}\n              >\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={!canZoomIn}\n                aria-label=\"Zoom in\"\n                onClick={zoomIn}\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          <Separator orientation=\"vertical\" className=\"mx-1 h-4 self-center\" />\n          <WorkbookSearchPopover\n            key={workbookIdentity}\n            viewportRef={viewportRef}\n          />\n          {toolbarActions ? (\n            <>\n              <Separator\n                orientation=\"vertical\"\n                className=\"mx-1 h-4 self-center\"\n              />\n              {toolbarActions}\n            </>\n          ) : null}\n          {(showDownloadButton && onDownload) ||\n          showUploadButton ||\n          showNightRenderToggle ? (\n            <>\n              <Separator\n                orientation=\"vertical\"\n                className=\"mx-1 h-4 self-center\"\n              />\n              <WorkbookFileActionsMenu\n                isDark={isDark}\n                onDownload={onDownload}\n                onIsDarkChange={onIsDarkChange}\n                onUploadClick={onUploadClick}\n                showDownloadButton={showDownloadButton}\n                showNightRenderToggle={showNightRenderToggle}\n                showUploadButton={showUploadButton}\n              />\n            </>\n          ) : null}\n        </div>\n      </TooltipProvider>\n    </div>\n  )\n}\nfunction WorkbookStandaloneToolbar({\n  onUploadClick,\n  showUploadButton = true,\n  toolbarActions,\n}: {\n  onUploadClick: () => void\n  showUploadButton?: boolean\n  toolbarActions?: React.ReactNode\n}) {\n  return (\n    <div className=\"flex min-h-12 flex-wrap items-center justify-end gap-2 border-b bg-background px-3 py-2\">\n      <TooltipProvider>\n        <div className=\"ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1\">\n          {toolbarActions ? <>{toolbarActions}</> : null}\n          {showUploadButton ? (\n            <>\n              {toolbarActions ? (\n                <Separator\n                  orientation=\"vertical\"\n                  className=\"mx-1 h-4 self-center\"\n                />\n              ) : null}\n              <WorkbookFileActionsMenu\n                onUploadClick={onUploadClick}\n                showDownloadButton={false}\n                showUploadButton={showUploadButton}\n              />\n            </>\n          ) : null}\n        </div>\n      </TooltipProvider>\n    </div>\n  )\n}\ntype WorkbookSheetTab = {\n  name: string\n  workbookSheetIndex: number\n}\ntype WorkbookSheetTabsInnerProps = {\n  activeSheetIndex: number\n  onActiveSheetIndexChange: (index: number) => void\n  sheets: WorkbookSheetTab[]\n}\nexport function WorkbookSheetTabs({\n  workbookIdentity,\n}: {\n  workbookIdentity: string\n}) {\n  const { activeSheetIndex, setActiveSheetIndex, sheets } = useXlsxViewer()\n  const handleActiveSheetIndexChange = React.useCallback(\n    (index: number) => setActiveSheetIndex(index),\n    [setActiveSheetIndex]\n  )\n  return (\n    <WorkbookSheetTabsInner\n      key={workbookIdentity}\n      activeSheetIndex={activeSheetIndex}\n      onActiveSheetIndexChange={handleActiveSheetIndexChange}\n      sheets={sheets}\n    />\n  )\n}\nconst WorkbookSheetTabsInner = React.memo(function WorkbookSheetTabsInner({\n  activeSheetIndex,\n  onActiveSheetIndexChange,\n  sheets,\n}: WorkbookSheetTabsInnerProps) {\n  const [visiblePreviewIndex, setVisiblePreviewIndex] = React.useState<\n    number | null\n  >(null)\n  const [previewPosition, setPreviewPosition] = React.useState({\n    left: 0,\n    top: 0,\n  })\n  const { thumbnails } = useXlsxViewerThumbnails(\n    XLSX_SHEET_TAB_THUMBNAIL_OPTIONS\n  )\n  const [thumbnailUrls, setThumbnailUrls] = React.useState<\n    Record<number, string>\n  >({})\n  const scrollRef = React.useRef<HTMLDivElement | null>(null)\n  const itemRefs = React.useRef<Record<number, HTMLButtonElement | null>>({})\n  const openTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n    null\n  )\n  const closeTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n    null\n  )\n  const previewWidth = 220\n  const previewHeight = (previewWidth * 7) / 11\n  const previewGap = 12\n  const previewOpenDelayMs = 500\n  const clearOpenTimeout = React.useCallback(() => {\n    if (openTimeoutRef.current) {\n      clearTimeout(openTimeoutRef.current)\n      openTimeoutRef.current = null\n    }\n  }, [])\n  const clearCloseTimeout = React.useCallback(() => {\n    if (closeTimeoutRef.current) {\n      clearTimeout(closeTimeoutRef.current)\n      closeTimeoutRef.current = null\n    }\n  }, [])\n  const getPreviewPosition = React.useCallback(\n    (sheetIndex: number) => {\n      const item = itemRefs.current[sheetIndex]\n      if (!item || typeof window === \"undefined\") {\n        return { left: 0, top: 0 }\n      }\n      const itemRect = item.getBoundingClientRect()\n      const centeredLeft = itemRect.left + itemRect.width / 2 - previewWidth / 2\n      const minLeft = 8\n      const maxLeft = Math.max(minLeft, window.innerWidth - previewWidth - 8)\n      const left = Math.max(minLeft, Math.min(centeredLeft, maxLeft))\n      const top = Math.max(8, itemRect.top - previewHeight - previewGap)\n      return { left, top }\n    },\n    [previewHeight]\n  )\n  const updatePreviewPosition = React.useCallback(\n    (sheetIndex: number) => {\n      setPreviewPosition(getPreviewPosition(sheetIndex))\n    },\n    [getPreviewPosition]\n  )\n  const handleItemEnter = React.useCallback(\n    (sheetIndex: number) => {\n      clearCloseTimeout()\n      const nextPreviewPosition = getPreviewPosition(sheetIndex)\n      if (visiblePreviewIndex !== null) {\n        clearOpenTimeout()\n        setPreviewPosition(nextPreviewPosition)\n        setVisiblePreviewIndex(sheetIndex)\n        return\n      }\n      clearOpenTimeout()\n      openTimeoutRef.current = setTimeout(() => {\n        setPreviewPosition(nextPreviewPosition)\n        setVisiblePreviewIndex(sheetIndex)\n      }, previewOpenDelayMs)\n    },\n    [\n      clearCloseTimeout,\n      clearOpenTimeout,\n      getPreviewPosition,\n      visiblePreviewIndex,\n    ]\n  )\n  const handleContainerLeave = React.useCallback(() => {\n    clearOpenTimeout()\n    clearCloseTimeout()\n    closeTimeoutRef.current = setTimeout(() => {\n      setVisiblePreviewIndex(null)\n    }, 80)\n  }, [clearCloseTimeout, clearOpenTimeout])\n  React.useEffect(() => {\n    return () => {\n      clearOpenTimeout()\n      clearCloseTimeout()\n    }\n  }, [clearCloseTimeout, clearOpenTimeout])\n  React.useEffect(() => {\n    thumbnails.forEach((thumbnail) => {\n      setThumbnailUrls((current) => {\n        if (current[thumbnail.sheetIndex]) return current\n        const canvas = document.createElement(\"canvas\")\n        canvas.width = thumbnail.width\n        canvas.height = thumbnail.height\n        if (!thumbnail.paint(canvas)) return current\n        return {\n          ...current,\n          [thumbnail.sheetIndex]: canvas.toDataURL(\"image/png\"),\n        }\n      })\n    })\n  }, [thumbnails])\n  React.useEffect(() => {\n    if (visiblePreviewIndex === null) return\n    const handleReposition = () => updatePreviewPosition(visiblePreviewIndex)\n    handleReposition()\n    const scrollElement = scrollRef.current\n    window.addEventListener(\"resize\", handleReposition)\n    scrollElement?.addEventListener(\"scroll\", handleReposition, {\n      passive: true,\n    })\n    return () => {\n      window.removeEventListener(\"resize\", handleReposition)\n      scrollElement?.removeEventListener(\"scroll\", handleReposition)\n    }\n  }, [updatePreviewPosition, visiblePreviewIndex])\n  // The preview card portals to document.body, so it outlives the tab\n  // strip's own visibility: when the viewer is hidden or reparented under\n  // the cursor (keep-alive preview pools, a closing dialog) no mouseleave\n  // fires and the card would hang on screen. While the preview is open,\n  // poll the strip's effective visibility and dismiss as soon as it stops\n  // being shown.\n  React.useEffect(() => {\n    if (visiblePreviewIndex === null) return\n    const dismissWhenHidden = () => {\n      const element = scrollRef.current\n      const isVisible = Boolean(\n        element?.isConnected &&\n          (element.checkVisibility?.({ checkVisibilityCSS: true }) ?? true)\n      )\n      if (isVisible) return\n      clearOpenTimeout()\n      clearCloseTimeout()\n      setVisiblePreviewIndex(null)\n    }\n    const interval = setInterval(dismissWhenHidden, 200)\n    return () => clearInterval(interval)\n  }, [clearCloseTimeout, clearOpenTimeout, visiblePreviewIndex])\n  if (sheets.length <= 1) return null\n  const previewSheet =\n    visiblePreviewIndex === null ? null : sheets[visiblePreviewIndex]\n  const previewUrl =\n    visiblePreviewIndex === null\n      ? null\n      : (thumbnailUrls[visiblePreviewIndex] ?? null)\n  return (\n    <div\n      className=\"border-t bg-muted/40 px-3 py-2\"\n      onMouseLeave={handleContainerLeave}\n    >\n      <Tabs\n        value={String(activeSheetIndex)}\n        onValueChange={(value) => onActiveSheetIndexChange(Number(value))}\n        className=\"gap-0\"\n      >\n        <InlineScrollArea2\n          orientation=\"horizontal\"\n          scrollbarGutter\n          className=\"h-10 w-full has-[[data-slot=scroll-area-viewport][data-has-overflow-x]]:h-[50px]\"\n          viewportClassName=\"overflow-y-hidden\"\n          viewportRef={scrollRef}\n        >\n          <div className=\"flex h-full items-center\">\n            <TabsList className=\"shrink-0\">\n              {sheets.map((sheet, index) => (\n                <TabsTrigger\n                  key={`${sheet.workbookSheetIndex}-${sheet.name}`}\n                  ref={(node) => {\n                    itemRefs.current[index] = node\n                  }}\n                  value={String(index)}\n                  className=\"max-w-48 flex-none\"\n                  onMouseEnter={() => handleItemEnter(index)}\n                >\n                  <span className=\"truncate\">{sheet.name}</span>\n                </TabsTrigger>\n              ))}\n            </TabsList>\n          </div>\n        </InlineScrollArea2>\n      </Tabs>\n      {typeof document !== \"undefined\" &&\n      previewSheet &&\n      visiblePreviewIndex !== null &&\n      previewUrl\n        ? createPortal(\n            <div\n              className=\"pointer-events-none fixed z-40 translate-y-0 overflow-hidden rounded-lg border bg-background/95 opacity-100 shadow-xl backdrop-blur-md transition-[opacity,transform] duration-100\"\n              style={{\n                left: previewPosition.left,\n                top: previewPosition.top,\n                width: previewWidth,\n              }}\n            >\n              <div className=\"relative aspect-[11/7] w-full overflow-hidden bg-muted/60\">\n                {/* eslint-disable-next-line @next/next/no-img-element -- Workbook sheet previews are generated runtime image URLs. */}\n                <img\n                  key={`${visiblePreviewIndex}-${previewUrl}`}\n                  src={previewUrl}\n                  alt={`${previewSheet.name} preview`}\n                  className=\"absolute inset-0 h-full w-full object-cover object-left-top\"\n                />\n              </div>\n            </div>,\n            document.body\n          )\n        : null}\n    </div>\n  )\n})\nexport function XlsxWorkbookSurface({\n  className,\n  isDark,\n  onDownload,\n  onIsDarkChange,\n  onUploadClick,\n  renderTableHeaderMenu,\n  showDownloadButton = true,\n  showNightRenderToggle,\n  showToolbar = true,\n  showUploadButton = true,\n  toolbarActions,\n  workbookIdentity,\n}: {\n  className?: string\n  isDark: boolean\n  onDownload?: () => void\n  onIsDarkChange: (checked: boolean) => void\n  onUploadClick: () => void\n  renderTableHeaderMenu: (\n    props: XlsxTableHeaderMenuRenderProps\n  ) => React.ReactNode\n  showDownloadButton?: boolean\n  showNightRenderToggle: boolean\n  showToolbar?: boolean\n  showUploadButton?: boolean\n  toolbarActions?: React.ReactNode\n  workbookIdentity: string\n}) {\n  const { error } = useXlsxViewer()\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const renderSearchableScroller = React.useCallback(\n    ({ children, viewportProps }: XlsxScrollerRenderProps) => (\n      <InlineScrollArea2\n        className=\"h-full min-h-0 w-full min-w-0 flex-1\"\n        viewportProps={viewportProps}\n        viewportRef={viewportRef}\n      >\n        {children}\n      </InlineScrollArea2>\n    ),\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      {showToolbar ? (\n        <WorkbookToolbar\n          isDark={isDark}\n          onDownload={onDownload}\n          onIsDarkChange={onIsDarkChange}\n          onUploadClick={onUploadClick}\n          showDownloadButton={showDownloadButton}\n          showNightRenderToggle={showNightRenderToggle}\n          showUploadButton={showUploadButton}\n          toolbarActions={toolbarActions}\n          viewportRef={viewportRef}\n          workbookIdentity={workbookIdentity}\n        />\n      ) : null}\n      <div className=\"flex min-h-0 flex-1 flex-col\">\n        <div className=\"min-h-0 flex-1 bg-muted/20\">\n          <XlsxViewer\n            experimentalCanvas\n            allowResizeInReadOnly\n            className=\"h-full min-h-0 min-w-0\"\n            height=\"100%\"\n            isDark={isDark}\n            readOnly\n            rounded={false}\n            showDefaultToolbar={false}\n            showImages\n            fileTooLargeState={\n              <div className=\"grid h-full w-full min-w-full place-items-center p-6\">\n                <div className=\"max-w-sm rounded-lg border bg-background p-4 text-sm\">\n                  <p className=\"font-medium\">File too large</p>\n                  <p className=\"mt-1 text-muted-foreground\">\n                    This workbook exceeds the display limit. Download it to view\n                    the full file.\n                  </p>\n                </div>\n              </div>\n            }\n            loadingState={<ViewerLoadingSurface />}\n            renderScroller={renderSearchableScroller}\n            errorState={\n              <div className=\"grid h-full w-full min-w-full place-items-center p-6 text-sm text-destructive\">\n                {error?.message ?? \"Unable to display workbook.\"}\n              </div>\n            }\n            renderTableHeaderMenu={renderTableHeaderMenu}\n          />\n        </div>\n        <WorkbookSheetTabs workbookIdentity={workbookIdentity} />\n      </div>\n    </div>\n  )\n}\nexport function XlsxViewerPreview({\n  className,\n  fileName,\n  isDark,\n  onIsDarkChange,\n  showDownload = true,\n  showToolbar = true,\n  showUpload = true,\n  src,\n  toolbarActions,\n}: {\n  className?: string\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    <XlsxViewerContent\n      key={src}\n      className={className}\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 XlsxViewerContent({\n  className,\n  effectiveIsDark,\n  fileName,\n  setNightRenderEnabled,\n  shouldRenderNightMode,\n  showDownload,\n  showToolbar = true,\n  showUpload,\n  toolbarActions,\n  url,\n}: {\n  className?: string\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 [uploadedWorkbook, setUploadedWorkbook] =\n    React.useState<UploadedWorkbook | null>(null)\n  const sourceFileName = React.useMemo(\n    () =>\n      url ? formatWorkbookName(fileName, url) : (fileName ?? \"workbook.xlsx\"),\n    [fileName, url]\n  )\n  const displayFileName = React.useMemo(\n    () => uploadedWorkbook?.fileName ?? sourceFileName,\n    [sourceFileName, uploadedWorkbook?.fileName]\n  )\n  const workbookIdentity = React.useMemo(\n    () => uploadedWorkbook?.identity ?? `${url ?? \"empty\"}::${displayFileName}`,\n    [displayFileName, uploadedWorkbook?.identity, url]\n  )\n  const [workbookBuffer, setWorkbookBuffer] =\n    React.useState<ArrayBuffer | null>(null)\n  const [loadError, setLoadError] = React.useState<string>()\n  const shouldShowLoadingSpinner = useDelayedLoadingIndicator(\n    !workbookBuffer && !loadError && !uploadedWorkbook,\n    XLSX_LOADING_INDICATOR_DELAY_MS\n  )\n  React.useEffect(() => {\n    let isCurrent = true\n    async function loadWorkbook(): Promise<void> {\n      if (!url) return\n      try {\n        const response = await fetch(url)\n        if (!response.ok) {\n          throw new Error(`Failed to fetch XLSX (${response.status})`)\n        }\n        const nextWorkbookBuffer = await response.arrayBuffer()\n        if (!isCurrent) return\n        setWorkbookBuffer(nextWorkbookBuffer)\n      } catch (error) {\n        if (!isCurrent) return\n        setLoadError(\n          error instanceof Error ? error.message : \"Unknown XLSX load error\"\n        )\n      }\n    }\n    void loadWorkbook()\n    return () => {\n      isCurrent = false\n    }\n  }, [url])\n  async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {\n    const file = event.target.files?.[0]\n    event.target.value = \"\"\n    if (!file) return\n    const buffer = await file.arrayBuffer()\n    setLoadError(undefined)\n    setUploadedWorkbook({\n      buffer,\n      fileName: file.name,\n      identity: `${file.name}-${file.size}-${file.lastModified}`,\n    })\n  }\n  const activeBuffer = uploadedWorkbook?.buffer ?? workbookBuffer\n  const activeFileName = uploadedWorkbook?.fileName ?? displayFileName\n  const activeIdentity = workbookIdentity\n  if (!url && !uploadedWorkbook) {\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=\".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel\"\n          className=\"hidden\"\n          onChange={handleUpload}\n        />\n        <WorkbookStandaloneToolbar\n          onUploadClick={() => fileInputRef.current?.click()}\n          showUploadButton={showUpload}\n          toolbarActions={toolbarActions}\n        />\n        <div className=\"grid min-h-0 flex-1 place-items-center bg-muted/30 p-4\">\n          <div className=\"max-w-md rounded-lg border bg-background p-4 text-center text-sm shadow-xs\">\n            <p className=\"font-medium\">Upload a workbook to preview</p>\n            <p className=\"mt-1 text-muted-foreground\">\n              Pass an XLSX URL with the <code>src</code> prop or upload a file.\n            </p>\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 XLSX\n            </Button>\n          </div>\n        </div>\n      </div>\n    )\n  }\n  if (loadError && !activeBuffer) {\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=\".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel\"\n          className=\"hidden\"\n          onChange={handleUpload}\n        />\n        <WorkbookStandaloneToolbar\n          onUploadClick={() => fileInputRef.current?.click()}\n          showUploadButton={showUpload}\n          toolbarActions={toolbarActions}\n        />\n        <div className=\"grid min-h-0 flex-1 place-items-center bg-muted/30 p-4\">\n          <div className=\"max-w-md rounded-lg border bg-background p-4 text-sm\">\n            <p className=\"font-medium\">Unable to display workbook</p>\n            <p className=\"mt-1 text-muted-foreground\">{loadError}</p>\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 XLSX\n            </Button>\n          </div>\n        </div>\n      </div>\n    )\n  }\n  if (!activeBuffer) {\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=\".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel\"\n          className=\"hidden\"\n          onChange={handleUpload}\n        />\n        <WorkbookStandaloneToolbar\n          onUploadClick={() => fileInputRef.current?.click()}\n          showUploadButton={showUpload}\n          toolbarActions={toolbarActions}\n        />\n        <ViewerLoadingSurface showSpinner={shouldShowLoadingSpinner} />\n      </div>\n    )\n  }\n  return (\n    <div className={cn(\"overflow-hidden\", className)}>\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept=\".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel\"\n        className=\"hidden\"\n        onChange={handleUpload}\n      />\n      <XlsxWorkbookLoadedViewer\n        className={className}\n        fileName={activeFileName}\n        isDark={effectiveIsDark}\n        onDownload={() => downloadWorkbookBuffer(activeBuffer, activeFileName)}\n        onIsDarkChange={setNightRenderEnabled}\n        onUploadClick={() => fileInputRef.current?.click()}\n        renderTableHeaderMenu={(props) => (\n          <WorkbookTableHeaderMenu {...props} />\n        )}\n        showDownloadButton={showDownload}\n        showNightRenderToggle={shouldRenderNightMode}\n        showToolbar={showToolbar}\n        showUploadButton={showUpload}\n        toolbarActions={toolbarActions}\n        workbookBuffer={activeBuffer}\n        workbookIdentity={activeIdentity}\n      />\n    </div>\n  )\n}\nfunction XlsxWorkbookLoadedViewer({\n  className,\n  fileName,\n  isDark,\n  onDownload,\n  onIsDarkChange,\n  onUploadClick,\n  renderTableHeaderMenu,\n  showDownloadButton,\n  showNightRenderToggle,\n  showToolbar = true,\n  showUploadButton,\n  toolbarActions,\n  workbookBuffer,\n  workbookIdentity,\n}: {\n  className?: string\n  fileName: string\n  isDark: boolean\n  onDownload: () => void\n  onIsDarkChange: (checked: boolean) => void\n  onUploadClick: () => void\n  renderTableHeaderMenu: (\n    props: XlsxTableHeaderMenuRenderProps\n  ) => React.ReactNode\n  showDownloadButton: boolean\n  showNightRenderToggle: boolean\n  showToolbar?: boolean\n  showUploadButton: boolean\n  toolbarActions?: React.ReactNode\n  workbookBuffer: ArrayBuffer\n  workbookIdentity: string\n}) {\n  const controller = useXlsxViewerController(\n    React.useMemo(\n      () => ({\n        allowResizeInReadOnly: true,\n        file: workbookBuffer,\n        fileName,\n        maxFileSizeBytes: XLSX_MAX_FILE_SIZE_BYTES,\n        readOnly: true,\n        useWorker: true,\n      }),\n      [fileName, workbookBuffer]\n    )\n  )\n  return (\n    <XlsxViewerProvider controller={controller} isDark={isDark}>\n      <XlsxWorkbookSurface\n        className={className}\n        isDark={isDark}\n        onDownload={onDownload}\n        onIsDarkChange={onIsDarkChange}\n        onUploadClick={onUploadClick}\n        renderTableHeaderMenu={renderTableHeaderMenu}\n        showDownloadButton={showDownloadButton}\n        showNightRenderToggle={showNightRenderToggle}\n        showToolbar={showToolbar}\n        showUploadButton={showUploadButton}\n        toolbarActions={toolbarActions}\n        workbookIdentity={workbookIdentity}\n      />\n    </XlsxViewerProvider>\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/xlsx-viewer.tsx"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui"
}
