{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdf-editor-core",
  "title": "PDF Editor Core",
  "description": "Shared PDF editor behavior and state for the Base UI and Radix UI artifacts.",
  "dependencies": [
    "@embedpdf/core@^2.15.0",
    "@embedpdf/engines@^2.15.0",
    "@embedpdf/models@^2.15.0",
    "@embedpdf/plugin-annotation@^2.15.0",
    "@embedpdf/plugin-attachment@^2.15.0",
    "@embedpdf/plugin-bookmark@^2.15.0",
    "@embedpdf/plugin-capture@^2.15.0",
    "@embedpdf/plugin-document-manager@^2.15.0",
    "@embedpdf/plugin-export@^2.15.0",
    "@embedpdf/plugin-form@^2.15.0",
    "@embedpdf/plugin-fullscreen@^2.15.0",
    "@embedpdf/plugin-history@^2.15.0",
    "@embedpdf/plugin-interaction-manager@^2.15.0",
    "@embedpdf/plugin-pan@^2.15.0",
    "@embedpdf/plugin-print@^2.15.0",
    "@embedpdf/plugin-redaction@^2.15.0",
    "@embedpdf/plugin-render@^2.15.0",
    "@embedpdf/plugin-rotate@^2.15.0",
    "@embedpdf/plugin-scroll@^2.15.0",
    "@embedpdf/plugin-search@^2.15.0",
    "@embedpdf/plugin-selection@^2.15.0",
    "@embedpdf/plugin-signature@^2.15.0",
    "@embedpdf/plugin-spread@^2.15.0",
    "@embedpdf/plugin-stamp@^2.15.0",
    "@embedpdf/plugin-thumbnail@^2.15.0",
    "@embedpdf/plugin-tiling@^2.15.0",
    "@embedpdf/plugin-viewport@^2.15.0",
    "@embedpdf/plugin-zoom@^2.15.0",
    "pdf-lib@^1.17.1",
    "@base-ui/react@^1.4.1"
  ],
  "registryDependencies": [
    "@extend/document-viewer-sidebar",
    "resizable",
    "utils",
    "badge",
    "button",
    "dropdown-menu",
    "input",
    "popover",
    "scroll-area",
    "select",
    "tooltip",
    "@extend/document-color-picker",
    "@extend/document-toast",
    "kbd",
    "dialog",
    "switch",
    "toggle-group"
  ],
  "files": [
    {
      "path": "components/extend/pdf-editor.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\nimport { createPluginRegistration } from \"@embedpdf/core\"\nimport {\n  EmbedPDF,\n  PDFContext,\n  useDocumentPermissions,\n  useRegistry,\n} from \"@embedpdf/core/react\"\nimport {\n  PdfAnnotationSubtype,\n  PdfErrorCode,\n  PdfPageFlattenFlag,\n  uuidV4,\n  type PdfAnnotationObject,\n  type PdfDocumentObject,\n  type PdfEngine,\n  type PdfLinkAnnoObject,\n  type PdfPrintOptions,\n} from \"@embedpdf/models\"\nimport {\n  AnnotationLayer,\n  AnnotationPluginPackage,\n  getSelectedAnnotations,\n  LockModeType,\n  useAnnotation,\n  useAnnotationCapability,\n  type AnnotationEvent,\n  type AnnotationSelectionMenuProps,\n  type AnnotationTransferItem,\n  type GroupSelectionMenuProps,\n} from \"@embedpdf/plugin-annotation/react\"\nimport { AttachmentPluginPackage } from \"@embedpdf/plugin-attachment/react\"\nimport { BookmarkPluginPackage } from \"@embedpdf/plugin-bookmark/react\"\nimport {\n  CapturePluginPackage,\n  MarqueeCapture,\n  useCapture,\n  type CaptureAreaEvent,\n} from \"@embedpdf/plugin-capture/react\"\nimport {\n  DocumentManagerPluginPackage,\n  useActiveDocument,\n  useDocumentManagerCapability,\n} from \"@embedpdf/plugin-document-manager/react\"\nimport { ExportPluginPackage, useExport } from \"@embedpdf/plugin-export/react\"\nimport {\n  FormPluginPackage,\n  useFormCapability,\n} from \"@embedpdf/plugin-form/react\"\nimport {\n  FullscreenPluginPackage,\n  useFullscreen,\n} from \"@embedpdf/plugin-fullscreen/react\"\nimport {\n  HistoryPluginPackage,\n  useHistoryCapability,\n} from \"@embedpdf/plugin-history/react\"\nimport {\n  GlobalPointerProvider,\n  InteractionManagerPluginPackage,\n  PagePointerProvider,\n} from \"@embedpdf/plugin-interaction-manager/react\"\nimport { PanPluginPackage, usePan } from \"@embedpdf/plugin-pan/react\"\nimport { PrintPluginPackage, usePrint } from \"@embedpdf/plugin-print/react\"\nimport {\n  RedactionLayer,\n  RedactionMode,\n  RedactionPluginPackage,\n  useRedaction,\n  type RedactionSelectionMenuProps,\n} from \"@embedpdf/plugin-redaction/react\"\nimport { RenderLayer, RenderPluginPackage } from \"@embedpdf/plugin-render/react\"\nimport {\n  Rotate,\n  RotatePluginPackage,\n  useRotate,\n} from \"@embedpdf/plugin-rotate/react\"\nimport {\n  Scroller,\n  ScrollPluginPackage,\n  ScrollStrategy,\n  useScroll,\n  useScrollCapability,\n  type PageLayout,\n} from \"@embedpdf/plugin-scroll/react\"\nimport {\n  SearchLayer,\n  SearchPluginPackage,\n  useSearch,\n} from \"@embedpdf/plugin-search/react\"\nimport {\n  CopyToClipboard,\n  SelectionLayer,\n  SelectionPluginPackage,\n  useSelectionCapability,\n  type SelectionSelectionMenuProps,\n} from \"@embedpdf/plugin-selection/react\"\nimport {\n  deserializeEntries,\n  serializeEntries,\n  SignatureMode,\n  SignaturePluginPackage,\n  useSignatureCapability,\n} from \"@embedpdf/plugin-signature/react\"\nimport {\n  SpreadMode,\n  SpreadPluginPackage,\n  useSpread,\n} from \"@embedpdf/plugin-spread/react\"\nimport { StampPluginPackage } from \"@embedpdf/plugin-stamp/react\"\nimport { ThumbnailPluginPackage } from \"@embedpdf/plugin-thumbnail/react\"\nimport { TilingLayer, TilingPluginPackage } from \"@embedpdf/plugin-tiling/react\"\nimport {\n  useIsViewportGated,\n  useViewportCapability,\n  useViewportElement,\n  useViewportRef,\n  ViewportElementContext,\n  ViewportPluginPackage,\n} from \"@embedpdf/plugin-viewport/react\"\nimport {\n  MarqueeZoom,\n  useZoom,\n  ZoomMode,\n  ZoomPluginPackage,\n} from \"@embedpdf/plugin-zoom/react\"\n\nimport { getPdfSourceUrl, loadSharedPdfEngine } from \"@/lib/pdf-thumbnail-utils\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\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 { TooltipProvider } from \"@/components/ui/tooltip\"\nimport { ColorPicker } from \"@/components/extend/document-color-picker\"\nimport {\n  useElementWidth,\n  useInlineThumbnailSidebar,\n} from \"@/components/extend/document-viewer-sidebar\"\nimport {\n  PdfEditorCaptureDialog,\n  PdfEditorConfirmDialog,\n  PdfEditorLinkDialog,\n  PdfEditorPasswordPrompt,\n  PdfEditorPrintDialog,\n  PdfEditorPropertiesDialog,\n  PdfEditorSecurityDialog,\n  PdfEditorShortcutsDialog,\n  PdfEditorSignatureDialog,\n} from \"@/components/extend/pdf-editor-dialogs\"\nimport {\n  PdfEditorAttachmentsPanel,\n  PdfEditorCommentsPanel,\n  PdfEditorFormsPanel,\n  PdfEditorOutlinePanel,\n  PdfEditorPagesPanel,\n  PdfEditorRedactionsPanel,\n  PdfEditorSignaturesPanel,\n  PdfEditorStampsPanel,\n  PdfEditorThumbnailsPanel,\n} from \"@/components/extend/pdf-editor-panels\"\nimport { PdfEditorPropertiesPanel } from \"@/components/extend/pdf-editor-properties\"\nimport {\n  BookOpenGlyph,\n  CameraGlyph,\n  ChevronLeftGlyph,\n  ChevronRightGlyph,\n  CloseGlyph,\n  CommentGlyph,\n  CommentsGlyph,\n  CopyGlyph,\n  CursorGlyph,\n  downloadArrayBuffer,\n  downloadBlob,\n  DownloadGlyph,\n  EditGlyph,\n  ExitFullscreenGlyph,\n  ExternalLinkGlyph,\n  EyeOffGlyph,\n  FileTextGlyph,\n  FullscreenGlyph,\n  getPdfFileName,\n  GridGlyph,\n  GroupGlyph,\n  HandGlyph,\n  HighlighterGlyph,\n  InfoGlyph,\n  isTypingTarget,\n  KeyboardGlyph,\n  LayersGlyph,\n  LinkGlyph,\n  ListTreeGlyph,\n  LockGlyph,\n  MarqueeZoomGlyph,\n  MoreGlyph,\n  noop,\n  PaletteGlyph,\n  PanelLeftGlyph,\n  PanelRightGlyph,\n  PaperclipGlyph,\n  PDF_EDITOR_ANNOTATION_TOOL_GROUPS,\n  PDF_EDITOR_DEFAULT_COLOR_PRESETS,\n  PDF_EDITOR_DEFAULT_FEATURES,\n  PDF_EDITOR_DEFAULT_SIGNATURE_FONTS_URL,\n  PDF_EDITOR_DEFAULT_STAMP_MANIFEST_URL,\n  PDF_EDITOR_FORM_TOOLS,\n  PDF_EDITOR_MODE_OPTIONS,\n  PDF_EDITOR_SELECTION_COLOR,\n  PdfEditorContextProvider,\n  PdfEditorScrollArea,\n  PdfEditorScrollAreaResolverContext,\n  PdfEditorToolbarSeparator,\n  PdfEditorToolButton,\n  PdfEditorTooltip,\n  PlusGlyph,\n  PrintGlyph,\n  RedactAreaGlyph,\n  RedoGlyph,\n  resolveDefaultScrollAreaViewport,\n  RotateGlyph,\n  SearchGlyph,\n  ShieldGlyph,\n  SignatureGlyph,\n  SlidersGlyph,\n  StampGlyph,\n  StrikethroughGlyph,\n  TextFieldGlyph,\n  TextGlyph,\n  toNormalizedRect,\n  TrashGlyph,\n  UnderlineGlyph,\n  UndoGlyph,\n  UngroupGlyph,\n  UnlockGlyph,\n  UploadGlyph,\n  usePdfEditor,\n  ZoomInGlyph,\n  ZoomOutGlyph,\n  type PdfEditorContextValue,\n  type PdfEditorDialogRequest,\n  type PdfEditorFeatureFlags,\n  type PdfEditorGlyph,\n  type PdfEditorLeftPanel,\n  type PdfEditorMode,\n  type PdfEditorNoticeTone,\n  type PdfEditorResolvedFeatures,\n  type PdfEditorRightPanel,\n  type PdfEditorScrollAreaViewportResolver,\n  type PdfEditorSelectionAction,\n  type PdfEditorSelectionPage,\n  type PdfEditorSelectionPayload,\n} from \"@/components/extend/pdf-editor-shared\"\nimport { PdfEditorWorkspace } from \"@/components/extend/pdf-editor-workspace\"\nimport { ToastPrimitive, ToastProvider } from \"@/components/extend/ui-toast\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\nexport type {\n  PdfEditorFeatureFlags,\n  PdfEditorLeftPanel,\n  PdfEditorMode,\n  PdfEditorNormalizedRect,\n  PdfEditorRightPanel,\n  PdfEditorSelectionAction,\n  PdfEditorSelectionPayload,\n} from \"@/components/extend/pdf-editor-shared\"\n/* -------------------------------------------------------------------------- */\n/* Public types                                                               */\n/* -------------------------------------------------------------------------- */\nexport type PDFEditorPageOverlayProps = {\n  pageNumber: number\n  pageWidth: number\n  pageHeight: number\n  scale: number\n  rotation: number\n}\nexport type PDFEditorZoomLevel = number | \"fit-page\" | \"fit-width\" | \"automatic\"\nexport type PDFEditorToast = {\n  message: string\n  tone: PdfEditorNoticeTone\n}\nexport type PDFEditorHandle = {\n  /** Current PDF bytes including annotations, form values, and pending security changes. */\n  getDocumentBuffer: () => Promise<ArrayBuffer>\n  download: (fileName?: string) => Promise<void>\n  print: (options?: PdfPrintOptions) => Promise<void>\n  exportAnnotations: () => Promise<AnnotationTransferItem[]>\n  importAnnotations: (items: AnnotationTransferItem[]) => void\n  getFormValues: () => Record<string, string>\n  setFormValues: (values: Record<string, string>) => Promise<void>\n  /** Permanently applies every pending redaction mark. */\n  applyRedactions: () => Promise<void>\n  setMode: (mode: PdfEditorMode) => void\n  /** Activates an EmbedPDF annotation tool id such as `highlight` or `ink`; `null` returns to selection. */\n  setActiveTool: (toolId: string | null) => void\n  scrollToPage: (pageNumber: number, options?: ScrollIntoViewOptions) => void\n  undo: () => void\n  redo: () => void\n  getViewportElement: () => HTMLDivElement | null\n}\nexport type PDFEditorProps = {\n  className?: string\n  /**\n   * A path, URL, PDF data URL, or raw base64 string.\n   * Raw base64 is recognized by the encoded %PDF- header.\n   * Also accepts PDF bytes as an ArrayBuffer, Uint8Array, Blob, or File.\n   */\n  src?: string | ArrayBuffer | Uint8Array | Blob\n  fileName?: string\n  defaultZoom?: PDFEditorZoomLevel\n  defaultMode?: PdfEditorMode\n  /** Author written into new annotations. Defaults to \"Guest\". */\n  annotationAuthor?: string\n  colorPresets?: string[]\n  /** Turn individual editing capabilities off. Everything is enabled by default. */\n  features?: PdfEditorFeatureFlags\n  showToolbar?: boolean\n  showUpload?: boolean\n  showDownload?: boolean\n  toolbarActions?: React.ReactNode\n  /** Extra actions for the text selection menu. Each receives normalized page coordinates. */\n  selectionActions?: PdfEditorSelectionAction[]\n  /** Persist saved signatures in localStorage. Pass a string to use a custom storage key. */\n  persistSignatures?: boolean | string\n  /** Additional rubber stamp manifests to load next to the built-in library. */\n  stampManifests?: string[]\n  /** Stylesheet used for the typed-signature fonts. Pass `null` to skip loading web fonts. */\n  signatureFontsStylesheetUrl?: string | null\n  resolveScrollAreaViewport?: PdfEditorScrollAreaViewportResolver\n  renderPageOverlay?: (props: PDFEditorPageOverlayProps) => React.ReactNode\n  onDocumentLoadSuccess?: (info: {\n    numPages: number\n    documentId: string\n    fileName: string\n  }) => void\n  onDocumentLoadError?: (message: string) => void\n  onActivePageChange?: (pageNumber: number) => void\n  onModeChange?: (mode: PdfEditorMode) => void\n  onAnnotationEvent?: (event: AnnotationEvent) => void\n  /** Fires with the full annotation set after every committed change (debounced). */\n  onAnnotationsChange?: (items: AnnotationTransferItem[]) => void\n  onFormValuesChange?: (values: Record<string, string>) => void\n  onRedactionsApplied?: () => void\n  onSave?: (result: { buffer: ArrayBuffer; fileName: string }) => void\n  onCapture?: (event: CaptureAreaEvent) => void\n  onPdfUpload?: (file: File) => void\n  /** Routes editor notices to a host-owned toast system instead of the built-in viewport. */\n  onToast?: (toast: PDFEditorToast) => void\n  onSelectionAction?: (\n    actionId: string,\n    payload: PdfEditorSelectionPayload\n  ) => void\n}\n/* -------------------------------------------------------------------------- */\n/* Constants                                                                  */\n/* -------------------------------------------------------------------------- */\nconst PAGE_GAP = 24\nconst THUMBNAIL_PAGE_WIDTH = 92\nconst THUMBNAIL_IMAGE_PADDING = 8\nconst THUMBNAIL_WIDTH = THUMBNAIL_PAGE_WIDTH + THUMBNAIL_IMAGE_PADDING * 2\nconst THUMBNAIL_LABEL_HEIGHT = 24\nconst THUMBNAIL_GAP = 12\nconst THUMBNAIL_PANE_PADDING_Y = 16\nconst RIGHT_PANEL_INLINE_MIN_WIDTH = 1100\nconst TOOLBAR_STACK_MAX_WIDTH = 640\nconst ZOOM_PRESETS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4]\nconst MIN_ZOOM = 0.1\nconst MAX_ZOOM = 8\nconst PAGE_BASE_RENDER_MAX_SCALE = 1\nconst PAGE_BASE_RENDER_DPR = 1\nconst SEARCH_DEBOUNCE_MS = 300\nconst NOTICE_DURATION_MS = 4200\nconst TOOLTIP_DELAY_MS = 5\nconst SELECTION_MENU_OFFSET = 8\nconst SELECTION_MENU_HEIGHT = 36\nconst DEFAULT_SIGNATURE_STORAGE_KEY = \"pdf-editor:signatures\"\nconst DEFAULT_ANNOTATION_AUTHOR = \"Guest\"\nconst ZOOM_MODE_LABELS: Record<ZoomMode, string> = {\n  [ZoomMode.FitPage]: \"Fit page\",\n  [ZoomMode.FitWidth]: \"Fit width\",\n  [ZoomMode.Automatic]: \"Automatic\",\n}\nconst SPREAD_MODE_OPTIONS = [\n  { label: \"Single page\", value: SpreadMode.None },\n  { label: \"Two pages\", value: SpreadMode.Odd },\n  { label: \"Two pages, cover\", value: SpreadMode.Even },\n]\nconst SCROLL_DIRECTION_OPTIONS = [\n  { label: \"Scroll down\", value: \"vertical\" },\n  { label: \"Scroll sideways\", value: \"horizontal\" },\n]\nfunction toZoomLevel(level: PDFEditorZoomLevel): ZoomMode | number {\n  if (level === \"fit-page\") return ZoomMode.FitPage\n  if (level === \"fit-width\") return ZoomMode.FitWidth\n  if (level === \"automatic\") return ZoomMode.Automatic\n  return level\n}\nfunction isZoomMode(value: unknown): value is ZoomMode {\n  return (\n    value === ZoomMode.FitPage ||\n    value === ZoomMode.FitWidth ||\n    value === ZoomMode.Automatic\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Engine + document source                                                   */\n/* -------------------------------------------------------------------------- */\nfunction useSharedPdfEngine() {\n  const [engine, setEngine] = React.useState<PdfEngine | null>(null)\n  const [error, setError] = React.useState<Error | null>(null)\n  React.useEffect(() => {\n    let cancelled = false\n    loadSharedPdfEngine().then(\n      (loadedEngine) => {\n        if (!cancelled) setEngine(loadedEngine)\n      },\n      (loadError: Error) => {\n        if (!cancelled) setError(loadError)\n      }\n    )\n    return () => {\n      cancelled = true\n    }\n  }, [])\n  return { engine, error }\n}\ntype EditorSource =\n  | {\n      key: string\n      kind: \"url\"\n      url: string\n      name: string\n    }\n  | {\n      key: string\n      kind: \"buffer\"\n      buffer: ArrayBuffer\n      name: string\n    }\nlet sourceCounter = 0\nfunction nextSourceKey() {\n  sourceCounter += 1\n  return `pdf-editor-source-${sourceCounter}`\n}\nfunction bytesToArrayBuffer(bytes: Uint8Array) {\n  const copy = new ArrayBuffer(bytes.byteLength)\n  new Uint8Array(copy).set(bytes)\n  return copy\n}\nfunction useEditorSource(\n  src: PDFEditorProps[\"src\"],\n  fileName: string | undefined\n) {\n  const sourceId = React.useId()\n  const [input, setInput] = React.useState({ src, fileName, version: 0 })\n  if (input.src !== src || input.fileName !== fileName) {\n    setInput({ src, fileName, version: input.version + 1 })\n  }\n  const immediateSource = React.useMemo<EditorSource | null>(() => {\n    const key = `${sourceId}-${input.version}`\n    if (typeof input.src === \"string\") {\n      const url = getPdfSourceUrl(input.src)\n      return {\n        key,\n        kind: \"url\",\n        url,\n        name: getPdfFileName(input.fileName, url),\n      }\n    }\n    if (input.src instanceof ArrayBuffer || input.src instanceof Uint8Array) {\n      return {\n        key,\n        kind: \"buffer\",\n        buffer:\n          input.src instanceof ArrayBuffer\n            ? input.src.slice(0)\n            : bytesToArrayBuffer(input.src),\n        name: getPdfFileName(input.fileName),\n      }\n    }\n    return null\n  }, [input, sourceId])\n  const [resolved, setResolved] = React.useState<{\n    input: typeof input\n    source: EditorSource\n  } | null>(null)\n  React.useEffect(() => {\n    if (!(input.src instanceof Blob)) return\n    let cancelled = false\n    const blobName =\n      input.fileName ?? (input.src instanceof File ? input.src.name : undefined)\n    input.src.arrayBuffer().then((buffer) => {\n      if (cancelled) return\n      setResolved({\n        input,\n        source: {\n          key: nextSourceKey(),\n          kind: \"buffer\",\n          buffer,\n          name: getPdfFileName(blobName),\n        },\n      })\n    })\n    return () => {\n      cancelled = true\n    }\n  }, [input])\n  const replace = React.useCallback(\n    (buffer: ArrayBuffer, name: string) => {\n      setResolved({\n        input,\n        source: { key: nextSourceKey(), kind: \"buffer\", buffer, name },\n      })\n    },\n    [input]\n  )\n  const source = resolved?.input === input ? resolved.source : immediateSource\n  return { source, loading: input.src instanceof Blob && !source, replace }\n}\n/* -------------------------------------------------------------------------- */\n/* Annotation JSON serialization                                              */\n/* -------------------------------------------------------------------------- */\nfunction arrayBufferToBase64(buffer: ArrayBuffer) {\n  const bytes = new Uint8Array(buffer)\n  let binary = \"\"\n  for (let index = 0; index < bytes.length; index += 0x8000) {\n    binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000))\n  }\n  return btoa(binary)\n}\nfunction base64ToArrayBuffer(value: string) {\n  const binary = atob(value)\n  const bytes = new Uint8Array(binary.length)\n  for (let index = 0; index < binary.length; index += 1) {\n    bytes[index] = binary.charCodeAt(index)\n  }\n  return bytes.buffer\n}\ntype SerializedAnnotationItem = {\n  annotation: PdfAnnotationObject\n  ctx?: {\n    data: string\n    mimeType?: string\n  }\n}\nexport function serializeAnnotationItems(items: AnnotationTransferItem[]) {\n  return {\n    format: \"extend-ui/pdf-editor-annotations\",\n    version: 1,\n    exportedAt: new Date().toISOString(),\n    annotations: items.map((item): SerializedAnnotationItem => {\n      const context = item.ctx as\n        | {\n            data?: ArrayBuffer\n            mimeType?: string\n          }\n        | undefined\n      return {\n        annotation: item.annotation,\n        ...(context?.data instanceof ArrayBuffer\n          ? {\n              ctx: {\n                data: arrayBufferToBase64(context.data),\n                mimeType: context.mimeType,\n              },\n            }\n          : {}),\n      }\n    }),\n  }\n}\nexport function deserializeAnnotationItems(\n  payload: unknown\n): AnnotationTransferItem[] {\n  const list = Array.isArray(payload)\n    ? payload\n    : payload &&\n        typeof payload === \"object\" &&\n        Array.isArray(\n          (\n            payload as {\n              annotations?: unknown\n            }\n          ).annotations\n        )\n      ? (\n          payload as {\n            annotations: unknown[]\n          }\n        ).annotations\n      : null\n  if (!list) throw new Error(\"Unrecognized annotation export.\")\n  return list.map((entry) => {\n    const item = entry as SerializedAnnotationItem\n    if (!item || typeof item !== \"object\" || !item.annotation) {\n      throw new Error(\"Unrecognized annotation export.\")\n    }\n    return {\n      annotation: item.annotation,\n      ...(item.ctx?.data\n        ? {\n            ctx: {\n              data: base64ToArrayBuffer(item.ctx.data),\n              mimeType: item.ctx.mimeType,\n            } as AnnotationTransferItem[\"ctx\"],\n          }\n        : {}),\n    }\n  })\n}\n/* -------------------------------------------------------------------------- */\n/* Viewport                                                                   */\n/* -------------------------------------------------------------------------- */\nfunction PdfEditorViewport({\n  children,\n  className,\n  documentId,\n}: {\n  children: React.ReactNode\n  className?: string\n  documentId: string\n}) {\n  const viewportRef = useViewportRef(documentId)\n  const { provides: viewport } = useViewportCapability()\n  const isGated = useIsViewportGated(documentId)\n  const viewportGap = viewport?.getViewportGap() ?? 0\n  return (\n    <ViewportElementContext.Provider value={viewportRef}>\n      <PdfEditorScrollArea\n        className={className}\n        orientation=\"both\"\n        scrollbarGutter={false}\n        viewportClassName=\"relative select-none selection:bg-transparent selection:text-inherit\"\n        viewportProps={{ style: { padding: viewportGap } }}\n        viewportRef={viewportRef}\n      >\n        {isGated ? null : children}\n      </PdfEditorScrollArea>\n    </ViewportElementContext.Provider>\n  )\n}\nfunction PdfEditorViewportBridge({\n  viewportElementRef,\n}: {\n  viewportElementRef: React.MutableRefObject<HTMLDivElement | null>\n}) {\n  const elementRef = useViewportElement()\n  React.useEffect(() => {\n    viewportElementRef.current = elementRef?.current ?? null\n  })\n  return null\n}\n/* -------------------------------------------------------------------------- */\n/* Selection menus rendered inside the page layers                            */\n/* -------------------------------------------------------------------------- */\nfunction SelectionMenuShell({\n  rect,\n  placement,\n  menuWrapperProps,\n  children,\n}: {\n  rect: SelectionSelectionMenuProps[\"rect\"]\n  placement: SelectionSelectionMenuProps[\"placement\"]\n  menuWrapperProps: SelectionSelectionMenuProps[\"menuWrapperProps\"]\n  children: React.ReactNode\n}) {\n  return (\n    <div {...menuWrapperProps}>\n      <div\n        data-slot=\"pdf-editor-selection-menu\"\n        className=\"flex items-center gap-0.5 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg\"\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: placement.suggestTop\n            ? -(SELECTION_MENU_HEIGHT + SELECTION_MENU_OFFSET)\n            : rect.size.height + SELECTION_MENU_OFFSET,\n          pointerEvents: \"auto\",\n          cursor: \"default\",\n          whiteSpace: \"nowrap\",\n        }}\n        onPointerDown={(event) => event.stopPropagation()}\n      >\n        <TooltipProvider delay={TOOLTIP_DELAY_MS}>{children}</TooltipProvider>\n      </div>\n    </div>\n  )\n}\nfunction MenuButton({\n  label,\n  glyph: Glyph,\n  onClick,\n  disabled,\n  destructive,\n}: {\n  label: string\n  glyph: PdfEditorGlyph\n  onClick: () => void\n  disabled?: boolean\n  destructive?: boolean\n}) {\n  return (\n    <PdfEditorToolButton\n      label={label}\n      size=\"icon-xs\"\n      disabled={disabled}\n      className={cn(destructive && \"text-destructive hover:text-destructive\")}\n      onClick={(event) => {\n        event.stopPropagation()\n        onClick()\n      }}\n    >\n      <Glyph className=\"size-3.5\" />\n    </PdfEditorToolButton>\n  )\n}\nasync function buildSelectionPayload(\n  documentId: string,\n  document: PdfDocumentObject | null,\n  selectionScope: ReturnType<\n    NonNullable<\n      ReturnType<typeof useSelectionCapability>[\"provides\"]\n    >[\"forDocument\"]\n  >\n): Promise<PdfEditorSelectionPayload | null> {\n  const formatted = selectionScope.getFormattedSelection()\n  if (formatted.length === 0) return null\n  let text = \"\"\n  try {\n    const lines = await selectionScope.getSelectedText().toPromise()\n    text = lines.join(\"\\n\")\n  } catch {\n    text = \"\"\n  }\n  const pages: PdfEditorSelectionPage[] = formatted.map((page) => {\n    const pageSize = document?.pages[page.pageIndex]?.size ?? {\n      width: 1,\n      height: 1,\n    }\n    return {\n      pageIndex: page.pageIndex,\n      pageNumber: page.pageIndex + 1,\n      pageSize,\n      rect: page.rect,\n      rects: page.segmentRects,\n      normalizedRect: toNormalizedRect(page.rect, pageSize),\n      normalizedRects: page.segmentRects.map((rect) =>\n        toNormalizedRect(rect, pageSize)\n      ),\n    }\n  })\n  return { documentId, text, pages, ...pages[0] }\n}\nfunction PdfEditorTextSelectionMenu({\n  documentId,\n  rect,\n  placement,\n  menuWrapperProps,\n}: SelectionSelectionMenuProps & {\n  documentId: string\n}) {\n  const { provides: selectionCapability } = useSelectionCapability()\n  const { provides: annotationCapability } = useAnnotationCapability()\n  const { provides: annotation } = useAnnotation(documentId)\n  const { provides: redaction } = useRedaction(documentId)\n  const editor = usePdfEditor()\n  const selectionScope = React.useMemo(\n    () => selectionCapability?.forDocument(documentId) ?? null,\n    [documentId, selectionCapability]\n  )\n  const canAnnotate =\n    editor.features.annotate && editor.permissions.canModifyAnnotations\n  const canRedact =\n    editor.features.redact && editor.permissions.canModifyContents\n  const createMarkup = (\n    toolId: string,\n    subtype:\n      | PdfAnnotationSubtype.HIGHLIGHT\n      | PdfAnnotationSubtype.UNDERLINE\n      | PdfAnnotationSubtype.STRIKEOUT,\n    options: {\n      select?: boolean\n      openComments?: boolean\n    } = {}\n  ) => {\n    if (!selectionScope || !annotation) return\n    const formatted = selectionScope.getFormattedSelection()\n    if (formatted.length === 0) return\n    const defaults = (annotationCapability?.getTool(toolId)?.defaults ??\n      {}) as Record<string, unknown>\n    const finish = (text: string[]) => {\n      let firstId: {\n        pageIndex: number\n        id: string\n      } | null = null\n      for (const page of formatted) {\n        const id = uuidV4()\n        annotation.createAnnotation(page.pageIndex, {\n          ...defaults,\n          id,\n          type: subtype,\n          pageIndex: page.pageIndex,\n          rect: page.rect,\n          segmentRects: page.segmentRects,\n          author: editor.annotationAuthor,\n          created: new Date(),\n          modified: new Date(),\n          flags: [\"print\"],\n          custom: text.length ? { text: text.join(\"\\n\") } : undefined,\n        } as PdfAnnotationObject)\n        firstId ??= { pageIndex: page.pageIndex, id }\n      }\n      selectionScope.clear()\n      if (options.select && firstId) {\n        annotation.selectAnnotation(firstId.pageIndex, firstId.id)\n      }\n      if (options.openComments) editor.setRightPanel(\"comments\")\n    }\n    selectionScope.getSelectedText().wait(finish, () => finish([]))\n  }\n  const runCustomAction = async (action: PdfEditorSelectionAction) => {\n    if (!selectionScope) return\n    const payload = await buildSelectionPayload(\n      documentId,\n      editor.document,\n      selectionScope\n    )\n    if (!payload) return\n    action.onSelect?.(payload)\n    editor.onSelectionAction?.(action.id, payload)\n    if (!action.keepSelection) selectionScope.clear()\n  }\n  return (\n    <SelectionMenuShell\n      rect={rect}\n      placement={placement}\n      menuWrapperProps={menuWrapperProps}\n    >\n      <MenuButton\n        label=\"Copy\"\n        glyph={CopyGlyph}\n        onClick={() => {\n          selectionScope?.copyToClipboard()\n          selectionScope?.clear()\n        }}\n      />\n      {canAnnotate ? (\n        <>\n          <PdfEditorToolbarSeparator />\n          <MenuButton\n            label=\"Highlight\"\n            glyph={HighlighterGlyph}\n            onClick={() =>\n              createMarkup(\"highlight\", PdfAnnotationSubtype.HIGHLIGHT)\n            }\n          />\n          <MenuButton\n            label=\"Underline\"\n            glyph={UnderlineGlyph}\n            onClick={() =>\n              createMarkup(\"underline\", PdfAnnotationSubtype.UNDERLINE)\n            }\n          />\n          <MenuButton\n            label=\"Strikethrough\"\n            glyph={StrikethroughGlyph}\n            onClick={() =>\n              createMarkup(\"strikeout\", PdfAnnotationSubtype.STRIKEOUT)\n            }\n          />\n          {editor.features.comments ? (\n            <MenuButton\n              label=\"Comment\"\n              glyph={CommentGlyph}\n              onClick={() =>\n                createMarkup(\"highlight\", PdfAnnotationSubtype.HIGHLIGHT, {\n                  select: true,\n                  openComments: true,\n                })\n              }\n            />\n          ) : null}\n          <MenuButton\n            label=\"Add link\"\n            glyph={LinkGlyph}\n            onClick={() =>\n              editor.openDialog({ type: \"link\", source: \"selection\" })\n            }\n          />\n        </>\n      ) : null}\n      {canRedact ? (\n        <>\n          <PdfEditorToolbarSeparator />\n          <MenuButton\n            label=\"Mark for redaction\"\n            glyph={EyeOffGlyph}\n            onClick={() => {\n              redaction?.queueCurrentSelectionAsPending().wait(\n                () => {\n                  selectionScope?.clear()\n                  editor.setRightPanel(\"redactions\")\n                },\n                () =>\n                  editor.notify(\"The selection could not be marked.\", \"error\")\n              )\n            }}\n          />\n        </>\n      ) : null}\n      {editor.selectionActions.length > 0 ? (\n        <>\n          <PdfEditorToolbarSeparator />\n          {editor.selectionActions.map((action) => (\n            <PdfEditorTooltip key={action.id} label={action.label}>\n              <Button\n                type=\"button\"\n                size=\"xs\"\n                variant=\"ghost\"\n                onClick={(event) => {\n                  event.stopPropagation()\n                  void runCustomAction(action)\n                }}\n              >\n                {action.icon}\n                {action.label}\n              </Button>\n            </PdfEditorTooltip>\n          ))}\n        </>\n      ) : null}\n    </SelectionMenuShell>\n  )\n}\nconst STAMP_SOURCE_TYPES = new Set<PdfAnnotationSubtype>([\n  PdfAnnotationSubtype.INK,\n  PdfAnnotationSubtype.SQUARE,\n  PdfAnnotationSubtype.CIRCLE,\n  PdfAnnotationSubtype.LINE,\n  PdfAnnotationSubtype.POLYLINE,\n  PdfAnnotationSubtype.POLYGON,\n  PdfAnnotationSubtype.FREETEXT,\n  PdfAnnotationSubtype.STAMP,\n  PdfAnnotationSubtype.TEXT,\n])\nfunction PdfEditorAnnotationMenu({\n  documentId,\n  selected,\n  context,\n  rect,\n  placement,\n  menuWrapperProps,\n}: AnnotationSelectionMenuProps & {\n  documentId: string\n}) {\n  const { provides: annotation } = useAnnotation(documentId)\n  const { provides: redaction } = useRedaction(documentId)\n  const editor = usePdfEditor()\n  if (!selected || !annotation) return null\n  const object = context.annotation.object\n  const { pageIndex, id } = object\n  const isRedact = object.type === PdfAnnotationSubtype.REDACT\n  const isWidget = object.type === PdfAnnotationSubtype.WIDGET\n  const isLink = object.type === PdfAnnotationSubtype.LINK\n  const isLocked = (object.flags ?? []).includes(\"locked\")\n  const canModify = editor.permissions.canModifyAnnotations\n  const canDelete = canModify && !context.structurallyLocked\n  const remove = () => annotation.deleteAnnotation(pageIndex, id)\n  if (isRedact) {\n    return (\n      <SelectionMenuShell\n        rect={rect}\n        placement={placement}\n        menuWrapperProps={menuWrapperProps}\n      >\n        <MenuButton\n          label=\"Apply redaction\"\n          glyph={EyeOffGlyph}\n          disabled={!editor.permissions.canModifyContents}\n          onClick={() =>\n            editor.openDialog({\n              type: \"confirm\",\n              title: \"Apply this redaction?\",\n              description: \"The marked content is removed permanently.\",\n              confirmLabel: \"Apply\",\n              onConfirm: () => {\n                redaction\n                  ?.commitPending(pageIndex, id)\n                  .wait(noop, () =>\n                    editor.notify(\n                      \"The redaction could not be applied.\",\n                      \"error\"\n                    )\n                  )\n              },\n            })\n          }\n        />\n        <MenuButton\n          label=\"Style\"\n          glyph={SlidersGlyph}\n          onClick={() => editor.setRightPanel(\"properties\")}\n        />\n        <MenuButton\n          label=\"Remove mark\"\n          glyph={TrashGlyph}\n          destructive\n          onClick={remove}\n        />\n      </SelectionMenuShell>\n    )\n  }\n  if (isLink) {\n    const target = (object as PdfLinkAnnoObject).target\n    return (\n      <SelectionMenuShell\n        rect={rect}\n        placement={placement}\n        menuWrapperProps={menuWrapperProps}\n      >\n        <MenuButton\n          label=\"Open link\"\n          glyph={ExternalLinkGlyph}\n          disabled={!target}\n          onClick={() => {\n            if (!target) return\n            annotation.navigateTarget(target).wait((result) => {\n              if (result.outcome === \"uri\") {\n                window.open(result.uri, \"_blank\", \"noopener\")\n              }\n            }, noop)\n          }}\n        />\n        <MenuButton\n          label=\"Edit link\"\n          glyph={EditGlyph}\n          disabled={!canModify}\n          onClick={() =>\n            editor.openDialog({ type: \"link\", source: \"annotation\" })\n          }\n        />\n        <MenuButton\n          label=\"Delete\"\n          glyph={TrashGlyph}\n          destructive\n          disabled={!canDelete}\n          onClick={remove}\n        />\n      </SelectionMenuShell>\n    )\n  }\n  return (\n    <SelectionMenuShell\n      rect={rect}\n      placement={placement}\n      menuWrapperProps={menuWrapperProps}\n    >\n      {!isWidget && editor.features.comments ? (\n        <MenuButton\n          label=\"Comment\"\n          glyph={CommentGlyph}\n          onClick={() => editor.setRightPanel(\"comments\")}\n        />\n      ) : null}\n      <MenuButton\n        label=\"Properties\"\n        glyph={SlidersGlyph}\n        onClick={() => editor.setRightPanel(\"properties\")}\n      />\n      {!isWidget && canModify ? (\n        <MenuButton\n          label=\"Add link\"\n          glyph={LinkGlyph}\n          onClick={() =>\n            editor.openDialog({ type: \"link\", source: \"annotation\" })\n          }\n        />\n      ) : null}\n      {!isWidget &&\n      editor.features.stamps &&\n      canModify &&\n      STAMP_SOURCE_TYPES.has(object.type) ? (\n        <MenuButton\n          label=\"Save as stamp\"\n          glyph={StampGlyph}\n          onClick={() => editor.setRightPanel(\"stamps\")}\n        />\n      ) : null}\n      {!isWidget && canModify ? (\n        <MenuButton\n          label={isLocked ? \"Unlock\" : \"Lock\"}\n          glyph={isLocked ? UnlockGlyph : LockGlyph}\n          onClick={() => {\n            const flags = object.flags ?? []\n            annotation.updateAnnotation(pageIndex, id, {\n              flags: isLocked\n                ? flags.filter((flag) => flag !== \"locked\")\n                : [...flags, \"locked\"],\n            })\n          }}\n        />\n      ) : null}\n      <MenuButton\n        label=\"Delete\"\n        glyph={TrashGlyph}\n        destructive\n        disabled={!canDelete}\n        onClick={remove}\n      />\n    </SelectionMenuShell>\n  )\n}\nfunction PdfEditorGroupMenu({\n  documentId,\n  selected,\n  context,\n  rect,\n  placement,\n  menuWrapperProps,\n}: GroupSelectionMenuProps & {\n  documentId: string\n}) {\n  const { provides: annotation } = useAnnotation(documentId)\n  const editor = usePdfEditor()\n  if (!selected || !annotation) return null\n  const groupingAction = annotation.getGroupingAction()\n  const canModify = editor.permissions.canModifyAnnotations\n  return (\n    <SelectionMenuShell\n      rect={rect}\n      placement={placement}\n      menuWrapperProps={menuWrapperProps}\n    >\n      <span className=\"px-1.5 text-xs text-muted-foreground\">\n        {context.annotations.length} selected\n      </span>\n      <MenuButton\n        label={groupingAction === \"ungroup\" ? \"Ungroup\" : \"Group\"}\n        glyph={groupingAction === \"ungroup\" ? UngroupGlyph : GroupGlyph}\n        disabled={!canModify || groupingAction === \"disabled\"}\n        onClick={() => {\n          if (groupingAction === \"ungroup\") {\n            annotation.ungroupAnnotations(context.annotations[0].object.id)\n          } else {\n            annotation.groupAnnotations()\n          }\n        }}\n      />\n      <MenuButton\n        label=\"Properties\"\n        glyph={SlidersGlyph}\n        onClick={() => editor.setRightPanel(\"properties\")}\n      />\n      <MenuButton\n        label=\"Delete all\"\n        glyph={TrashGlyph}\n        destructive\n        disabled={!canModify}\n        onClick={() =>\n          annotation.deleteAnnotations(\n            context.annotations.map((item) => ({\n              pageIndex: item.object.pageIndex,\n              id: item.object.id,\n            }))\n          )\n        }\n      />\n    </SelectionMenuShell>\n  )\n}\nfunction PdfEditorRedactionMenu({\n  documentId,\n  selected,\n  context,\n  rect,\n  placement,\n  menuWrapperProps,\n}: RedactionSelectionMenuProps & {\n  documentId: string\n}) {\n  const { provides: redaction } = useRedaction(documentId)\n  const editor = usePdfEditor()\n  if (!selected || !redaction) return null\n  const { page, id } = context.item\n  return (\n    <SelectionMenuShell\n      rect={rect}\n      placement={placement}\n      menuWrapperProps={menuWrapperProps}\n    >\n      <MenuButton\n        label=\"Apply redaction\"\n        glyph={EyeOffGlyph}\n        disabled={!editor.permissions.canModifyContents}\n        onClick={() =>\n          editor.openDialog({\n            type: \"confirm\",\n            title: \"Apply this redaction?\",\n            description: \"The marked content is removed permanently.\",\n            confirmLabel: \"Apply\",\n            onConfirm: () => {\n              redaction\n                .commitPending(page, id)\n                .wait(noop, () =>\n                  editor.notify(\"The redaction could not be applied.\", \"error\")\n                )\n            },\n          })\n        }\n      />\n      <MenuButton\n        label=\"Remove mark\"\n        glyph={TrashGlyph}\n        destructive\n        onClick={() => redaction.removePending(page, id)}\n      />\n    </SelectionMenuShell>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Toolbar controls                                                           */\n/* -------------------------------------------------------------------------- */\nfunction PdfEditorPageControl({\n  activePage,\n  numPages,\n  disabled,\n  onPageChange,\n}: {\n  activePage: number\n  numPages: number\n  disabled: boolean\n  onPageChange: (pageNumber: number) => void\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const displayPage = numPages ? 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(numPages, 1)))\n    },\n    [numPages, onPageChange]\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={disabled || !numPages}\n          onClick={() => {\n            setDraftPage(String(displayPage))\n            setIsEditing(true)\n          }}\n        >\n          {displayPage}\n        </Button>\n      )}\n      <span>of {numPages || \"–\"}</span>\n    </div>\n  )\n}\nfunction PdfEditorZoomControl({\n  documentId,\n  disabled,\n}: {\n  documentId: string\n  disabled: boolean\n}) {\n  const { state, provides: zoom } = useZoom(documentId)\n  const current = state.currentZoomLevel\n  const selectValue = isZoomMode(state.zoomLevel)\n    ? state.zoomLevel\n    : String(Number(current.toFixed(2)))\n  const presets = ZOOM_PRESETS.includes(Number(current.toFixed(2)))\n    ? ZOOM_PRESETS\n    : [...ZOOM_PRESETS, Number(current.toFixed(2))].sort((a, b) => a - b)\n  return (\n    <div className=\"flex items-center gap-0.5\">\n      <PdfEditorToolButton\n        label=\"Zoom out\"\n        shortcut=\"mod+-\"\n        disabled={disabled || current <= MIN_ZOOM}\n        onClick={() => zoom?.zoomOut()}\n      >\n        <ZoomOutGlyph className=\"size-4\" />\n      </PdfEditorToolButton>\n      <Select\n        value={selectValue}\n        disabled={disabled}\n        modal={false}\n        onValueChange={(value) => {\n          const next = String(value)\n          zoom?.requestZoom(isZoomMode(next) ? next : Number(next))\n        }}\n      >\n        <SelectTrigger\n          size=\"sm\"\n          className=\"w-[104px] min-w-[104px]\"\n          aria-label=\"Zoom level\"\n        >\n          <SelectValue placeholder=\"Zoom\">\n            {Math.round(current * 100)}%\n          </SelectValue>\n        </SelectTrigger>\n        <SelectContent alignItemWithTrigger={false}>\n          {(Object.keys(ZOOM_MODE_LABELS) as ZoomMode[]).map((mode) => (\n            <SelectItem key={mode} value={mode}>\n              {ZOOM_MODE_LABELS[mode]}\n            </SelectItem>\n          ))}\n          {presets.map((preset) => (\n            <SelectItem key={preset} value={String(preset)}>\n              {Math.round(preset * 100)}%\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n      <PdfEditorToolButton\n        label=\"Zoom in\"\n        shortcut=\"mod+=\"\n        disabled={disabled || current >= MAX_ZOOM}\n        onClick={() => zoom?.zoomIn()}\n      >\n        <ZoomInGlyph className=\"size-4\" />\n      </PdfEditorToolButton>\n    </div>\n  )\n}\nfunction PdfEditorSearchControl({\n  documentId,\n  disabled,\n  open,\n  onOpenChange,\n}: {\n  documentId: string\n  disabled: boolean\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}) {\n  const { state, provides } = useSearch(documentId)\n  const { provides: scroll } = useScroll(documentId)\n  const [draft, setDraft] = React.useState(\"\")\n  const [query, setQuery] = React.useState(\"\")\n  const [isSearching, setIsSearching] = React.useState(false)\n  const providesRef = React.useRef(provides)\n  const scrollRef = React.useRef(scroll)\n  const requestIdRef = React.useRef(0)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  React.useEffect(() => {\n    providesRef.current = provides\n    scrollRef.current = scroll\n  }, [provides, scroll])\n  React.useEffect(() => {\n    if (!open) return\n    const frame = window.requestAnimationFrame(() => inputRef.current?.focus())\n    return () => window.cancelAnimationFrame(frame)\n  }, [open])\n  const scrollToResult = React.useCallback(\n    (index: number) => {\n      const result = state.results[index]\n      if (!result || !scroll) return\n      const firstRect = result.rects[0]\n      scroll.scrollToPage({\n        pageNumber: result.pageIndex + 1,\n        ...(firstRect\n          ? {\n              pageCoordinates: { x: firstRect.origin.x, y: firstRect.origin.y },\n              alignY: 30,\n            }\n          : {}),\n        behavior: \"auto\",\n      })\n    },\n    [scroll, state.results]\n  )\n  const runSearch = React.useCallback((rawQuery: string) => {\n    const nextQuery = rawQuery.trim()\n    const requestId = requestIdRef.current + 1\n    requestIdRef.current = requestId\n    setQuery(nextQuery)\n    const searchProvider = providesRef.current\n    const scrollProvider = scrollRef.current\n    if (!searchProvider) {\n      setIsSearching(false)\n      return\n    }\n    if (!nextQuery) {\n      searchProvider.stopSearch()\n      setIsSearching(false)\n      return\n    }\n    setIsSearching(true)\n    searchProvider.startSearch()\n    searchProvider.searchAllPages(nextQuery).wait(\n      (result) => {\n        if (requestIdRef.current !== requestId) return\n        const first = result.results[0]\n        if (first && scrollProvider) {\n          searchProvider.goToResult(0)\n          const firstRect = first.rects[0]\n          scrollProvider.scrollToPage({\n            pageNumber: first.pageIndex + 1,\n            ...(firstRect\n              ? {\n                  pageCoordinates: {\n                    x: firstRect.origin.x,\n                    y: firstRect.origin.y,\n                  },\n                  alignY: 30,\n                }\n              : {}),\n            behavior: \"auto\",\n          })\n        }\n        setIsSearching(false)\n      },\n      () => {\n        if (requestIdRef.current === requestId) setIsSearching(false)\n      }\n    )\n  }, [])\n  React.useEffect(() => {\n    if (!draft.trim()) return\n    const timeout = window.setTimeout(\n      () => runSearch(draft),\n      SEARCH_DEBOUNCE_MS\n    )\n    return () => window.clearTimeout(timeout)\n  }, [draft, runSearch])\n  const clear = () => {\n    requestIdRef.current += 1\n    setDraft(\"\")\n    setQuery(\"\")\n    setIsSearching(false)\n    provides?.stopSearch()\n  }\n  const navigate = (direction: 1 | -1) => {\n    if (!provides || state.total === 0) return\n    scrollToResult(\n      direction === 1 ? provides.nextResult() : provides.previousResult()\n    )\n  }\n  const resultLabel = isSearching\n    ? \"Searching…\"\n    : !query\n      ? \"Type to search\"\n      : state.total\n        ? `${state.activeResultIndex + 1} / ${state.total}`\n        : \"No results\"\n  return (\n    <Popover open={open} onOpenChange={onOpenChange}>\n      <PdfEditorTooltip label=\"Search\" shortcut=\"mod+F\">\n        <PopoverTrigger\n          render={\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label=\"Search\"\n              disabled={disabled}\n              className={cn(query && \"text-primary\")}\n            >\n              <SearchGlyph className=\"size-4\" />\n            </Button>\n          }\n        ></PopoverTrigger>\n      </PdfEditorTooltip>\n      <PopoverContent\n        align=\"end\"\n        className=\"w-72 p-0 [&>[data-slot=popover-viewport]]:p-3.5!\"\n      >\n        <div className=\"space-y-3\">\n          <Input\n            ref={inputRef}\n            placeholder=\"Search text\"\n            value={draft}\n            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {\n              const next = event.target.value\n              setDraft(next)\n              if (next.trim()) {\n                setIsSearching(true)\n              } else {\n                clear()\n              }\n            }}\n            onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {\n              if (event.key !== \"Enter\") return\n              event.preventDefault()\n              if (state.total) {\n                navigate(event.shiftKey ? -1 : 1)\n              } else if (draft.trim()) {\n                runSearch(draft)\n              }\n            }}\n          />\n          <div className=\"flex items-center justify-between gap-2\">\n            <div className=\"min-w-0 truncate text-xs text-muted-foreground\">\n              {resultLabel}\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 || state.total === 0}\n                onClick={() => navigate(-1)}\n              >\n                <ChevronLeftGlyph className=\"size-4\" />\n              </Button>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"icon-sm\"\n                aria-label=\"Next result\"\n                disabled={isSearching || state.total === 0}\n                onClick={() => navigate(1)}\n              >\n                <ChevronRightGlyph className=\"size-4\" />\n              </Button>\n              <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={clear}>\n                Clear\n              </Button>\n            </div>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\nfunction PdfEditorModeSwitch({\n  mode,\n  features,\n  compact = false,\n  onModeChange,\n}: {\n  mode: PdfEditorMode\n  features: PdfEditorResolvedFeatures\n  compact?: boolean\n  onModeChange: (mode: PdfEditorMode) => void\n}) {\n  const options = PDF_EDITOR_MODE_OPTIONS.filter(\n    (option) => option.feature === null || features[option.feature]\n  )\n  return (\n    <div\n      role=\"group\"\n      aria-label=\"Editor mode\"\n      className=\"flex shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5\"\n    >\n      {options.map((option) => {\n        const Glyph = option.glyph\n        const active = option.id === mode\n        return (\n          <Button\n            key={option.id}\n            type=\"button\"\n            size={compact ? \"icon-xs\" : \"xs\"}\n            variant={active ? \"default\" : \"ghost\"}\n            aria-pressed={active}\n            aria-label={option.label}\n            className={cn(\"gap-1.5\", !active && \"text-muted-foreground\")}\n            onClick={() => onModeChange(option.id)}\n          >\n            <Glyph className=\"size-3.5\" />\n            {compact ? null : (\n              <span className=\"hidden md:inline\">{option.label}</span>\n            )}\n          </Button>\n        )\n      })}\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Side panel shells                                                          */\n/* -------------------------------------------------------------------------- */\nfunction PanelTabStrip<T extends string>({\n  tabs,\n  value,\n  onChange,\n  onClose,\n  label,\n  disabled = false,\n}: {\n  tabs: Array<{\n    id: T\n    label: string\n    glyph: PdfEditorGlyph\n  }>\n  value: T | null\n  onChange: (value: T) => void\n  onClose: () => void\n  label: string\n  disabled?: boolean\n}) {\n  return (\n    <div className=\"flex shrink-0 items-center justify-between gap-1 border-b px-1.5 py-1\">\n      <TooltipProvider delay={TOOLTIP_DELAY_MS}>\n        <div\n          className=\"flex min-w-0 items-center gap-0.5 overflow-x-auto overflow-y-hidden\"\n          role=\"tablist\"\n          aria-label={label}\n        >\n          {tabs.map((tab) => {\n            const Glyph = tab.glyph\n            return (\n              <PdfEditorToolButton\n                key={tab.id}\n                label={tab.label}\n                active={value === tab.id}\n                role=\"tab\"\n                aria-selected={value === tab.id}\n                disabled={disabled}\n                onClick={() => onChange(tab.id)}\n              >\n                <Glyph className=\"size-4\" />\n              </PdfEditorToolButton>\n            )\n          })}\n        </div>\n        <PdfEditorToolButton\n          label=\"Close panel\"\n          size=\"icon-xs\"\n          disabled={disabled}\n          onClick={onClose}\n        >\n          <CloseGlyph className=\"size-3.5\" />\n        </PdfEditorToolButton>\n      </TooltipProvider>\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Signature persistence + engine warm-up utilities                           */\n/* -------------------------------------------------------------------------- */\nfunction PdfEditorSignaturePersistence({\n  storageKey,\n}: {\n  storageKey: string | null\n}) {\n  const { provides: signatureCapability } = useSignatureCapability()\n  React.useEffect(() => {\n    if (!signatureCapability || !storageKey) return\n    try {\n      const raw = window.localStorage.getItem(storageKey)\n      if (raw && signatureCapability.getEntries().length === 0) {\n        signatureCapability.loadEntries(deserializeEntries(JSON.parse(raw)))\n      }\n    } catch {\n      // Ignore corrupt or unavailable storage.\n    }\n    return signatureCapability.onEntriesChange((entries) => {\n      try {\n        window.localStorage.setItem(\n          storageKey,\n          JSON.stringify(serializeEntries(entries))\n        )\n      } catch {\n        // Storage may be unavailable (private mode, quota).\n      }\n    })\n  }, [signatureCapability, storageKey])\n  return null\n}\n/* -------------------------------------------------------------------------- */\n/* Fallback shell (engine loading, document loading, errors, empty)           */\n/* -------------------------------------------------------------------------- */\nfunction PdfEditorDocumentStatus({\n  state,\n  errorMessage,\n  onUploadFile,\n}: {\n  state: \"loading\" | \"error\" | \"empty\"\n  errorMessage?: string\n  onUploadFile?: (file: File) => void\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  return (\n    <div\n      data-slot=\"pdf-editor-document-status\"\n      data-state={state}\n      className=\"grid min-h-0 flex-1 place-items-center p-6 text-center text-sm text-muted-foreground\"\n    >\n      {state === \"loading\" ? (\n        <InlineSpinner className=\"size-4\" />\n      ) : (\n        <div className=\"flex max-w-sm flex-col items-center gap-3\">\n          <div role={state === \"error\" ? \"alert\" : undefined}>\n            {state === \"error\"\n              ? (errorMessage ?? \"The PDF could not be loaded.\")\n              : \"Open a PDF to start editing\"}\n          </div>\n          {onUploadFile ? (\n            <>\n              <input\n                ref={inputRef}\n                type=\"file\"\n                accept=\"application/pdf,.pdf\"\n                className=\"sr-only\"\n                tabIndex={-1}\n                onChange={(event) => {\n                  const file = event.target.files?.[0]\n                  if (file) onUploadFile(file)\n                  event.currentTarget.value = \"\"\n                }}\n              />\n              <Button\n                type=\"button\"\n                size=\"sm\"\n                variant=\"outline\"\n                onClick={() => inputRef.current?.click()}\n              >\n                <UploadGlyph className=\"size-4\" />\n                Open PDF\n              </Button>\n            </>\n          ) : null}\n        </div>\n      )}\n    </div>\n  )\n}\nconst EMPTY_EDITOR_CONTEXT: React.ContextType<typeof PDFContext> = {\n  registry: null,\n  coreState: null,\n  isInitializing: true,\n  pluginsReady: false,\n  activeDocumentId: null,\n  activeDocument: null,\n  documents: {},\n  documentStates: [],\n}\n/* -------------------------------------------------------------------------- */\n/* Editor (per loaded document)                                               */\n/* -------------------------------------------------------------------------- */\ntype EditorHandleImpl = PDFEditorHandle\ntype PdfEditorInnerProps = {\n  documentId: string\n  document: PdfDocumentObject | null\n  documentState: \"loading\" | \"ready\" | \"empty\" | \"error\" | \"password\"\n  unavailableContent?: React.ReactNode\n  fileName: string\n  className?: string\n  defaultZoom: PDFEditorZoomLevel\n  defaultMode: PdfEditorMode\n  leftPanel: PdfEditorLeftPanel | null\n  setLeftPanel: React.Dispatch<React.SetStateAction<PdfEditorLeftPanel | null>>\n  formDesignMode: boolean\n  setFormDesignMode: React.Dispatch<React.SetStateAction<boolean>>\n  annotationAuthor: string\n  features: PdfEditorResolvedFeatures\n  showToolbar: boolean\n  showUpload: boolean\n  showDownload: boolean\n  toolbarActions?: React.ReactNode\n  selectionActions: PdfEditorSelectionAction[]\n  signatureFontsStylesheetUrl: string | null\n  renderPageOverlay?: (props: PDFEditorPageOverlayProps) => React.ReactNode\n  handleRef: React.MutableRefObject<EditorHandleImpl | null>\n  onReplaceDocument: (buffer: ArrayBuffer, fileName: string) => void\n  onUploadFile: (file: File) => void\n  onActivePageChange?: (pageNumber: number) => void\n  onModeChange?: (mode: PdfEditorMode) => void\n  onAnnotationEvent?: (event: AnnotationEvent) => void\n  onAnnotationsChange?: (items: AnnotationTransferItem[]) => void\n  onFormValuesChange?: (values: Record<string, string>) => void\n  onRedactionsApplied?: () => void\n  onSave?: (result: { buffer: ArrayBuffer; fileName: string }) => void\n  onCapture?: (event: CaptureAreaEvent) => void\n  onToast?: (toast: PDFEditorToast) => void\n  onSelectionAction?: (\n    actionId: string,\n    payload: PdfEditorSelectionPayload\n  ) => void\n}\nconst LEFT_PANEL_TABS: Array<{\n  id: PdfEditorLeftPanel\n  label: string\n  glyph: PdfEditorGlyph\n  feature: keyof PdfEditorFeatureFlags | null\n}> = [\n  { id: \"thumbnails\", label: \"Thumbnails\", glyph: GridGlyph, feature: null },\n  { id: \"outline\", label: \"Outline\", glyph: ListTreeGlyph, feature: \"outline\" },\n  {\n    id: \"attachments\",\n    label: \"Attachments\",\n    glyph: PaperclipGlyph,\n    feature: \"attachments\",\n  },\n  {\n    id: \"pages\",\n    label: \"Organize pages\",\n    glyph: LayersGlyph,\n    feature: \"pages\",\n  },\n]\nconst RIGHT_PANEL_TABS: Array<{\n  id: PdfEditorRightPanel\n  label: string\n  glyph: PdfEditorGlyph\n  feature: keyof PdfEditorFeatureFlags | null\n}> = [\n  { id: \"properties\", label: \"Properties\", glyph: SlidersGlyph, feature: null },\n  {\n    id: \"comments\",\n    label: \"Comments\",\n    glyph: CommentsGlyph,\n    feature: \"comments\",\n  },\n  {\n    id: \"redactions\",\n    label: \"Redactions\",\n    glyph: EyeOffGlyph,\n    feature: \"redact\",\n  },\n  { id: \"stamps\", label: \"Stamps\", glyph: StampGlyph, feature: \"stamps\" },\n  {\n    id: \"signatures\",\n    label: \"Signatures\",\n    glyph: SignatureGlyph,\n    feature: \"sign\",\n  },\n  {\n    id: \"forms\",\n    label: \"Form fields\",\n    glyph: TextFieldGlyph,\n    feature: \"forms\",\n  },\n]\nfunction PdfEditorInner({\n  documentId,\n  document: pdfDocument,\n  documentState,\n  unavailableContent,\n  fileName,\n  className,\n  defaultZoom,\n  defaultMode,\n  leftPanel,\n  setLeftPanel,\n  formDesignMode,\n  setFormDesignMode,\n  annotationAuthor,\n  features,\n  showToolbar,\n  showUpload,\n  showDownload,\n  toolbarActions,\n  selectionActions,\n  signatureFontsStylesheetUrl,\n  renderPageOverlay,\n  handleRef,\n  onReplaceDocument,\n  onUploadFile,\n  onActivePageChange,\n  onModeChange,\n  onAnnotationEvent,\n  onAnnotationsChange,\n  onFormValuesChange,\n  onRedactionsApplied,\n  onSave,\n  onCapture,\n  onToast,\n  onSelectionAction,\n}: PdfEditorInnerProps) {\n  const { registry } = useRegistry()\n  const permissions = useDocumentPermissions(documentId)\n  const { state: scrollState, provides: scroll } = useScroll(documentId)\n  const { provides: scrollCapability } = useScrollCapability()\n  const { state: zoomState, provides: zoom } = useZoom(documentId)\n  const { provides: rotate } = useRotate(documentId)\n  const { spreadMode, provides: spread } = useSpread(documentId)\n  const { provides: pan, isPanning } = usePan(documentId)\n  const { state: captureState, provides: capture } = useCapture(documentId)\n  const { provides: fullscreen, state: fullscreenState } = useFullscreen()\n  const { provides: historyCapability } = useHistoryCapability()\n  const { provides: annotationCapability } = useAnnotationCapability()\n  const { state: annotationState, provides: annotation } =\n    useAnnotation(documentId)\n  const { state: redactionState, provides: redaction } =\n    useRedaction(documentId)\n  const { provides: exportScope } = useExport(documentId)\n  const { provides: print } = usePrint(documentId)\n  const { provides: formCapability } = useFormCapability()\n  const { provides: selectionCapability } = useSelectionCapability()\n  const [mode, setModeState] = React.useState<PdfEditorMode>(defaultMode)\n  const [rightPanel, setRightPanel] =\n    React.useState<PdfEditorRightPanel | null>(null)\n  const [dialog, setDialog] = React.useState<PdfEditorDialogRequest | null>(\n    null\n  )\n  const [searchOpen, setSearchOpen] = React.useState(false)\n  const [historyState, setHistoryState] = React.useState({\n    canUndo: false,\n    canRedo: false,\n  })\n  const [horizontalScroll, setHorizontalScroll] = React.useState(false)\n  const [isDownloading, setIsDownloading] = React.useState(false)\n  const rootRef = React.useRef<HTMLDivElement | null>(null)\n  const [toastContainer, setToastContainer] =\n    React.useState<HTMLDivElement | null>(null)\n  const [editorToastManager] = React.useState(() =>\n    ToastPrimitive.createToastManager()\n  )\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null)\n  const uploadInputRef = React.useRef<HTMLInputElement>(null)\n  const annotationImportInputRef = React.useRef<HTMLInputElement>(null)\n  const [shellRef, shellWidth] = useElementWidth<HTMLDivElement>()\n  const leftInline = useInlineThumbnailSidebar(shellWidth)\n  const rightInline = shellWidth >= RIGHT_PANEL_INLINE_MIN_WIDTH\n  const compactToolbar = shellWidth > 0 && shellWidth <= TOOLBAR_STACK_MAX_WIDTH\n  const activePage = scrollState.currentPage\n  const numPages = pdfDocument?.pageCount ?? 0\n  const controlsDisabled = !numPages\n  const currentZoom = zoomState.currentZoomLevel\n  const selectedAnnotations = React.useMemo(\n    () => getSelectedAnnotations(annotationState),\n    [annotationState]\n  )\n  const activeToolId = annotationState.activeToolId\n  const activeTool = React.useMemo(\n    () =>\n      activeToolId && annotationCapability\n        ? annotationCapability.getTool(activeToolId)\n        : null,\n    [activeToolId, annotationCapability]\n  )\n  const historyScope = React.useMemo(\n    () => historyCapability?.forDocument(documentId) ?? null,\n    [documentId, historyCapability]\n  )\n  const formScope = React.useMemo(\n    () => formCapability?.forDocument(documentId) ?? null,\n    [documentId, formCapability]\n  )\n  const selectionScope = React.useMemo(\n    () => selectionCapability?.forDocument(documentId) ?? null,\n    [documentId, selectionCapability]\n  )\n  /* ---- notices ---------------------------------------------------------- */\n  const notify = React.useCallback(\n    (message: string, tone: PdfEditorNoticeTone = \"info\") => {\n      if (onToast) {\n        onToast({ message, tone })\n        return\n      }\n      editorToastManager.add({ title: message, type: tone })\n    },\n    [editorToastManager, onToast]\n  )\n  const setRootElement = React.useCallback((element: HTMLDivElement | null) => {\n    rootRef.current = element\n    setToastContainer(element)\n  }, [])\n  /* ---- tool helpers ----------------------------------------------------- */\n  const deactivateTools = React.useCallback(() => {\n    annotation?.setActiveTool(null)\n    if (redactionState.isRedacting) redaction?.endRedact()\n    if (isPanning) pan?.disablePan()\n    if (captureState.isMarqueeCaptureActive) capture?.disableMarqueeCapture()\n    if (zoomState.isMarqueeZoomActive) zoom?.disableMarqueeZoom()\n  }, [\n    annotation,\n    capture,\n    captureState.isMarqueeCaptureActive,\n    isPanning,\n    pan,\n    redaction,\n    redactionState.isRedacting,\n    zoom,\n    zoomState.isMarqueeZoomActive,\n  ])\n  const setActiveTool = React.useCallback(\n    (toolId: string | null) => {\n      if (!annotation) return\n      if (isPanning) pan?.disablePan()\n      if (captureState.isMarqueeCaptureActive) capture?.disableMarqueeCapture()\n      if (zoomState.isMarqueeZoomActive) zoom?.disableMarqueeZoom()\n      if (redactionState.isRedacting) redaction?.endRedact()\n      annotation.setActiveTool(\n        toolId !== null && annotationState.activeToolId === toolId\n          ? null\n          : toolId\n      )\n    },\n    [\n      annotation,\n      annotationState.activeToolId,\n      capture,\n      captureState.isMarqueeCaptureActive,\n      isPanning,\n      pan,\n      redaction,\n      redactionState.isRedacting,\n      zoom,\n      zoomState.isMarqueeZoomActive,\n    ]\n  )\n  const setMode = React.useCallback(\n    (nextMode: PdfEditorMode) => {\n      setModeState((previous) => {\n        if (previous === nextMode) return previous\n        onModeChange?.(nextMode)\n        return nextMode\n      })\n      const defaultPanel: Record<PdfEditorMode, PdfEditorRightPanel | null> = {\n        view: null,\n        annotate: \"properties\",\n        sign: \"signatures\",\n        forms: \"forms\",\n        redact: \"redactions\",\n      }\n      const panel = defaultPanel[nextMode]\n      if (panel) setRightPanel(panel)\n      else setRightPanel(null)\n    },\n    [onModeChange]\n  )\n  React.useEffect(() => {\n    deactivateTools()\n    annotation?.deselectAnnotation()\n    // Only react to mode changes here; tool state is otherwise managed by the toolbar.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mode])\n  React.useEffect(() => {\n    annotation?.setLocked(\n      mode === \"forms\" && formDesignMode\n        ? { type: LockModeType.None }\n        : { type: LockModeType.Include, categories: [\"form\"] }\n    )\n  }, [annotation, formDesignMode, mode])\n  React.useEffect(() => {\n    if (mode !== \"forms\" && formDesignMode) setFormDesignMode(false)\n  }, [formDesignMode, mode, setFormDesignMode])\n  /* ---- history ---------------------------------------------------------- */\n  React.useEffect(() => {\n    if (!historyScope) return\n    const sync = () => {\n      const state = historyScope.getHistoryState()\n      setHistoryState({\n        canUndo: state.global.canUndo,\n        canRedo: state.global.canRedo,\n      })\n    }\n    sync()\n    return historyScope.onHistoryChange(sync)\n  }, [historyScope])\n  /* ---- callbacks: page, annotations, forms, redactions ------------------ */\n  React.useEffect(() => {\n    if (!controlsDisabled && activePage > 0) onActivePageChange?.(activePage)\n  }, [activePage, controlsDisabled, onActivePageChange])\n  const onAnnotationEventRef = React.useRef(onAnnotationEvent)\n  const onAnnotationsChangeRef = React.useRef(onAnnotationsChange)\n  const onFormValuesChangeRef = React.useRef(onFormValuesChange)\n  const onRedactionsAppliedRef = React.useRef(onRedactionsApplied)\n  React.useEffect(() => {\n    onAnnotationEventRef.current = onAnnotationEvent\n    onAnnotationsChangeRef.current = onAnnotationsChange\n    onFormValuesChangeRef.current = onFormValuesChange\n    onRedactionsAppliedRef.current = onRedactionsApplied\n  })\n  React.useEffect(() => {\n    if (!annotation) return\n    let timeout: number | null = null\n    const unsubscribe = annotation.onAnnotationEvent((event) => {\n      onAnnotationEventRef.current?.(event)\n      if (!onAnnotationsChangeRef.current) return\n      if (event.type === \"loaded\" || !event.committed) return\n      if (timeout !== null) window.clearTimeout(timeout)\n      timeout = window.setTimeout(() => {\n        timeout = null\n        annotation.exportAnnotations().wait((items) => {\n          onAnnotationsChangeRef.current?.(items)\n        }, noop)\n      }, 400)\n    })\n    return () => {\n      if (timeout !== null) window.clearTimeout(timeout)\n      unsubscribe()\n    }\n  }, [annotation])\n  React.useEffect(() => {\n    if (!formScope) return\n    return formScope.onFieldValueChange(() => {\n      onFormValuesChangeRef.current?.(formScope.getFormValues())\n    })\n  }, [formScope])\n  React.useEffect(() => {\n    if (!redaction) return\n    return redaction.onRedactionEvent((event) => {\n      if (event.type !== \"commit\") return\n      if (event.success) {\n        notify(\n          \"Redactions applied. The content was removed from the document.\",\n          \"success\"\n        )\n        onRedactionsAppliedRef.current?.()\n      } else {\n        notify(\"The redactions could not be applied.\", \"error\")\n      }\n    })\n  }, [notify, redaction])\n  /* ---- initial zoom (numeric defaults never lift the viewport gate) ----- */\n  const initialZoomDocumentRef = React.useRef<string | null>(null)\n  React.useEffect(() => {\n    if (!pdfDocument || !zoom) return\n    if (initialZoomDocumentRef.current === documentId) return\n    initialZoomDocumentRef.current = documentId\n    zoom.requestZoom(toZoomLevel(defaultZoom))\n  }, [defaultZoom, documentId, pdfDocument, zoom])\n  /* ---- document actions ------------------------------------------------- */\n  const scrollToPage = React.useCallback(\n    (\n      pageNumber: number,\n      options?:\n        | {\n            smooth?: boolean\n          }\n        | ScrollIntoViewOptions\n    ) => {\n      const smooth =\n        options && \"smooth\" in options\n          ? options.smooth\n          : options && \"behavior\" in options\n            ? options.behavior === \"smooth\"\n            : false\n      scroll?.scrollToPage({ pageNumber, behavior: smooth ? \"smooth\" : \"auto\" })\n    },\n    [scroll]\n  )\n  const scrollToAnnotation = React.useCallback(\n    (target: PdfAnnotationObject) => {\n      scroll?.scrollToPage({\n        pageNumber: target.pageIndex + 1,\n        pageCoordinates: { x: target.rect.origin.x, y: target.rect.origin.y },\n        alignX: 50,\n        alignY: 25,\n        behavior: \"smooth\",\n      })\n    },\n    [scroll]\n  )\n  const getDocumentBuffer = React.useCallback(async () => {\n    if (!exportScope) throw new Error(\"The document is not ready.\")\n    if (annotation && annotationState.hasPendingChanges) {\n      try {\n        await annotation.commit().toPromise()\n      } catch {\n        // Continue with whatever the engine currently holds.\n      }\n    }\n    return exportScope.saveAsCopy().toPromise()\n  }, [annotation, annotationState.hasPendingChanges, exportScope])\n  const downloadDocument = React.useCallback(\n    async (name?: string) => {\n      const targetName = getPdfFileName(name ?? fileName)\n      setIsDownloading(true)\n      try {\n        const buffer = await getDocumentBuffer()\n        downloadArrayBuffer(buffer, targetName)\n        onSave?.({ buffer, fileName: targetName })\n      } catch (error) {\n        console.error(error)\n        notify(\"The document could not be saved.\", \"error\")\n      } finally {\n        setIsDownloading(false)\n      }\n    },\n    [fileName, getDocumentBuffer, notify, onSave]\n  )\n  const flattenDocument = React.useCallback(() => {\n    setDialog({\n      type: \"confirm\",\n      title: \"Flatten annotations and form fields?\",\n      description:\n        \"Annotations and filled form fields become part of the page content and can no longer be edited.\",\n      confirmLabel: \"Flatten\",\n      destructive: true,\n      onConfirm: async () => {\n        if (!registry || !pdfDocument) return\n        try {\n          if (annotation && annotationState.hasPendingChanges) {\n            await annotation.commit().toPromise()\n          }\n          const engine = registry.getEngine()\n          for (const page of pdfDocument.pages) {\n            await engine\n              .flattenPage(pdfDocument, page, {\n                flag: PdfPageFlattenFlag.Display,\n              })\n              .toPromise()\n          }\n          const buffer = await engine.saveAsCopy(pdfDocument).toPromise()\n          onReplaceDocument(buffer, fileName)\n          notify(\"Annotations flattened into the page content.\", \"success\")\n        } catch (error) {\n          console.error(error)\n          notify(\"The document could not be flattened.\", \"error\")\n        }\n      },\n    })\n  }, [\n    annotation,\n    annotationState.hasPendingChanges,\n    fileName,\n    notify,\n    onReplaceDocument,\n    pdfDocument,\n    registry,\n  ])\n  const exportAnnotationsJson = React.useCallback(() => {\n    if (!annotation) return\n    annotation.exportAnnotations().wait(\n      (items) => {\n        downloadBlob(\n          new Blob([JSON.stringify(serializeAnnotationItems(items), null, 2)], {\n            type: \"application/json\",\n          }),\n          `${fileName.replace(/\\.pdf$/i, \"\")}-annotations.json`\n        )\n        notify(\n          `Exported ${items.length} annotation${items.length === 1 ? \"\" : \"s\"}.`,\n          \"success\"\n        )\n      },\n      () => notify(\"The annotations could not be exported.\", \"error\")\n    )\n  }, [annotation, fileName, notify])\n  const importAnnotationsFile = React.useCallback(\n    async (file: File) => {\n      if (!annotation) return\n      try {\n        const items = deserializeAnnotationItems(JSON.parse(await file.text()))\n        annotation.importAnnotations(items)\n        notify(\n          `Imported ${items.length} annotation${items.length === 1 ? \"\" : \"s\"}.`,\n          \"success\"\n        )\n      } catch {\n        notify(\"The file is not a valid annotation export.\", \"error\")\n      }\n    },\n    [annotation, notify]\n  )\n  const deleteSelectedAnnotations = React.useCallback(() => {\n    if (!annotation || selectedAnnotations.length === 0) return\n    const deletable = selectedAnnotations.filter(\n      (item) => !annotation.isAnnotationStructurallyLocked(item.object)\n    )\n    if (deletable.length === 0) return\n    annotation.deleteAnnotations(\n      deletable.map((item) => ({\n        pageIndex: item.object.pageIndex,\n        id: item.object.id,\n      }))\n    )\n  }, [annotation, selectedAnnotations])\n  const applyAllRedactions = React.useCallback(() => {\n    if (!redaction || redactionState.pendingCount === 0) return\n    setDialog({\n      type: \"confirm\",\n      title: `Apply ${redactionState.pendingCount} redaction${redactionState.pendingCount === 1 ? \"\" : \"s\"}?`,\n      description:\n        \"Redaction permanently removes the marked content from the document. This cannot be undone.\",\n      confirmLabel: \"Apply redactions\",\n      onConfirm: () => {\n        redaction.commitAllPending().wait(noop, noop)\n      },\n    })\n  }, [redaction, redactionState.pendingCount])\n  /* ---- imperative handle ------------------------------------------------ */\n  React.useEffect(() => {\n    if (controlsDisabled) return\n    handleRef.current = {\n      getDocumentBuffer,\n      download: downloadDocument,\n      print: async (options) => {\n        if (!print) throw new Error(\"Printing is not available.\")\n        await print.print(options).toPromise()\n      },\n      exportAnnotations: async () => {\n        if (!annotation) return []\n        return annotation.exportAnnotations().toPromise()\n      },\n      importAnnotations: (items) => annotation?.importAnnotations(items),\n      getFormValues: () => formScope?.getFormValues() ?? {},\n      setFormValues: async (values) => {\n        if (!formScope) return\n        await formScope.setFormValues(values).toPromise()\n      },\n      applyRedactions: async () => {\n        if (!redaction) return\n        await redaction.commitAllPending().toPromise()\n      },\n      setMode,\n      setActiveTool: (toolId) => annotation?.setActiveTool(toolId),\n      scrollToPage: (pageNumber, options) => scrollToPage(pageNumber, options),\n      undo: () => historyScope?.undo(),\n      redo: () => historyScope?.redo(),\n      getViewportElement: () => viewportElementRef.current,\n    }\n    return () => {\n      handleRef.current = null\n    }\n  }, [\n    annotation,\n    controlsDisabled,\n    downloadDocument,\n    formScope,\n    getDocumentBuffer,\n    handleRef,\n    historyScope,\n    print,\n    redaction,\n    scrollToPage,\n    setMode,\n  ])\n  /* ---- keyboard shortcuts ----------------------------------------------- */\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (controlsDisabled) return\n      const key = event.key\n      const mod = event.metaKey || event.ctrlKey\n      if (isTypingTarget(event.target)) {\n        if (key === \"Escape\") (event.target as HTMLElement).blur()\n        return\n      }\n      if (key === \"Escape\") {\n        deactivateTools()\n        annotation?.deselectAnnotation()\n        selectionScope?.clear()\n        setSearchOpen(false)\n        event.preventDefault()\n        return\n      }\n      if (\n        (key === \"Delete\" || key === \"Backspace\") &&\n        selectedAnnotations.length > 0\n      ) {\n        deleteSelectedAnnotations()\n        event.preventDefault()\n        return\n      }\n      if (mod && key.toLowerCase() === \"z\") {\n        if (event.shiftKey) historyScope?.redo()\n        else historyScope?.undo()\n        event.preventDefault()\n        return\n      }\n      if (mod && key.toLowerCase() === \"y\") {\n        historyScope?.redo()\n        event.preventDefault()\n        return\n      }\n      if (mod && (key === \"=\" || key === \"+\")) {\n        zoom?.zoomIn()\n        event.preventDefault()\n        return\n      }\n      if (mod && key === \"-\") {\n        zoom?.zoomOut()\n        event.preventDefault()\n        return\n      }\n      if (mod && key === \"0\") {\n        zoom?.requestZoom(ZoomMode.FitPage)\n        event.preventDefault()\n        return\n      }\n      if (mod && key.toLowerCase() === \"p\" && features.print) {\n        setDialog({ type: \"print\" })\n        event.preventDefault()\n        return\n      }\n      if (mod && key.toLowerCase() === \"s\" && showDownload) {\n        void downloadDocument()\n        event.preventDefault()\n        return\n      }\n      if (mod && key.toLowerCase() === \"f\") {\n        setSearchOpen(true)\n        event.preventDefault()\n        return\n      }\n      if (mod && key.toLowerCase() === \"a\" && annotation && mode !== \"view\") {\n        const ids = annotationState.pages[Math.max(0, activePage - 1)] ?? []\n        if (ids.length > 0) {\n          annotation.setSelection(ids)\n          event.preventDefault()\n        }\n        return\n      }\n      if (!mod && !event.altKey && key.toLowerCase() === \"h\") {\n        pan?.togglePan()\n        event.preventDefault()\n        return\n      }\n      if (!mod && !event.altKey && key.toLowerCase() === \"v\") {\n        deactivateTools()\n        event.preventDefault()\n      }\n    },\n    [\n      activePage,\n      annotation,\n      annotationState.pages,\n      controlsDisabled,\n      deactivateTools,\n      deleteSelectedAnnotations,\n      downloadDocument,\n      features.print,\n      historyScope,\n      mode,\n      pan,\n      selectedAnnotations.length,\n      selectionScope,\n      showDownload,\n      zoom,\n    ]\n  )\n  const handlePointerDownCapture = React.useCallback(\n    (event: React.PointerEvent<HTMLDivElement>) => {\n      const root = rootRef.current\n      if (!root) return\n      const active = window.document.activeElement\n      if (active && root.contains(active) && active !== root) return\n      if (!(event.target instanceof Node) || !root.contains(event.target))\n        return\n      if (isTypingTarget(event.target)) return\n      root.focus({ preventScroll: true })\n    },\n    []\n  )\n  /* ---- context ---------------------------------------------------------- */\n  const openDialog = React.useCallback(\n    (request: PdfEditorDialogRequest) => setDialog(request),\n    []\n  )\n  const toggleLeftPanel = React.useCallback(\n    (panel: PdfEditorLeftPanel) =>\n      setLeftPanel((previous) => (previous === panel ? null : panel)),\n    [setLeftPanel]\n  )\n  const toggleRightPanel = React.useCallback(\n    (panel: PdfEditorRightPanel) =>\n      setRightPanel((previous) => (previous === panel ? null : panel)),\n    []\n  )\n  const contextValue = React.useMemo<PdfEditorContextValue>(\n    () => ({\n      documentId,\n      document: pdfDocument,\n      fileName,\n      mode,\n      setMode,\n      leftPanel,\n      setLeftPanel,\n      toggleLeftPanel,\n      rightPanel,\n      setRightPanel,\n      toggleRightPanel,\n      openDialog,\n      notify,\n      features,\n      permissions,\n      annotationAuthor,\n      selectionActions,\n      formDesignMode,\n      setFormDesignMode,\n      replaceDocument: (buffer, name) =>\n        onReplaceDocument(buffer, name ?? fileName),\n      downloadDocument,\n      getDocumentBuffer,\n      scrollToPage,\n      scrollToAnnotation,\n      onCapture,\n      onSelectionAction,\n      signatureFontsStylesheetUrl,\n    }),\n    [\n      annotationAuthor,\n      documentId,\n      downloadDocument,\n      features,\n      fileName,\n      formDesignMode,\n      getDocumentBuffer,\n      leftPanel,\n      mode,\n      notify,\n      onCapture,\n      onReplaceDocument,\n      onSelectionAction,\n      openDialog,\n      pdfDocument,\n      permissions,\n      rightPanel,\n      scrollToAnnotation,\n      scrollToPage,\n      selectionActions,\n      setFormDesignMode,\n      setLeftPanel,\n      setMode,\n      signatureFontsStylesheetUrl,\n      toggleLeftPanel,\n      toggleRightPanel,\n    ]\n  )\n  /* ---- page renderer ---------------------------------------------------- */\n  const renderPage = React.useCallback(\n    (page: PageLayout) => (\n      <Rotate documentId={documentId} pageIndex={page.pageIndex}>\n        <PagePointerProvider\n          documentId={documentId}\n          pageIndex={page.pageIndex}\n          data-pdf-editor-page={page.pageNumber}\n          className=\"relative border border-transparent bg-transparent shadow-xs select-none selection:bg-transparent selection:text-inherit\"\n          style={{ backgroundColor: \"transparent\" }}\n        >\n          <div\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 bg-white\"\n          />\n          <RenderLayer\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            scale={Math.min(currentZoom, PAGE_BASE_RENDER_MAX_SCALE)}\n            dpr={PAGE_BASE_RENDER_DPR}\n            className=\"pointer-events-none absolute inset-0 h-full w-full object-fill opacity-100 blur-[0.35px] transition-none\"\n          />\n          <TilingLayer\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            className=\"pointer-events-none opacity-100 transition-none [&_img]:opacity-100 [&_img]:transition-none\"\n          />\n          <SearchLayer\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            className=\"pointer-events-none\"\n            highlightColor=\"rgba(253, 224, 71, 0.45)\"\n            activeHighlightColor=\"rgba(249, 115, 22, 0.55)\"\n          />\n          <MarqueeZoom documentId={documentId} pageIndex={page.pageIndex} />\n          <MarqueeCapture documentId={documentId} pageIndex={page.pageIndex} />\n          <SelectionLayer\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            textStyle={{ background: \"rgba(59, 130, 246, 0.3)\" }}\n            selectionMenu={(props) => (\n              <PdfEditorTextSelectionMenu {...props} documentId={documentId} />\n            )}\n          />\n          <AnnotationLayer\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            selectionOutline={{\n              color: PDF_EDITOR_SELECTION_COLOR,\n              style: \"solid\",\n              width: 1,\n              offset: 2,\n            }}\n            groupSelectionOutline={{\n              color: PDF_EDITOR_SELECTION_COLOR,\n              style: \"dashed\",\n              width: 1,\n              offset: 3,\n            }}\n            resizeUI={{ size: 10, color: PDF_EDITOR_SELECTION_COLOR }}\n            vertexUI={{ size: 10, color: PDF_EDITOR_SELECTION_COLOR }}\n            rotationUI={{\n              size: 18,\n              color: \"#ffffff\",\n              iconColor: PDF_EDITOR_SELECTION_COLOR,\n              connectorColor: PDF_EDITOR_SELECTION_COLOR,\n              showConnector: true,\n            }}\n            selectionMenu={(props) => (\n              <PdfEditorAnnotationMenu {...props} documentId={documentId} />\n            )}\n            groupSelectionMenu={(props) => (\n              <PdfEditorGroupMenu {...props} documentId={documentId} />\n            )}\n          />\n          <RedactionLayer\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            selectionMenu={(props) => (\n              <PdfEditorRedactionMenu {...props} documentId={documentId} />\n            )}\n          />\n          {renderPageOverlay?.({\n            pageNumber: page.pageNumber,\n            pageWidth: page.width,\n            pageHeight: page.height,\n            scale: currentZoom,\n            rotation: 0,\n          })}\n        </PagePointerProvider>\n      </Rotate>\n    ),\n    [currentZoom, documentId, renderPageOverlay]\n  )\n  /* ---- toolbar pieces --------------------------------------------------- */\n  const activeToolColorKey = React.useMemo(() => {\n    if (!activeTool) return null\n    const defaults = activeTool.defaults as Record<string, unknown>\n    if (\"fontColor\" in defaults) return \"fontColor\"\n    if (\"strokeColor\" in defaults) return \"strokeColor\"\n    if (\"color\" in defaults) return \"color\"\n    return null\n  }, [activeTool])\n  const activeToolColor = activeToolColorKey\n    ? String(\n        (activeTool?.defaults as Record<string, unknown>)[activeToolColorKey] ??\n          \"#111827\"\n      )\n    : null\n  const visibleLeftTabs = LEFT_PANEL_TABS.filter(\n    (tab) => tab.feature === null || features[tab.feature]\n  )\n  const visibleRightTabs = RIGHT_PANEL_TABS.filter(\n    (tab) => tab.feature === null || features[tab.feature]\n  )\n  const canAnnotate = features.annotate && permissions.canModifyAnnotations\n  const canRedact = features.redact && permissions.canModifyContents\n  const canDesignForms = features.forms && permissions.canModifyAnnotations\n  const selectToolButton = (\n    <PdfEditorToolButton\n      label=\"Select\"\n      shortcut=\"V\"\n      active={\n        !activeToolId &&\n        !isPanning &&\n        !captureState.isMarqueeCaptureActive &&\n        !zoomState.isMarqueeZoomActive &&\n        !redactionState.isRedacting\n      }\n      onClick={deactivateTools}\n    >\n      <CursorGlyph className=\"size-4\" />\n    </PdfEditorToolButton>\n  )\n  const renderToolButton = (\n    tool: {\n      id: string\n      label: string\n      glyph: PdfEditorGlyph\n      hint?: string\n    },\n    disabled: boolean\n  ) => {\n    const Glyph = tool.glyph\n    return (\n      <PdfEditorToolButton\n        key={tool.id}\n        label={tool.hint ? `${tool.label} · ${tool.hint}` : tool.label}\n        active={activeToolId === tool.id}\n        disabled={disabled}\n        onClick={() => setActiveTool(tool.id)}\n      >\n        <Glyph className=\"size-4\" />\n      </PdfEditorToolButton>\n    )\n  }\n  let ribbon: React.ReactNode = null\n  if (mode === \"view\") {\n    ribbon = (\n      <>\n        {selectToolButton}\n        <PdfEditorToolButton\n          label=\"Hand tool\"\n          shortcut=\"H\"\n          active={isPanning}\n          onClick={() => pan?.togglePan()}\n        >\n          <HandGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Zoom to area\"\n          active={zoomState.isMarqueeZoomActive}\n          onClick={() => zoom?.toggleMarqueeZoom()}\n        >\n          <MarqueeZoomGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        {features.capture ? (\n          <PdfEditorToolButton\n            label=\"Capture area as image\"\n            active={captureState.isMarqueeCaptureActive}\n            onClick={() => capture?.toggleMarqueeCapture()}\n          >\n            <CameraGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n        ) : null}\n        <PdfEditorToolbarSeparator />\n        <Select\n          items={SPREAD_MODE_OPTIONS}\n          value={spreadMode}\n          disabled={controlsDisabled}\n          modal={false}\n          onValueChange={(value) =>\n            spread?.setSpreadMode(String(value) as SpreadMode)\n          }\n        >\n          <SelectTrigger\n            size=\"sm\"\n            className=\"w-[132px]\"\n            aria-label=\"Page layout\"\n          >\n            <BookOpenGlyph className=\"size-4 text-muted-foreground\" />\n            <SelectValue placeholder=\"Layout\" />\n          </SelectTrigger>\n          <SelectContent alignItemWithTrigger={false}>\n            {SPREAD_MODE_OPTIONS.map((option) => (\n              <SelectItem key={option.value} value={option.value}>\n                {option.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n        <Select\n          items={SCROLL_DIRECTION_OPTIONS}\n          value={horizontalScroll ? \"horizontal\" : \"vertical\"}\n          disabled={controlsDisabled}\n          modal={false}\n          onValueChange={(value) => {\n            const horizontal = value === \"horizontal\"\n            setHorizontalScroll(horizontal)\n            scrollCapability?.setScrollStrategy(\n              horizontal ? ScrollStrategy.Horizontal : ScrollStrategy.Vertical,\n              documentId\n            )\n          }}\n        >\n          <SelectTrigger\n            size=\"sm\"\n            className=\"w-[128px]\"\n            aria-label=\"Scroll direction\"\n          >\n            <SelectValue placeholder=\"Scroll\" />\n          </SelectTrigger>\n          <SelectContent alignItemWithTrigger={false}>\n            {SCROLL_DIRECTION_OPTIONS.map((option) => (\n              <SelectItem key={option.value} value={option.value}>\n                {option.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      </>\n    )\n  } else if (mode === \"annotate\") {\n    ribbon = (\n      <>\n        {selectToolButton}\n        {PDF_EDITOR_ANNOTATION_TOOL_GROUPS.map((group) => (\n          <React.Fragment key={group.id}>\n            <PdfEditorToolbarSeparator />\n            {group.tools.map((tool) => renderToolButton(tool, !canAnnotate))}\n          </React.Fragment>\n        ))}\n        {features.stamps ? (\n          <PdfEditorToolButton\n            label=\"Rubber stamps\"\n            active={rightPanel === \"stamps\"}\n            disabled={!canAnnotate}\n            onClick={() => toggleRightPanel(\"stamps\")}\n          >\n            <StampGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n        ) : null}\n        {activeTool && activeToolColorKey && activeToolColor ? (\n          <>\n            <PdfEditorToolbarSeparator />\n            <ColorPicker\n              label={`${activeTool.name} color`}\n              icon={PaletteGlyph}\n              color={activeToolColor}\n              onChange={(color) =>\n                annotationCapability?.setToolDefaults(activeTool.id, {\n                  [activeToolColorKey]: color,\n                })\n              }\n            />\n          </>\n        ) : null}\n        <div className=\"ml-auto flex items-center gap-0.5\">\n          <PdfEditorToolButton\n            label=\"Delete selected\"\n            shortcut=\"Delete\"\n            disabled={selectedAnnotations.length === 0}\n            onClick={deleteSelectedAnnotations}\n          >\n            <TrashGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n          {features.comments ? (\n            <PdfEditorToolButton\n              label=\"Comments\"\n              active={rightPanel === \"comments\"}\n              onClick={() => toggleRightPanel(\"comments\")}\n            >\n              <CommentsGlyph className=\"size-4\" />\n            </PdfEditorToolButton>\n          ) : null}\n          <PdfEditorToolButton\n            label=\"Properties\"\n            active={rightPanel === \"properties\"}\n            onClick={() => toggleRightPanel(\"properties\")}\n          >\n            <SlidersGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n        </div>\n      </>\n    )\n  } else if (mode === \"sign\") {\n    ribbon = (\n      <>\n        {selectToolButton}\n        <PdfEditorToolbarSeparator />\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          disabled={!canAnnotate}\n          onClick={() => setDialog({ type: \"signature\" })}\n        >\n          <PlusGlyph className=\"size-4\" />\n          Create signature\n        </Button>\n        <PdfEditorToolButton\n          label=\"Saved signatures\"\n          active={rightPanel === \"signatures\"}\n          onClick={() => toggleRightPanel(\"signatures\")}\n        >\n          <SignatureGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolbarSeparator />\n        {renderToolButton(\n          {\n            id: \"freeText\",\n            label: \"Text box\",\n            glyph: TextGlyph,\n            hint: \"Click to add text\",\n          },\n          !canAnnotate\n        )}\n        <PdfEditorToolButton\n          label=\"Date · Click to add today's date\"\n          disabled={!canAnnotate}\n          active={\n            activeToolId === \"freeText\" &&\n            Boolean(annotationState.activeToolContext?.date)\n          }\n          onClick={() => {\n            if (!annotation) return\n            annotationCapability?.setToolDefaults(\"freeText\", {\n              contents: new Date().toLocaleDateString(),\n            })\n            annotation.setActiveTool(\"freeText\", { date: true })\n          }}\n        >\n          <FileTextGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        {renderToolButton(\n          { id: \"ink\", label: \"Draw\", glyph: EditGlyph, hint: \"Sign by hand\" },\n          !canAnnotate\n        )}\n      </>\n    )\n  } else if (mode === \"forms\") {\n    ribbon = (\n      <>\n        {selectToolButton}\n        <PdfEditorToolbarSeparator />\n        <div\n          role=\"group\"\n          aria-label=\"Form mode\"\n          className=\"flex shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5\"\n        >\n          <Button\n            type=\"button\"\n            size=\"xs\"\n            variant={formDesignMode ? \"ghost\" : \"default\"}\n            aria-pressed={!formDesignMode}\n            onClick={() => setFormDesignMode(false)}\n          >\n            Fill\n          </Button>\n          <Button\n            type=\"button\"\n            size=\"xs\"\n            variant={formDesignMode ? \"default\" : \"ghost\"}\n            aria-pressed={formDesignMode}\n            disabled={!canDesignForms}\n            onClick={() => setFormDesignMode(true)}\n          >\n            Design\n          </Button>\n        </div>\n        {formDesignMode ? (\n          <>\n            <PdfEditorToolbarSeparator />\n            {PDF_EDITOR_FORM_TOOLS.map((tool) =>\n              renderToolButton(tool, !canDesignForms)\n            )}\n          </>\n        ) : null}\n        <div className=\"ml-auto flex items-center gap-0.5\">\n          <PdfEditorToolButton\n            label=\"Delete selected\"\n            shortcut=\"Delete\"\n            disabled={selectedAnnotations.length === 0 || !formDesignMode}\n            onClick={deleteSelectedAnnotations}\n          >\n            <TrashGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n          <PdfEditorToolButton\n            label=\"Form fields\"\n            active={rightPanel === \"forms\"}\n            onClick={() => toggleRightPanel(\"forms\")}\n          >\n            <TextFieldGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n          <PdfEditorToolButton\n            label=\"Properties\"\n            active={rightPanel === \"properties\"}\n            onClick={() => toggleRightPanel(\"properties\")}\n          >\n            <SlidersGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n        </div>\n      </>\n    )\n  } else if (mode === \"redact\") {\n    const activeType = redactionState.activeType\n    const pendingCount = redactionState.pendingCount\n    ribbon = (\n      <>\n        {selectToolButton}\n        <PdfEditorToolbarSeparator />\n        <PdfEditorToolButton\n          label=\"Redact · Select text or drag an area\"\n          active={activeType === RedactionMode.Redact}\n          disabled={!canRedact}\n          onClick={() => {\n            deactivateTools()\n            if (activeType !== RedactionMode.Redact) redaction?.enableRedact()\n          }}\n        >\n          <EyeOffGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Mark text\"\n          active={activeType === RedactionMode.RedactSelection}\n          disabled={!canRedact}\n          onClick={() => {\n            deactivateTools()\n            if (activeType !== RedactionMode.RedactSelection)\n              redaction?.enableRedactSelection()\n          }}\n        >\n          <StrikethroughGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Mark area\"\n          active={activeType === RedactionMode.MarqueeRedact}\n          disabled={!canRedact}\n          onClick={() => {\n            deactivateTools()\n            if (activeType !== RedactionMode.MarqueeRedact)\n              redaction?.enableMarqueeRedact()\n          }}\n        >\n          <RedactAreaGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolbarSeparator />\n        <Badge\n          variant={pendingCount > 0 ? \"secondary\" : \"outline\"}\n          className=\"me-1 tabular-nums\"\n        >\n          {pendingCount} pending\n        </Badge>\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          disabled={!canRedact || pendingCount === 0}\n          onClick={applyAllRedactions}\n        >\n          Apply all\n        </Button>\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"ghost\"\n          disabled={pendingCount === 0}\n          onClick={() => redaction?.clearPending()}\n        >\n          Clear\n        </Button>\n        <div className=\"ml-auto flex items-center gap-0.5\">\n          <PdfEditorToolButton\n            label=\"Redactions\"\n            active={rightPanel === \"redactions\"}\n            onClick={() => toggleRightPanel(\"redactions\")}\n          >\n            <EyeOffGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n          <PdfEditorToolButton\n            label=\"Properties\"\n            active={rightPanel === \"properties\"}\n            onClick={() => toggleRightPanel(\"properties\")}\n          >\n            <SlidersGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n        </div>\n      </>\n    )\n  }\n  let rightPanelContent: React.ReactNode = null\n  switch (rightPanel) {\n    case \"properties\":\n      rightPanelContent = <PdfEditorPropertiesPanel documentId={documentId} />\n      break\n    case \"comments\":\n      rightPanelContent = <PdfEditorCommentsPanel documentId={documentId} />\n      break\n    case \"redactions\":\n      rightPanelContent = <PdfEditorRedactionsPanel documentId={documentId} />\n      break\n    case \"stamps\":\n      rightPanelContent = <PdfEditorStampsPanel documentId={documentId} />\n      break\n    case \"signatures\":\n      rightPanelContent = <PdfEditorSignaturesPanel documentId={documentId} />\n      break\n    case \"forms\":\n      rightPanelContent = <PdfEditorFormsPanel documentId={documentId} />\n      break\n    default:\n      rightPanelContent = null\n  }\n  let leftPanelContent: React.ReactNode = null\n  switch (leftPanel) {\n    case \"thumbnails\":\n      leftPanelContent = (\n        <PdfEditorThumbnailsPanel\n          documentId={documentId}\n          activePage={activePage}\n          pageCount={numPages}\n          onSelectPage={(pageNumber) => scrollToPage(pageNumber)}\n        />\n      )\n      break\n    case \"outline\":\n      leftPanelContent = <PdfEditorOutlinePanel documentId={documentId} />\n      break\n    case \"attachments\":\n      leftPanelContent = <PdfEditorAttachmentsPanel documentId={documentId} />\n      break\n    case \"pages\":\n      leftPanelContent = (\n        <PdfEditorPagesPanel documentId={documentId} activePage={activePage} />\n      )\n      break\n    default:\n      leftPanelContent = null\n  }\n  return (\n    <PdfEditorContextProvider value={contextValue}>\n      <ToastProvider\n        limit={3}\n        timeout={NOTICE_DURATION_MS}\n        toastManager={editorToastManager}\n        portalProps={{ container: toastContainer }}\n        viewportProps={{ className: \"absolute\" }}\n      >\n        <div\n          data-slot=\"pdf-editor\"\n          data-mode={mode}\n          data-state={documentState}\n          aria-busy={documentState === \"loading\"}\n          data-fullscreen={fullscreenState.isFullscreen ? \"\" : undefined}\n          className={cn(\n            \"flex h-full max-h-full min-h-0 w-full flex-col overflow-hidden bg-background\",\n            className,\n            // The fullscreen plugin auto-mounts a wrapper around the EmbedPDF\n            // tree; when that wrapper is fullscreen, fill it instead of the\n            // consumer-provided height.\n            fullscreenState.isFullscreen && \"h-full max-h-full\"\n          )}\n        >\n          <div\n            ref={setRootElement}\n            tabIndex={-1}\n            className=\"relative flex h-full min-h-0 flex-1 flex-col outline-none\"\n            onKeyDown={handleKeyDown}\n            onPointerDownCapture={handlePointerDownCapture}\n          >\n            <input\n              ref={uploadInputRef}\n              type=\"file\"\n              accept=\"application/pdf,.pdf\"\n              className=\"sr-only\"\n              tabIndex={-1}\n              onChange={(event) => {\n                const file = event.target.files?.[0]\n                if (file) onUploadFile(file)\n                event.currentTarget.value = \"\"\n              }}\n            />\n            <input\n              ref={annotationImportInputRef}\n              type=\"file\"\n              accept=\"application/json,.json\"\n              className=\"sr-only\"\n              tabIndex={-1}\n              onChange={(event) => {\n                const file = event.target.files?.[0]\n                if (file) void importAnnotationsFile(file)\n                event.currentTarget.value = \"\"\n              }}\n            />\n\n            {showToolbar ? (\n              <TooltipProvider delay={TOOLTIP_DELAY_MS}>\n                <fieldset disabled={controlsDisabled} className=\"contents\">\n                  <div\n                    data-slot=\"pdf-editor-toolbar\"\n                    className={cn(\n                      \"flex min-h-12 flex-wrap items-center justify-between gap-2 border-b bg-background px-3 py-2\",\n                      compactToolbar && \"gap-0 px-0 py-0\"\n                    )}\n                  >\n                    <div\n                      className={cn(\n                        \"flex min-w-0 items-center gap-2\",\n                        compactToolbar && \"w-full px-3 py-2\"\n                      )}\n                    >\n                      <PdfEditorToolButton\n                        label=\"Pages sidebar\"\n                        active={leftPanel !== null}\n                        disabled={controlsDisabled}\n                        onClick={() => toggleLeftPanel(\"thumbnails\")}\n                      >\n                        <PanelLeftGlyph className=\"size-4\" />\n                      </PdfEditorToolButton>\n                      <PdfEditorPageControl\n                        activePage={activePage}\n                        numPages={numPages}\n                        disabled={controlsDisabled}\n                        onPageChange={(pageNumber) => scrollToPage(pageNumber)}\n                      />\n                    </div>\n                    <InlineScrollArea2\n                      orientation=\"horizontal\"\n                      scrollFade\n                      scrollbarOverflowOnly\n                      className={cn(\n                        \"min-w-0 flex-1\",\n                        compactToolbar && \"h-12 w-full flex-none border-t\"\n                      )}\n                      viewportClassName={cn(compactToolbar && \"px-3 py-2\")}\n                    >\n                      <div\n                        className={cn(\n                          \"flex w-max min-w-full items-center gap-1\",\n                          compactToolbar ? \"justify-start\" : \"justify-end\"\n                        )}\n                      >\n                        <div className=\"flex items-center gap-0.5\">\n                          <PdfEditorToolButton\n                            label=\"Undo\"\n                            shortcut=\"mod+Z\"\n                            disabled={!historyState.canUndo}\n                            onClick={() => historyScope?.undo()}\n                          >\n                            <UndoGlyph className=\"size-4\" />\n                          </PdfEditorToolButton>\n                          <PdfEditorToolButton\n                            label=\"Redo\"\n                            shortcut=\"mod+shift+Z\"\n                            disabled={!historyState.canRedo}\n                            onClick={() => historyScope?.redo()}\n                          >\n                            <RedoGlyph className=\"size-4\" />\n                          </PdfEditorToolButton>\n                        </div>\n                        <PdfEditorToolbarSeparator />\n                        <PdfEditorZoomControl\n                          documentId={documentId}\n                          disabled={controlsDisabled}\n                        />\n                        <PdfEditorToolbarSeparator />\n                        <div className=\"flex items-center gap-0.5\">\n                          <PdfEditorToolButton\n                            label=\"Rotate counterclockwise\"\n                            disabled={controlsDisabled}\n                            onClick={() => rotate?.rotateBackward()}\n                          >\n                            <RotateGlyph className=\"size-4 -scale-x-100\" />\n                          </PdfEditorToolButton>\n                          <PdfEditorToolButton\n                            label=\"Rotate clockwise\"\n                            disabled={controlsDisabled}\n                            onClick={() => rotate?.rotateForward()}\n                          >\n                            <RotateGlyph className=\"size-4\" />\n                          </PdfEditorToolButton>\n                        </div>\n                        <PdfEditorToolbarSeparator />\n                        <PdfEditorSearchControl\n                          documentId={documentId}\n                          disabled={controlsDisabled}\n                          open={searchOpen}\n                          onOpenChange={setSearchOpen}\n                        />\n                        <PdfEditorToolButton\n                          label=\"Details panel\"\n                          active={rightPanel !== null}\n                          disabled={controlsDisabled}\n                          onClick={() => toggleRightPanel(\"properties\")}\n                        >\n                          <PanelRightGlyph className=\"size-4\" />\n                        </PdfEditorToolButton>\n                        {toolbarActions ? (\n                          <>\n                            <PdfEditorToolbarSeparator />\n                            {toolbarActions}\n                          </>\n                        ) : null}\n                        <PdfEditorToolbarSeparator />\n                        {showDownload ? (\n                          <PdfEditorToolButton\n                            label=\"Download\"\n                            shortcut=\"mod+S\"\n                            disabled={controlsDisabled || isDownloading}\n                            onClick={() => void downloadDocument()}\n                          >\n                            {isDownloading ? (\n                              <InlineSpinner className=\"size-4\" />\n                            ) : (\n                              <DownloadGlyph className=\"size-4\" />\n                            )}\n                          </PdfEditorToolButton>\n                        ) : null}\n                        <DropdownMenu>\n                          <DropdownMenuTrigger\n                            render={\n                              <Button\n                                type=\"button\"\n                                variant=\"ghost\"\n                                size=\"icon-sm\"\n                                aria-label=\"More actions\"\n                              >\n                                <MoreGlyph className=\"size-4\" />\n                              </Button>\n                            }\n                          ></DropdownMenuTrigger>\n                          <DropdownMenuContent align=\"end\" className=\"w-60\">\n                            <DropdownMenuGroup>\n                              <DropdownMenuLabel className=\"truncate\">\n                                {fileName}\n                              </DropdownMenuLabel>\n                              {showUpload ? (\n                                <DropdownMenuItem\n                                  onClick={() =>\n                                    uploadInputRef.current?.click()\n                                  }\n                                >\n                                  <UploadGlyph className=\"size-4\" />\n                                  Open PDF…\n                                </DropdownMenuItem>\n                              ) : null}\n                              {showDownload ? (\n                                <DropdownMenuItem\n                                  disabled={controlsDisabled}\n                                  onClick={() => void downloadDocument()}\n                                >\n                                  <DownloadGlyph className=\"size-4\" />\n                                  Download\n                                  <DropdownMenuShortcut>\n                                    ⌘S\n                                  </DropdownMenuShortcut>\n                                </DropdownMenuItem>\n                              ) : null}\n                              {features.print ? (\n                                <DropdownMenuItem\n                                  disabled={\n                                    controlsDisabled || !permissions.canPrint\n                                  }\n                                  onClick={() => setDialog({ type: \"print\" })}\n                                >\n                                  <PrintGlyph className=\"size-4\" />\n                                  Print…\n                                  <DropdownMenuShortcut>\n                                    ⌘P\n                                  </DropdownMenuShortcut>\n                                </DropdownMenuItem>\n                              ) : null}\n                              {features.capture ? (\n                                <DropdownMenuItem\n                                  disabled={controlsDisabled}\n                                  onClick={() =>\n                                    capture?.toggleMarqueeCapture()\n                                  }\n                                >\n                                  <CameraGlyph className=\"size-4\" />\n                                  Capture area as image\n                                </DropdownMenuItem>\n                              ) : null}\n                              {features.fullscreen ? (\n                                <DropdownMenuItem\n                                  onClick={() => fullscreen?.toggleFullscreen()}\n                                >\n                                  {fullscreenState.isFullscreen ? (\n                                    <ExitFullscreenGlyph className=\"size-4\" />\n                                  ) : (\n                                    <FullscreenGlyph className=\"size-4\" />\n                                  )}\n                                  {fullscreenState.isFullscreen\n                                    ? \"Exit full screen\"\n                                    : \"Full screen\"}\n                                </DropdownMenuItem>\n                              ) : null}\n                            </DropdownMenuGroup>\n                            <DropdownMenuSeparator />\n                            <DropdownMenuGroup>\n                              <DropdownMenuItem\n                                disabled={controlsDisabled}\n                                onClick={() =>\n                                  setDialog({ type: \"properties\" })\n                                }\n                              >\n                                <InfoGlyph className=\"size-4\" />\n                                Document properties…\n                              </DropdownMenuItem>\n                              {features.security ? (\n                                <DropdownMenuItem\n                                  disabled={controlsDisabled}\n                                  onClick={() =>\n                                    setDialog({ type: \"security\" })\n                                  }\n                                >\n                                  <ShieldGlyph className=\"size-4\" />\n                                  Security…\n                                </DropdownMenuItem>\n                              ) : null}\n                              <DropdownMenuItem\n                                disabled={\n                                  controlsDisabled ||\n                                  !permissions.canModifyContents\n                                }\n                                onClick={flattenDocument}\n                              >\n                                <LayersGlyph className=\"size-4\" />\n                                Flatten annotations…\n                              </DropdownMenuItem>\n                            </DropdownMenuGroup>\n                            <DropdownMenuSeparator />\n                            <DropdownMenuGroup>\n                              <DropdownMenuItem\n                                disabled={controlsDisabled}\n                                onClick={exportAnnotationsJson}\n                              >\n                                <DownloadGlyph className=\"size-4\" />\n                                Export annotations (JSON)\n                              </DropdownMenuItem>\n                              <DropdownMenuItem\n                                disabled={\n                                  controlsDisabled ||\n                                  !permissions.canModifyAnnotations\n                                }\n                                onClick={() =>\n                                  annotationImportInputRef.current?.click()\n                                }\n                              >\n                                <UploadGlyph className=\"size-4\" />\n                                Import annotations (JSON)…\n                              </DropdownMenuItem>\n                            </DropdownMenuGroup>\n                            <DropdownMenuSeparator />\n                            <DropdownMenuGroup>\n                              <DropdownMenuLabel>Page layout</DropdownMenuLabel>\n                              <DropdownMenuRadioGroup\n                                value={spreadMode}\n                                onValueChange={(value) =>\n                                  spread?.setSpreadMode(\n                                    String(value) as SpreadMode\n                                  )\n                                }\n                              >\n                                <DropdownMenuRadioItem value={SpreadMode.None}>\n                                  Single page\n                                </DropdownMenuRadioItem>\n                                <DropdownMenuRadioItem value={SpreadMode.Odd}>\n                                  Two pages\n                                </DropdownMenuRadioItem>\n                                <DropdownMenuRadioItem value={SpreadMode.Even}>\n                                  Two pages, cover first\n                                </DropdownMenuRadioItem>\n                              </DropdownMenuRadioGroup>\n                            </DropdownMenuGroup>\n                            <DropdownMenuSeparator />\n                            <DropdownMenuItem\n                              onClick={() => setDialog({ type: \"shortcuts\" })}\n                            >\n                              <KeyboardGlyph className=\"size-4\" />\n                              Keyboard shortcuts\n                            </DropdownMenuItem>\n                          </DropdownMenuContent>\n                        </DropdownMenu>\n                      </div>\n                    </InlineScrollArea2>\n                  </div>\n                  <div\n                    data-slot=\"pdf-editor-ribbon\"\n                    className=\"flex min-h-10 items-center gap-1 border-b bg-muted/30 px-3 py-1.5\"\n                  >\n                    <PdfEditorModeSwitch\n                      mode={mode}\n                      features={features}\n                      compact={compactToolbar}\n                      onModeChange={setMode}\n                    />\n                    <PdfEditorToolbarSeparator className=\"mx-1.5\" />\n                    <InlineScrollArea2\n                      orientation=\"horizontal\"\n                      scrollFade\n                      scrollbarOverflowOnly\n                      className=\"min-w-0 flex-1 self-stretch\"\n                    >\n                      <div className=\"flex w-max min-w-full items-center gap-0.5 py-0.5\">\n                        {ribbon}\n                      </div>\n                    </InlineScrollArea2>\n                  </div>\n                </fieldset>\n              </TooltipProvider>\n            ) : null}\n\n            <div\n              ref={shellRef}\n              className=\"relative flex min-h-0 flex-1 overflow-hidden bg-muted/30\"\n            >\n              <PdfEditorWorkspace\n                leftInline={leftInline}\n                rightInline={rightInline}\n                left={\n                  leftPanel !== null ? (\n                    <div className=\"flex h-full flex-col\">\n                      <PanelTabStrip\n                        tabs={visibleLeftTabs}\n                        value={leftPanel}\n                        onChange={setLeftPanel}\n                        onClose={() => setLeftPanel(null)}\n                        label=\"Sidebar panels\"\n                        disabled={controlsDisabled}\n                      />\n                      <div className=\"min-h-0 flex-1\">\n                        {controlsDisabled ? null : leftPanelContent}\n                      </div>\n                    </div>\n                  ) : null\n                }\n                right={\n                  rightPanel !== null ? (\n                    <div className=\"flex h-full flex-col\">\n                      <PanelTabStrip\n                        tabs={visibleRightTabs}\n                        value={rightPanel}\n                        onChange={setRightPanel}\n                        onClose={() => setRightPanel(null)}\n                        label=\"Detail panels\"\n                        disabled={controlsDisabled}\n                      />\n                      <div className=\"min-h-0 flex-1 overflow-hidden\">\n                        {controlsDisabled ? null : rightPanel ===\n                          \"properties\" ? (\n                          <PdfEditorScrollArea\n                            className=\"h-full w-full\"\n                            scrollbarGutter={false}\n                          >\n                            {rightPanelContent}\n                          </PdfEditorScrollArea>\n                        ) : (\n                          rightPanelContent\n                        )}\n                      </div>\n                    </div>\n                  ) : null\n                }\n              >\n                {controlsDisabled ? (\n                  unavailableContent\n                ) : (\n                  <PdfEditorViewport\n                    documentId={documentId}\n                    className=\"relative h-full max-h-full min-h-0 min-w-0 flex-1\"\n                  >\n                    <PdfEditorViewportBridge\n                      viewportElementRef={viewportElementRef}\n                    />\n                    <GlobalPointerProvider documentId={documentId}>\n                      <Scroller\n                        documentId={documentId}\n                        renderPage={renderPage}\n                      />\n                    </GlobalPointerProvider>\n                    <CopyToClipboard />\n                  </PdfEditorViewport>\n                )}\n              </PdfEditorWorkspace>\n            </div>\n\n            {!controlsDisabled ? (\n              <>\n                {features.capture ? (\n                  <PdfEditorCaptureDialog documentId={documentId} />\n                ) : null}\n                <PdfEditorPrintDialog\n                  documentId={documentId}\n                  open={dialog?.type === \"print\"}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n                <PdfEditorSecurityDialog\n                  documentId={documentId}\n                  open={dialog?.type === \"security\"}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n                <PdfEditorPropertiesDialog\n                  documentId={documentId}\n                  open={dialog?.type === \"properties\"}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n                <PdfEditorShortcutsDialog\n                  open={dialog?.type === \"shortcuts\"}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n                <PdfEditorSignatureDialog\n                  open={dialog?.type === \"signature\"}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n                <PdfEditorLinkDialog\n                  documentId={documentId}\n                  open={dialog?.type === \"link\"}\n                  source={dialog?.type === \"link\" ? dialog.source : \"selection\"}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n                <PdfEditorConfirmDialog\n                  open={dialog?.type === \"confirm\"}\n                  title={dialog?.type === \"confirm\" ? dialog.title : \"\"}\n                  description={\n                    dialog?.type === \"confirm\" ? dialog.description : undefined\n                  }\n                  confirmLabel={\n                    dialog?.type === \"confirm\" ? dialog.confirmLabel : undefined\n                  }\n                  destructive={\n                    dialog?.type === \"confirm\" ? dialog.destructive : false\n                  }\n                  onConfirm={() => {\n                    if (dialog?.type === \"confirm\") dialog.onConfirm()\n                  }}\n                  onOpenChange={(open) => {\n                    if (!open) setDialog(null)\n                  }}\n                />\n              </>\n            ) : null}\n          </div>\n        </div>\n      </ToastProvider>\n    </PdfEditorContextProvider>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Document loader                                                            */\n/* -------------------------------------------------------------------------- */\ntype PdfEditorDocumentLoaderProps = Omit<\n  PdfEditorInnerProps,\n  | \"documentId\"\n  | \"document\"\n  | \"documentState\"\n  | \"fileName\"\n  | \"leftPanel\"\n  | \"setLeftPanel\"\n  | \"formDesignMode\"\n  | \"setFormDesignMode\"\n  | \"unavailableContent\"\n> & {\n  source: EditorSource | null\n  sourceLoading: boolean\n  engineError?: string\n  fileNameOverride?: string\n  signatureStorageKey: string | null\n  onDocumentLoadSuccess?: PDFEditorProps[\"onDocumentLoadSuccess\"]\n  onDocumentLoadError?: PDFEditorProps[\"onDocumentLoadError\"]\n}\nfunction PdfEditorDocumentLoader({\n  source,\n  sourceLoading,\n  engineError,\n  fileNameOverride,\n  signatureStorageKey,\n  onDocumentLoadSuccess,\n  onDocumentLoadError,\n  ...innerProps\n}: PdfEditorDocumentLoaderProps) {\n  const registryState = useRegistry()\n  const { provides: documentManager } = useDocumentManagerCapability()\n  const { activeDocumentId, activeDocument } = useActiveDocument()\n  const [loadError, setLoadError] = React.useState<{\n    sourceKey: string\n    message: string\n  } | null>(null)\n  const [requestedDocument, setRequestedDocument] = React.useState<{\n    sourceKey: string\n    documentId: string\n  } | null>(null)\n  const [leftPanel, setLeftPanel] = React.useState<PdfEditorLeftPanel | null>(\n    null\n  )\n  const [formDesignMode, setFormDesignMode] = React.useState(false)\n  const openedSourceRef = React.useRef<string | null>(null)\n  const successRef = React.useRef(onDocumentLoadSuccess)\n  const errorRef = React.useRef(onDocumentLoadError)\n  React.useEffect(() => {\n    successRef.current = onDocumentLoadSuccess\n    errorRef.current = onDocumentLoadError\n  })\n  React.useEffect(() => {\n    if (!documentManager || !source) return\n    if (openedSourceRef.current === source.key) return\n    openedSourceRef.current = source.key\n    setLoadError(null)\n    const previousIds = documentManager\n      .getOpenDocuments()\n      .map((item) => item.id)\n    const fail = (message: string) => {\n      if (openedSourceRef.current !== source.key) return\n      setLoadError({ sourceKey: source.key, message })\n      errorRef.current?.(message)\n    }\n    const task =\n      source.kind === \"url\"\n        ? documentManager.openDocumentUrl({\n            url: source.url,\n            name: source.name,\n            mode: /^(blob|data):/i.test(source.url) ? \"full-fetch\" : \"auto\",\n            autoActivate: previousIds.length === 0,\n          })\n        : documentManager.openDocumentBuffer({\n            buffer: source.buffer.slice(0),\n            name: source.name,\n            autoActivate: previousIds.length === 0,\n          })\n    task.wait(\n      (response) => {\n        if (openedSourceRef.current !== source.key) return\n        setRequestedDocument({\n          sourceKey: source.key,\n          documentId: response.documentId,\n        })\n        response.task.wait(\n          (openedDocument) => {\n            if (openedSourceRef.current !== source.key) return\n            documentManager.setActiveDocument(response.documentId)\n            successRef.current?.({\n              numPages: openedDocument.pageCount,\n              documentId: response.documentId,\n              fileName: source.name,\n            })\n            previousIds.forEach((id) => {\n              if (id !== response.documentId) {\n                documentManager.closeDocument(id).wait(noop, noop)\n              }\n            })\n          },\n          (failure) => {\n            // Password prompts are handled through the document state.\n            if (failure?.reason?.code === PdfErrorCode.Password) {\n              documentManager.setActiveDocument(response.documentId)\n              return\n            }\n            fail(failure?.reason?.message ?? \"The PDF could not be loaded.\")\n          }\n        )\n      },\n      (failure) =>\n        fail(failure?.reason?.message ?? \"The PDF could not be loaded.\")\n    )\n  }, [documentManager, source])\n  const fileName = source?.name ?? getPdfFileName(fileNameOverride)\n  const isRequestedDocument =\n    requestedDocument?.sourceKey === source?.key &&\n    requestedDocument?.documentId === activeDocumentId\n  const currentError =\n    loadError?.sourceKey === source?.key ? loadError?.message : undefined\n  const passwordRequired =\n    isRequestedDocument &&\n    activeDocument?.status === \"error\" &&\n    activeDocument.errorCode === PdfErrorCode.Password\n  const documentFailed =\n    currentError !== undefined ||\n    (isRequestedDocument &&\n      activeDocument?.status === \"error\" &&\n      !passwordRequired)\n  const document =\n    !sourceLoading &&\n    !documentFailed &&\n    !engineError &&\n    isRequestedDocument &&\n    activeDocument?.status === \"loaded\"\n      ? activeDocument.document\n      : null\n  const status =\n    engineError || documentFailed\n      ? \"error\"\n      : !source && !sourceLoading\n        ? \"empty\"\n        : \"loading\"\n  return (\n    <TooltipProvider delay={TOOLTIP_DELAY_MS}>\n      <PdfEditorSignaturePersistence storageKey={signatureStorageKey} />\n      <PDFContext.Provider\n        value={document ? registryState : EMPTY_EDITOR_CONTEXT}\n      >\n        <PdfEditorInner\n          {...innerProps}\n          documentId={document ? activeDocumentId! : \"\"}\n          document={document}\n          documentState={\n            document ? \"ready\" : passwordRequired ? \"password\" : status\n          }\n          fileName={fileName}\n          leftPanel={leftPanel}\n          setLeftPanel={setLeftPanel}\n          formDesignMode={formDesignMode}\n          setFormDesignMode={setFormDesignMode}\n          unavailableContent={\n            passwordRequired && activeDocument ? (\n              <PDFContext.Provider value={registryState}>\n                <PdfEditorPasswordPrompt documentState={activeDocument} />\n              </PDFContext.Provider>\n            ) : (\n              <PdfEditorDocumentStatus\n                state={status}\n                errorMessage={\n                  engineError ??\n                  currentError ??\n                  activeDocument?.error ??\n                  undefined\n                }\n                onUploadFile={\n                  innerProps.showUpload ? innerProps.onUploadFile : undefined\n                }\n              />\n            )\n          }\n        />\n      </PDFContext.Provider>\n    </TooltipProvider>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Root                                                                       */\n/* -------------------------------------------------------------------------- */\nexport const PDFEditor = React.forwardRef<PDFEditorHandle, PDFEditorProps>(\n  function PDFEditor(\n    {\n      className,\n      src,\n      fileName,\n      defaultZoom = \"fit-width\",\n      defaultMode = \"view\",\n      annotationAuthor = DEFAULT_ANNOTATION_AUTHOR,\n      colorPresets = PDF_EDITOR_DEFAULT_COLOR_PRESETS,\n      features,\n      showToolbar = true,\n      showUpload = true,\n      showDownload = true,\n      toolbarActions,\n      selectionActions,\n      persistSignatures = true,\n      stampManifests,\n      signatureFontsStylesheetUrl = PDF_EDITOR_DEFAULT_SIGNATURE_FONTS_URL,\n      resolveScrollAreaViewport,\n      renderPageOverlay,\n      onDocumentLoadSuccess,\n      onDocumentLoadError,\n      onActivePageChange,\n      onModeChange,\n      onAnnotationEvent,\n      onAnnotationsChange,\n      onFormValuesChange,\n      onRedactionsApplied,\n      onSave,\n      onCapture,\n      onPdfUpload,\n      onToast,\n      onSelectionAction,\n    },\n    ref\n  ) {\n    const { engine, error: engineError } = useSharedPdfEngine()\n    const {\n      source,\n      loading: sourceLoading,\n      replace,\n    } = useEditorSource(src, fileName)\n    const handleRef = React.useRef<EditorHandleImpl | null>(null)\n    const resolvedFeatures = React.useMemo<PdfEditorResolvedFeatures>(\n      () => ({ ...PDF_EDITOR_DEFAULT_FEATURES, ...features }),\n      [features]\n    )\n    const resolvedSelectionActions = React.useMemo(\n      () => selectionActions ?? [],\n      [selectionActions]\n    )\n    const signatureStorageKey =\n      persistSignatures === false\n        ? null\n        : typeof persistSignatures === \"string\"\n          ? persistSignatures\n          : DEFAULT_SIGNATURE_STORAGE_KEY\n    const handleUploadFile = React.useCallback(\n      (file: File) => {\n        file.arrayBuffer().then((buffer) => {\n          replace(buffer, getPdfFileName(file.name))\n          onPdfUpload?.(file)\n        })\n      },\n      [onPdfUpload, replace]\n    )\n    // Plugin registrations are created once per editor instance.\n    const [plugins] = React.useState(() => [\n      createPluginRegistration(DocumentManagerPluginPackage),\n      createPluginRegistration(ViewportPluginPackage, {\n        viewportGap: PAGE_GAP,\n      }),\n      createPluginRegistration(ScrollPluginPackage, {\n        defaultPageGap: PAGE_GAP,\n        defaultBufferSize: 2,\n      }),\n      createPluginRegistration(RenderPluginPackage),\n      createPluginRegistration(TilingPluginPackage, {\n        tileSize: 768,\n        overlapPx: 2.5,\n        extraRings: 0,\n      }),\n      createPluginRegistration(InteractionManagerPluginPackage),\n      createPluginRegistration(SelectionPluginPackage, {\n        menuHeight: SELECTION_MENU_HEIGHT,\n      }),\n      createPluginRegistration(SearchPluginPackage, { showAllResults: true }),\n      createPluginRegistration(ThumbnailPluginPackage, {\n        width: THUMBNAIL_WIDTH,\n        gap: THUMBNAIL_GAP,\n        imagePadding: THUMBNAIL_IMAGE_PADDING,\n        labelHeight: THUMBNAIL_LABEL_HEIGHT,\n        paddingY: THUMBNAIL_PANE_PADDING_Y,\n        buffer: 3,\n        autoScroll: true,\n        scrollBehavior: \"auto\",\n      }),\n      createPluginRegistration(ZoomPluginPackage, {\n        defaultZoomLevel: toZoomLevel(defaultZoom),\n        minZoom: MIN_ZOOM,\n        maxZoom: MAX_ZOOM,\n      }),\n      createPluginRegistration(RotatePluginPackage),\n      createPluginRegistration(SpreadPluginPackage, {\n        defaultSpreadMode: SpreadMode.None,\n      }),\n      createPluginRegistration(PanPluginPackage, { defaultMode: \"never\" }),\n      createPluginRegistration(HistoryPluginPackage),\n      createPluginRegistration(AnnotationPluginPackage, {\n        annotationAuthor,\n        colorPresets,\n        autoCommit: true,\n        selectAfterCreate: true,\n        deactivateToolAfterCreate: false,\n        locked: { type: LockModeType.Include, categories: [\"form\"] },\n      }),\n      createPluginRegistration(FormPluginPackage),\n      createPluginRegistration(RedactionPluginPackage, {\n        drawBlackBoxes: true,\n        useAnnotationMode: true,\n      }),\n      createPluginRegistration(StampPluginPackage, {\n        defaultLibrary: {\n          id: \"custom\",\n          name: \"Custom stamps\",\n          categories: [\"custom\", \"sidebar\"],\n        },\n        ...(stampManifests && stampManifests.length > 0\n          ? {\n              manifests: [\n                {\n                  url: PDF_EDITOR_DEFAULT_STAMP_MANIFEST_URL,\n                  fallbackLocale: \"en\",\n                },\n                ...stampManifests.map((url) => ({ url })),\n              ],\n            }\n          : {}),\n      }),\n      createPluginRegistration(SignaturePluginPackage, {\n        mode: SignatureMode.SignatureAndInitials,\n        defaultSize: { width: 160, height: 56 },\n      }),\n      createPluginRegistration(ExportPluginPackage, {\n        defaultFileName: \"document.pdf\",\n      }),\n      createPluginRegistration(PrintPluginPackage),\n      createPluginRegistration(CapturePluginPackage, {\n        scale: 2,\n        imageType: \"image/png\",\n        withAnnotations: true,\n      }),\n      createPluginRegistration(FullscreenPluginPackage),\n      createPluginRegistration(BookmarkPluginPackage),\n      createPluginRegistration(AttachmentPluginPackage),\n    ])\n    React.useImperativeHandle(\n      ref,\n      () => ({\n        getDocumentBuffer: () =>\n          handleRef.current?.getDocumentBuffer() ??\n          Promise.reject(new Error(\"The document is not ready.\")),\n        download: (name) =>\n          handleRef.current?.download(name) ?? Promise.resolve(),\n        print: (options) =>\n          handleRef.current?.print(options) ?? Promise.resolve(),\n        exportAnnotations: () =>\n          handleRef.current?.exportAnnotations() ?? Promise.resolve([]),\n        importAnnotations: (items) =>\n          handleRef.current?.importAnnotations(items),\n        getFormValues: () => handleRef.current?.getFormValues() ?? {},\n        setFormValues: (values) =>\n          handleRef.current?.setFormValues(values) ?? Promise.resolve(),\n        applyRedactions: () =>\n          handleRef.current?.applyRedactions() ?? Promise.resolve(),\n        setMode: (mode) => handleRef.current?.setMode(mode),\n        setActiveTool: (toolId) => handleRef.current?.setActiveTool(toolId),\n        scrollToPage: (pageNumber, options) =>\n          handleRef.current?.scrollToPage(pageNumber, options),\n        undo: () => handleRef.current?.undo(),\n        redo: () => handleRef.current?.redo(),\n        getViewportElement: () =>\n          handleRef.current?.getViewportElement() ?? null,\n      }),\n      []\n    )\n    const editor = (\n      <PdfEditorDocumentLoader\n        engineError={\n          engineError ? \"The PDF engine could not be loaded.\" : undefined\n        }\n        source={source}\n        sourceLoading={sourceLoading}\n        fileNameOverride={fileName}\n        signatureStorageKey={signatureStorageKey}\n        className={className}\n        defaultZoom={defaultZoom}\n        defaultMode={defaultMode}\n        annotationAuthor={annotationAuthor}\n        features={resolvedFeatures}\n        showToolbar={showToolbar}\n        showUpload={showUpload}\n        showDownload={showDownload}\n        toolbarActions={toolbarActions}\n        selectionActions={resolvedSelectionActions}\n        signatureFontsStylesheetUrl={signatureFontsStylesheetUrl}\n        renderPageOverlay={renderPageOverlay}\n        handleRef={handleRef}\n        onReplaceDocument={replace}\n        onUploadFile={handleUploadFile}\n        onDocumentLoadSuccess={onDocumentLoadSuccess}\n        onDocumentLoadError={onDocumentLoadError}\n        onActivePageChange={onActivePageChange}\n        onModeChange={onModeChange}\n        onAnnotationEvent={onAnnotationEvent}\n        onAnnotationsChange={onAnnotationsChange}\n        onFormValuesChange={onFormValuesChange}\n        onRedactionsApplied={onRedactionsApplied}\n        onSave={onSave}\n        onCapture={onCapture}\n        onToast={onToast}\n        onSelectionAction={onSelectionAction}\n      />\n    )\n    return (\n      <PDFContext.Provider value={EMPTY_EDITOR_CONTEXT}>\n        <PdfEditorScrollAreaResolverContext.Provider\n          value={resolveScrollAreaViewport ?? resolveDefaultScrollAreaViewport}\n        >\n          {engine ? (\n            <EmbedPDF engine={engine} plugins={plugins}>\n              {editor}\n            </EmbedPDF>\n          ) : (\n            editor\n          )}\n        </PdfEditorScrollAreaResolverContext.Provider>\n      </PDFContext.Provider>\n    )\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/pdf-editor.tsx"
    },
    {
      "path": "components/extend/pdf-editor-workspace.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  ResizableHandle,\n  ResizablePanel,\n  ResizablePanelGroup,\n} from \"@/components/ui/resizable\"\n\nfunction PanelBorder({ side }: { side: \"left\" | \"right\" }) {\n  return (\n    <ResizableHandle\n      aria-label={`Resize ${side} panel`}\n      className={cn(\n        \"pointer-events-auto z-40 w-px bg-border before:absolute before:inset-y-2 before:w-px before:bg-primary/50 before:[mask-image:linear-gradient(to_bottom,transparent,black_8%,black_92%,transparent)] before:opacity-0 before:transition-opacity before:duration-200 before:ease-in-out hover:before:opacity-100 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:before:opacity-100 data-[separator=active]:before:opacity-100 data-[separator=hover]:before:opacity-100 motion-reduce:before:transition-none\",\n        side === \"left\" ? \"before:-left-px\" : \"before:left-full\"\n      )}\n    />\n  )\n}\nfunction SidebarPanel({\n  defaultSize,\n  ...props\n}: React.ComponentProps<typeof ResizablePanel>) {\n  // Keep the registration size stable until the panel is mounted again.\n  const [initialSize] = React.useState(defaultSize)\n  return <ResizablePanel {...props} defaultSize={initialSize} />\n}\nexport function PdfEditorWorkspace({\n  children,\n  left,\n  right,\n  leftInline,\n  rightInline,\n}: {\n  children: React.ReactNode\n  left: React.ReactNode\n  right: React.ReactNode\n  leftInline: boolean\n  rightInline: boolean\n}) {\n  const id = React.useId()\n  const [leftSize, setLeftSize] = React.useState<string | number>(\"14rem\")\n  const [rightSize, setRightSize] = React.useState<string | number>(\"21rem\")\n  const leftElement = React.useRef<HTMLDivElement>(null)\n  const rightElement = React.useRef<HTMLDivElement>(null)\n  const leftId = `${id}-left`\n  const rightId = `${id}-right`\n  const rememberSizes = () => {\n    if (leftElement.current)\n      setLeftSize(leftElement.current.getBoundingClientRect().width)\n    if (rightElement.current)\n      setRightSize(rightElement.current.getBoundingClientRect().width)\n  }\n  const sidebar = (side: \"left\" | \"right\", content: React.ReactNode) => (\n    <SidebarPanel\n      id={side === \"left\" ? leftId : rightId}\n      defaultSize={side === \"left\" ? leftSize : rightSize}\n      minSize={side === \"left\" ? \"14rem\" : \"21rem\"}\n      maxSize={side === \"left\" ? \"28rem\" : \"32rem\"}\n      groupResizeBehavior=\"preserve-pixel-size\"\n      elementRef={side === \"left\" ? leftElement : rightElement}\n      className=\"pointer-events-auto h-full min-h-0 overflow-hidden bg-sidebar\"\n    >\n      <aside\n        data-slot={`pdf-editor-${side}-panel`}\n        className=\"flex h-full min-h-0 flex-col\"\n      >\n        {content}\n      </aside>\n    </SidebarPanel>\n  )\n  return (\n    <div className=\"relative h-full min-h-0 w-full flex-1 overflow-hidden\">\n      <ResizablePanelGroup\n        orientation=\"horizontal\"\n        onLayoutChanged={rememberSizes}\n      >\n        {left && leftInline ? sidebar(\"left\", left) : null}\n        {left && leftInline ? <PanelBorder side=\"left\" /> : null}\n        <ResizablePanel\n          id={`${id}-document`}\n          minSize={leftInline || rightInline ? \"15rem\" : \"0%\"}\n          className=\"relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden\"\n        >\n          {children}\n        </ResizablePanel>\n        {right && rightInline ? <PanelBorder side=\"right\" /> : null}\n        {right && rightInline ? sidebar(\"right\", right) : null}\n      </ResizablePanelGroup>\n      {([\"left\", \"right\"] as const).map((side) => {\n        const content = side === \"left\" ? left : right\n        const inline = side === \"left\" ? leftInline : rightInline\n        if (!content || inline) return null\n        return (\n          <ResizablePanelGroup\n            key={side}\n            orientation=\"horizontal\"\n            onLayoutChanged={rememberSizes}\n            className={cn(\n              \"pointer-events-none absolute inset-y-0 z-30\",\n              side === \"left\" ? \"left-0 min-w-60\" : \"right-0 min-w-88\"\n            )}\n          >\n            {side === \"right\" ? (\n              <ResizablePanel id={`${id}-right-space`} minSize=\"1rem\" />\n            ) : null}\n            {side === \"right\" ? <PanelBorder side=\"right\" /> : null}\n            {sidebar(side, content)}\n            {side === \"left\" ? <PanelBorder side=\"left\" /> : null}\n            {side === \"left\" ? (\n              <ResizablePanel id={`${id}-left-space`} minSize=\"1rem\" />\n            ) : null}\n          </ResizablePanelGroup>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/extend/pdf-editor-workspace.tsx"
    },
    {
      "path": "components/extend/pdf-editor-shared.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\nimport type { DocumentPermissions } from \"@embedpdf/core/react\"\nimport type {\n  PdfAnnotationObject,\n  PdfDocumentObject,\n  Rect,\n  Size,\n} from \"@embedpdf/models\"\nimport { PdfAnnotationSubtype } from \"@embedpdf/models\"\nimport type { CaptureAreaEvent } from \"@embedpdf/plugin-capture/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport {\n  ScrollArea as InlineScrollArea,\n  ScrollBar,\n} from \"@/components/ui/scroll-area\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\n/* -------------------------------------------------------------------------- */\n/* Types                                                                      */\n/* -------------------------------------------------------------------------- */\nexport type PdfEditorMode = \"view\" | \"annotate\" | \"sign\" | \"forms\" | \"redact\"\nexport type PdfEditorLeftPanel =\n  | \"thumbnails\"\n  | \"outline\"\n  | \"attachments\"\n  | \"pages\"\nexport type PdfEditorRightPanel =\n  | \"properties\"\n  | \"comments\"\n  | \"redactions\"\n  | \"stamps\"\n  | \"signatures\"\n  | \"forms\"\nexport type PdfEditorFeatureFlags = {\n  /** Text markup, drawing, shapes, text boxes, notes, links, and image stamps. */\n  annotate?: boolean\n  /** Mark text or areas for redaction and permanently apply them. */\n  redact?: boolean\n  /** Fill AcroForm fields and design new form widgets. */\n  forms?: boolean\n  /** Draw, type, or upload signatures and initials, then place them. */\n  sign?: boolean\n  /** Rubber stamp libraries and custom stamps. */\n  stamps?: boolean\n  /** Rotate, reorder, delete, insert, extract, and merge pages. */\n  pages?: boolean\n  /** Password protection and permission restrictions. */\n  security?: boolean\n  print?: boolean\n  /** Area screenshot capture. */\n  capture?: boolean\n  /** Bookmark outline sidebar. */\n  outline?: boolean\n  /** Embedded file attachments sidebar. */\n  attachments?: boolean\n  /** Comment threads sidebar. */\n  comments?: boolean\n  fullscreen?: boolean\n}\nexport type PdfEditorResolvedFeatures = Required<PdfEditorFeatureFlags>\nexport const PDF_EDITOR_DEFAULT_FEATURES: PdfEditorResolvedFeatures = {\n  annotate: true,\n  redact: true,\n  forms: true,\n  sign: true,\n  stamps: true,\n  pages: true,\n  security: true,\n  print: true,\n  capture: true,\n  outline: true,\n  attachments: true,\n  comments: true,\n  fullscreen: true,\n}\n/** A rectangle expressed as percentages (0-100) of the page size. */\nexport type PdfEditorNormalizedRect = {\n  top: number\n  left: number\n  width: number\n  height: number\n}\nexport type PdfEditorSelectionPage = {\n  pageIndex: number\n  pageNumber: number\n  pageSize: Size\n  /** Bounding box in PDF points (origin top-left). */\n  rect: Rect\n  /** Per-line rectangles in PDF points. */\n  rects: Rect[]\n  normalizedRect: PdfEditorNormalizedRect\n  normalizedRects: PdfEditorNormalizedRect[]\n}\n/** Emitted for custom text-selection actions. Coordinates are page-relative. */\nexport type PdfEditorSelectionPayload = PdfEditorSelectionPage & {\n  documentId: string\n  /** The selected text across all pages, joined with newlines. */\n  text: string\n  /** Every page that the selection spans (usually one). */\n  pages: PdfEditorSelectionPage[]\n}\nexport type PdfEditorSelectionAction = {\n  id: string\n  label: string\n  icon?: React.ReactNode\n  /** Called with page coordinates and the selected text. */\n  onSelect?: (payload: PdfEditorSelectionPayload) => void\n  /** Keep the text selected after the action runs. Defaults to false. */\n  keepSelection?: boolean\n}\nexport type PdfEditorDialogRequest =\n  | {\n      type: \"print\"\n    }\n  | {\n      type: \"security\"\n    }\n  | {\n      type: \"properties\"\n    }\n  | {\n      type: \"shortcuts\"\n    }\n  | {\n      type: \"signature\"\n    }\n  | {\n      type: \"link\"\n      source: \"annotation\" | \"selection\"\n    }\n  | {\n      type: \"confirm\"\n      title: string\n      description?: string\n      confirmLabel?: string\n      destructive?: boolean\n      onConfirm: () => void\n    }\nexport type PdfEditorNoticeTone = \"info\" | \"success\" | \"error\"\nexport type PdfEditorContextValue = {\n  documentId: string\n  document: PdfDocumentObject | null\n  fileName: string\n  mode: PdfEditorMode\n  setMode: (mode: PdfEditorMode) => void\n  leftPanel: PdfEditorLeftPanel | null\n  setLeftPanel: (panel: PdfEditorLeftPanel | null) => void\n  toggleLeftPanel: (panel: PdfEditorLeftPanel) => void\n  rightPanel: PdfEditorRightPanel | null\n  setRightPanel: (panel: PdfEditorRightPanel | null) => void\n  toggleRightPanel: (panel: PdfEditorRightPanel) => void\n  openDialog: (request: PdfEditorDialogRequest) => void\n  notify: (message: string, tone?: PdfEditorNoticeTone) => void\n  features: PdfEditorResolvedFeatures\n  permissions: DocumentPermissions\n  annotationAuthor: string\n  selectionActions: PdfEditorSelectionAction[]\n  formDesignMode: boolean\n  setFormDesignMode: React.Dispatch<React.SetStateAction<boolean>>\n  /** Replaces the open document with new PDF bytes (page operations, flattening). */\n  replaceDocument: (buffer: ArrayBuffer, fileName?: string) => void\n  /** Downloads the current document, including pending edits. */\n  downloadDocument: (fileName?: string) => Promise<void>\n  /** Returns the current document bytes, including pending edits. */\n  getDocumentBuffer: () => Promise<ArrayBuffer>\n  scrollToPage: (\n    pageNumber: number,\n    options?: {\n      smooth?: boolean\n    }\n  ) => void\n  scrollToAnnotation: (annotation: PdfAnnotationObject) => void\n  onCapture?: (event: CaptureAreaEvent) => void\n  onSelectionAction?: (\n    actionId: string,\n    payload: PdfEditorSelectionPayload\n  ) => void\n  signatureFontsStylesheetUrl: string | null\n}\nconst PdfEditorContext = React.createContext<PdfEditorContextValue | null>(null)\nexport const PdfEditorContextProvider = PdfEditorContext.Provider\nexport function usePdfEditor() {\n  const context = React.useContext(PdfEditorContext)\n  if (!context) {\n    throw new Error(\"usePdfEditor must be used inside a PDFEditor.\")\n  }\n  return context\n}\n/* -------------------------------------------------------------------------- */\n/* Constants                                                                  */\n/* -------------------------------------------------------------------------- */\nexport const PDF_EDITOR_DEFAULT_COLOR_PRESETS = [\n  \"#111827\",\n  \"#e11d48\",\n  \"#f97316\",\n  \"#facc15\",\n  \"#22c55e\",\n  \"#0ea5e9\",\n  \"#2563eb\",\n  \"#8b5cf6\",\n  \"#ec4899\",\n  \"#64748b\",\n  \"#ffffff\",\n]\nexport const PDF_EDITOR_SELECTION_COLOR = \"#2563eb\"\nexport const PDF_EDITOR_DEFAULT_STAMP_MANIFEST_URL =\n  \"https://cdn.jsdelivr.net/npm/@embedpdf/default-stamps/{locale}/manifest.json\"\nexport const PDF_EDITOR_DEFAULT_SIGNATURE_FONTS_URL =\n  \"https://fonts.googleapis.com/css2?family=Caveat&family=Dancing+Script&family=Great+Vibes&family=Pacifico&display=swap\"\nexport const PDF_EDITOR_SIGNATURE_FONTS: ReadonlyArray<{\n  name: string\n  family: string\n}> = [\n  { name: \"Dancing Script\", family: \"'Dancing Script', cursive\" },\n  { name: \"Great Vibes\", family: \"'Great Vibes', cursive\" },\n  { name: \"Pacifico\", family: \"'Pacifico', cursive\" },\n  { name: \"Caveat\", family: \"'Caveat', cursive\" },\n]\n/* -------------------------------------------------------------------------- */\n/* Icons                                                                      */\n/*                                                                            */\n/* Icons that end up in value position (tool catalogs, switch statements) are */\n/* wrapped in small `*Glyph` components so the shadcn CLI can still swap the  */\n/* placeholder for the consumer's icon library at install time.               */\n/* -------------------------------------------------------------------------- */\nexport type PdfEditorGlyphProps = {\n  className?: string\n  style?: React.CSSProperties\n}\nexport type PdfEditorGlyph = React.ComponentType<PdfEditorGlyphProps>\nexport function PanelLeftGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"PanelLeft\"\n      tabler=\"IconLayoutSidebar\"\n      hugeicons=\"SidebarLeftIcon\"\n      phosphor=\"SidebarIcon\"\n      remixicon=\"RiLayoutLeftLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PanelRightGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"PanelRight\"\n      tabler=\"IconLayoutSidebarRight\"\n      hugeicons=\"SidebarRightIcon\"\n      phosphor=\"SidebarSimpleIcon\"\n      remixicon=\"RiLayoutRightLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function UndoGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Undo2\"\n      tabler=\"IconArrowBackUp\"\n      hugeicons=\"Undo02Icon\"\n      phosphor=\"ArrowUUpLeftIcon\"\n      remixicon=\"RiArrowGoBackLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function RedoGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Redo2\"\n      tabler=\"IconArrowForwardUp\"\n      hugeicons=\"Redo02Icon\"\n      phosphor=\"ArrowUUpRightIcon\"\n      remixicon=\"RiArrowGoForwardLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ZoomOutGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"CircleMinus\"\n      tabler=\"IconCircleMinus\"\n      hugeicons=\"MinusSignCircleIcon\"\n      phosphor=\"MinusCircleIcon\"\n      remixicon=\"RiIndeterminateCircleLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ZoomInGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"CirclePlus\"\n      tabler=\"IconCirclePlus\"\n      hugeicons=\"PlusSignCircleIcon\"\n      phosphor=\"PlusCircleIcon\"\n      remixicon=\"RiAddCircleLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function RotateGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"RotateCw\"\n      tabler=\"IconRotateClockwise\"\n      hugeicons=\"RotateClockwiseIcon\"\n      phosphor=\"ArrowClockwiseIcon\"\n      remixicon=\"RiClockwiseLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SearchGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Search\"\n      tabler=\"IconSearch\"\n      hugeicons=\"Search01Icon\"\n      phosphor=\"MagnifyingGlassIcon\"\n      remixicon=\"RiSearchLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function MoreGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Ellipsis\"\n      tabler=\"IconDots\"\n      hugeicons=\"MoreHorizontalIcon\"\n      phosphor=\"DotsThreeIcon\"\n      remixicon=\"RiMoreLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function DownloadGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Download\"\n      tabler=\"IconDownload\"\n      hugeicons=\"Download01Icon\"\n      phosphor=\"DownloadSimpleIcon\"\n      remixicon=\"RiDownload2Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function UploadGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Upload\"\n      tabler=\"IconUpload\"\n      hugeicons=\"Upload01Icon\"\n      phosphor=\"UploadSimpleIcon\"\n      remixicon=\"RiUpload2Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PrintGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Printer\"\n      tabler=\"IconPrinter\"\n      hugeicons=\"PrinterIcon\"\n      phosphor=\"PrinterIcon\"\n      remixicon=\"RiPrinterLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function FullscreenGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Maximize\"\n      tabler=\"IconMaximize\"\n      hugeicons=\"FullScreenIcon\"\n      phosphor=\"CornersOutIcon\"\n      remixicon=\"RiFullscreenLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ExitFullscreenGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Minimize\"\n      tabler=\"IconMinimize\"\n      hugeicons=\"MinimizeScreenIcon\"\n      phosphor=\"CornersInIcon\"\n      remixicon=\"RiFullscreenExitLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function HandGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Hand\"\n      tabler=\"IconHandStop\"\n      hugeicons=\"HandIcon\"\n      phosphor=\"HandIcon\"\n      remixicon=\"RiHand\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CursorGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MousePointer2\"\n      tabler=\"IconPointer\"\n      hugeicons=\"Cursor01Icon\"\n      phosphor=\"CursorIcon\"\n      remixicon=\"RiCursorLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CameraGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Camera\"\n      tabler=\"IconCamera\"\n      hugeicons=\"Camera01Icon\"\n      phosphor=\"CameraIcon\"\n      remixicon=\"RiScreenshotLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function MarqueeZoomGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ScanSearch\"\n      tabler=\"IconZoomInArea\"\n      hugeicons=\"ZoomInAreaIcon\"\n      phosphor=\"MagnifyingGlassPlusIcon\"\n      remixicon=\"RiZoomInLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function BookOpenGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"BookOpen\"\n      tabler=\"IconBook\"\n      hugeicons=\"Book01Icon\"\n      phosphor=\"BookOpenIcon\"\n      remixicon=\"RiBookOpenLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function FileGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"File\"\n      tabler=\"IconFile\"\n      hugeicons=\"File01Icon\"\n      phosphor=\"FileIcon\"\n      remixicon=\"RiFileLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ArrowsHorizontalGlyph({\n  className,\n  style,\n}: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MoveHorizontal\"\n      tabler=\"IconArrowsHorizontal\"\n      hugeicons=\"ArrowLeftRightIcon\"\n      phosphor=\"ArrowsHorizontalIcon\"\n      remixicon=\"RiArrowLeftRightLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ArrowsVerticalGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MoveVertical\"\n      tabler=\"IconArrowsVertical\"\n      hugeicons=\"ArrowUpDownIcon\"\n      phosphor=\"ArrowsVerticalIcon\"\n      remixicon=\"RiArrowUpDownLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function HighlighterGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Highlighter\"\n      tabler=\"IconHighlight\"\n      hugeicons=\"HighlighterIcon\"\n      phosphor=\"HighlighterIcon\"\n      remixicon=\"RiMarkPenLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function UnderlineGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Underline\"\n      tabler=\"IconUnderline\"\n      hugeicons=\"TextUnderlineIcon\"\n      phosphor=\"TextUnderlineIcon\"\n      remixicon=\"RiUnderline\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function StrikethroughGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Strikethrough\"\n      tabler=\"IconStrikethrough\"\n      hugeicons=\"TextStrikethroughIcon\"\n      phosphor=\"TextStrikethroughIcon\"\n      remixicon=\"RiStrikethrough\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SquigglyGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LineSquiggle\"\n      tabler=\"IconWaveSine\"\n      hugeicons=\"WaveIcon\"\n      phosphor=\"WaveSineIcon\"\n      remixicon=\"RiPulseLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PenGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Pen\"\n      tabler=\"IconPencil\"\n      hugeicons=\"Pen01Icon\"\n      phosphor=\"PenIcon\"\n      remixicon=\"RiPencilLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function BrushGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Brush\"\n      tabler=\"IconBrush\"\n      hugeicons=\"BrushIcon\"\n      phosphor=\"PaintBrushIcon\"\n      remixicon=\"RiBrushLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function TextGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Type\"\n      tabler=\"IconTypography\"\n      hugeicons=\"TextIcon\"\n      phosphor=\"TextTIcon\"\n      remixicon=\"RiText\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function TextFontGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Type\"\n      tabler=\"IconTypography\"\n      hugeicons=\"TextFontIcon\"\n      phosphor=\"TextAaIcon\"\n      remixicon=\"RiFontSize\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CalloutGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MessageSquareText\"\n      tabler=\"IconMessage2\"\n      hugeicons=\"BubbleChatIcon\"\n      phosphor=\"ChatTextIcon\"\n      remixicon=\"RiChat3Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function StickyNoteGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"StickyNote\"\n      tabler=\"IconNote\"\n      hugeicons=\"StickyNote01Icon\"\n      phosphor=\"NoteIcon\"\n      remixicon=\"RiStickyNoteLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SquareGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Square\"\n      tabler=\"IconSquare\"\n      hugeicons=\"SquareIcon\"\n      phosphor=\"SquareIcon\"\n      remixicon=\"RiCheckboxBlankLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CircleGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Circle\"\n      tabler=\"IconCircle\"\n      hugeicons=\"CircleIcon\"\n      phosphor=\"CircleIcon\"\n      remixicon=\"RiCircleLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function LineGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Minus\"\n      tabler=\"IconLine\"\n      hugeicons=\"MinusSignIcon\"\n      phosphor=\"LineSegmentIcon\"\n      remixicon=\"RiSubtractLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ArrowGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MoveUpRight\"\n      tabler=\"IconArrowUpRight\"\n      hugeicons=\"ArrowUpRight01Icon\"\n      phosphor=\"ArrowUpRightIcon\"\n      remixicon=\"RiArrowRightUpLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PolylineGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Spline\"\n      tabler=\"IconVectorSpline\"\n      hugeicons=\"SplinePointerIcon\"\n      phosphor=\"VectorTwoIcon\"\n      remixicon=\"RiRouteLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PolygonGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Pentagon\"\n      tabler=\"IconPolygon\"\n      hugeicons=\"PolygonIcon\"\n      phosphor=\"PolygonIcon\"\n      remixicon=\"RiPentagonLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ImageGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Image\"\n      tabler=\"IconPhoto\"\n      hugeicons=\"Image01Icon\"\n      phosphor=\"ImageIcon\"\n      remixicon=\"RiImageLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function StampGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Stamp\"\n      tabler=\"IconRubberStamp\"\n      hugeicons=\"StampIcon\"\n      phosphor=\"StampIcon\"\n      remixicon=\"RiVerifiedBadgeLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function LinkGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Link\"\n      tabler=\"IconLink\"\n      hugeicons=\"Link02Icon\"\n      phosphor=\"LinkIcon\"\n      remixicon=\"RiLink\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function InsertTextGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"TextCursorInput\"\n      tabler=\"IconCursorText\"\n      hugeicons=\"CursorTextIcon\"\n      phosphor=\"CursorTextIcon\"\n      remixicon=\"RiInputCursorMove\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ReplaceTextGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Replace\"\n      tabler=\"IconReplace\"\n      hugeicons=\"ReplaceIcon\"\n      phosphor=\"ArrowsLeftRightIcon\"\n      remixicon=\"RiFindReplaceLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function TrashGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Trash2\"\n      tabler=\"IconTrash\"\n      hugeicons=\"Delete02Icon\"\n      phosphor=\"TrashIcon\"\n      remixicon=\"RiDeleteBinLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function GroupGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Group\"\n      tabler=\"IconLayersUnion\"\n      hugeicons=\"GroupItemsIcon\"\n      phosphor=\"StackIcon\"\n      remixicon=\"RiStackLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function UngroupGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Ungroup\"\n      tabler=\"IconLayersSubtract\"\n      hugeicons=\"UngroupItemsIcon\"\n      phosphor=\"StackSimpleIcon\"\n      remixicon=\"RiStackLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function LockGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Lock\"\n      tabler=\"IconLock\"\n      hugeicons=\"SquareLock02Icon\"\n      phosphor=\"LockIcon\"\n      remixicon=\"RiLockLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function UnlockGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LockOpen\"\n      tabler=\"IconLockOpen\"\n      hugeicons=\"SquareUnlock02Icon\"\n      phosphor=\"LockOpenIcon\"\n      remixicon=\"RiLockUnlockLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CommentGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MessageCircle\"\n      tabler=\"IconMessageCircle\"\n      hugeicons=\"Comment01Icon\"\n      phosphor=\"ChatCircleIcon\"\n      remixicon=\"RiChat1Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CommentsGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"MessagesSquare\"\n      tabler=\"IconMessages\"\n      hugeicons=\"CommentAdd01Icon\"\n      phosphor=\"ChatsIcon\"\n      remixicon=\"RiChat3Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CheckGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Check\"\n      tabler=\"IconCheck\"\n      hugeicons=\"Tick02Icon\"\n      phosphor=\"CheckIcon\"\n      remixicon=\"RiCheckLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CloseGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"X\"\n      tabler=\"IconX\"\n      hugeicons=\"Cancel01Icon\"\n      phosphor=\"XIcon\"\n      remixicon=\"RiCloseLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PlusGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Plus\"\n      tabler=\"IconPlus\"\n      hugeicons=\"Add01Icon\"\n      phosphor=\"PlusIcon\"\n      remixicon=\"RiAddLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function MinusGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Minus\"\n      tabler=\"IconMinus\"\n      hugeicons=\"MinusSignIcon\"\n      phosphor=\"MinusIcon\"\n      remixicon=\"RiSubtractLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CopyGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Copy\"\n      tabler=\"IconCopy\"\n      hugeicons=\"Copy01Icon\"\n      phosphor=\"CopyIcon\"\n      remixicon=\"RiFileCopyLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function EyeOffGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"EyeOff\"\n      tabler=\"IconEyeOff\"\n      hugeicons=\"ViewOffIcon\"\n      phosphor=\"EyeSlashIcon\"\n      remixicon=\"RiEyeOffLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function EyeGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Eye\"\n      tabler=\"IconEye\"\n      hugeicons=\"ViewIcon\"\n      phosphor=\"EyeIcon\"\n      remixicon=\"RiEyeLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function RedactAreaGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"SquareDashed\"\n      tabler=\"IconSquareDashed\"\n      hugeicons=\"CursorRectangleSelection01Icon\"\n      phosphor=\"SelectionIcon\"\n      remixicon=\"RiCropLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SignatureGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Signature\"\n      tabler=\"IconSignature\"\n      hugeicons=\"SignatureIcon\"\n      phosphor=\"SignatureIcon\"\n      remixicon=\"RiQuillPenLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function TextFieldGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"RectangleEllipsis\"\n      tabler=\"IconForms\"\n      hugeicons=\"InputTextIcon\"\n      phosphor=\"TextboxIcon\"\n      remixicon=\"RiInputField\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function CheckboxGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"SquareCheck\"\n      tabler=\"IconSquareCheck\"\n      hugeicons=\"CheckmarkSquare01Icon\"\n      phosphor=\"CheckSquareIcon\"\n      remixicon=\"RiCheckboxLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function RadioGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"CircleDot\"\n      tabler=\"IconCircleDot\"\n      hugeicons=\"RadioButtonIcon\"\n      phosphor=\"RadioButtonIcon\"\n      remixicon=\"RiRadioButtonLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ComboboxGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronsUpDown\"\n      tabler=\"IconSelect\"\n      hugeicons=\"UnfoldMoreIcon\"\n      phosphor=\"CaretUpDownIcon\"\n      remixicon=\"RiArrowUpDownLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ListboxGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"List\"\n      tabler=\"IconList\"\n      hugeicons=\"LeftToRightListBulletIcon\"\n      phosphor=\"ListIcon\"\n      remixicon=\"RiListUnordered\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ListTreeGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ListTree\"\n      tabler=\"IconListTree\"\n      hugeicons=\"ListTreeIcon\"\n      phosphor=\"TreeStructureIcon\"\n      remixicon=\"RiNodeTree\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PaperclipGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Paperclip\"\n      tabler=\"IconPaperclip\"\n      hugeicons=\"Attachment01Icon\"\n      phosphor=\"PaperclipIcon\"\n      remixicon=\"RiAttachment2\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function GridGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LayoutGrid\"\n      tabler=\"IconLayoutGrid\"\n      hugeicons=\"GridViewIcon\"\n      phosphor=\"SquaresFourIcon\"\n      remixicon=\"RiLayoutGridLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SlidersGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"SlidersHorizontal\"\n      tabler=\"IconAdjustmentsHorizontal\"\n      hugeicons=\"SlidersHorizontalIcon\"\n      phosphor=\"SlidersHorizontalIcon\"\n      remixicon=\"RiEqualizerLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function InfoGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Info\"\n      tabler=\"IconInfoCircle\"\n      hugeicons=\"InformationCircleIcon\"\n      phosphor=\"InfoIcon\"\n      remixicon=\"RiInformationLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ShieldGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ShieldCheck\"\n      tabler=\"IconShieldLock\"\n      hugeicons=\"Shield01Icon\"\n      phosphor=\"ShieldCheckIcon\"\n      remixicon=\"RiShieldKeyholeLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function FileTextGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"FileText\"\n      tabler=\"IconFileText\"\n      hugeicons=\"File01Icon\"\n      phosphor=\"FileTextIcon\"\n      remixicon=\"RiFileTextLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ChevronDownGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronDown\"\n      tabler=\"IconChevronDown\"\n      hugeicons=\"ArrowDown01Icon\"\n      phosphor=\"CaretDownIcon\"\n      remixicon=\"RiArrowDownSLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ChevronUpGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronUp\"\n      tabler=\"IconChevronUp\"\n      hugeicons=\"ArrowUp01Icon\"\n      phosphor=\"CaretUpIcon\"\n      remixicon=\"RiArrowUpSLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ChevronLeftGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronLeft\"\n      tabler=\"IconChevronLeft\"\n      hugeicons=\"ArrowLeft01Icon\"\n      phosphor=\"CaretLeftIcon\"\n      remixicon=\"RiArrowLeftSLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ChevronRightGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronRight\"\n      tabler=\"IconChevronRight\"\n      hugeicons=\"ArrowRight01Icon\"\n      phosphor=\"CaretRightIcon\"\n      remixicon=\"RiArrowRightSLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function LayersGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Layers\"\n      tabler=\"IconStack2\"\n      hugeicons=\"Layers01Icon\"\n      phosphor=\"StackIcon\"\n      remixicon=\"RiStackLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function FilePlusGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"FilePlus\"\n      tabler=\"IconFilePlus\"\n      hugeicons=\"FileAddIcon\"\n      phosphor=\"FilePlusIcon\"\n      remixicon=\"RiFileAddLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ScissorsGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Scissors\"\n      tabler=\"IconScissors\"\n      hugeicons=\"Scissor01Icon\"\n      phosphor=\"ScissorsIcon\"\n      remixicon=\"RiScissorsLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function MergeGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Merge\"\n      tabler=\"IconArrowMerge\"\n      hugeicons=\"GitMergeIcon\"\n      phosphor=\"GitMergeIcon\"\n      remixicon=\"RiGitMergeLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function RefreshGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"RefreshCw\"\n      tabler=\"IconRefresh\"\n      hugeicons=\"RefreshIcon\"\n      phosphor=\"ArrowsClockwiseIcon\"\n      remixicon=\"RiRefreshLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SaveGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Save\"\n      tabler=\"IconDeviceFloppy\"\n      hugeicons=\"FloppyDiskIcon\"\n      phosphor=\"FloppyDiskIcon\"\n      remixicon=\"RiSaveLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function KeyboardGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Keyboard\"\n      tabler=\"IconKeyboard\"\n      hugeicons=\"KeyboardIcon\"\n      phosphor=\"KeyboardIcon\"\n      remixicon=\"RiKeyboardLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function PaletteGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Palette\"\n      tabler=\"IconPalette\"\n      hugeicons=\"PaintBoardIcon\"\n      phosphor=\"PaletteIcon\"\n      remixicon=\"RiPaletteLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function BoldGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Bold\"\n      tabler=\"IconBold\"\n      hugeicons=\"TextBoldIcon\"\n      phosphor=\"TextBIcon\"\n      remixicon=\"RiBold\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ItalicGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Italic\"\n      tabler=\"IconItalic\"\n      hugeicons=\"TextItalicIcon\"\n      phosphor=\"TextItalicIcon\"\n      remixicon=\"RiItalic\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlignLeftGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"TextAlignStart\"\n      tabler=\"IconAlignLeft\"\n      hugeicons=\"TextAlignLeft01Icon\"\n      phosphor=\"TextAlignLeftIcon\"\n      remixicon=\"RiAlignLeft\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlignCenterGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"TextAlignCenter\"\n      tabler=\"IconAlignCenter\"\n      hugeicons=\"TextAlignCenterIcon\"\n      phosphor=\"TextAlignCenterIcon\"\n      remixicon=\"RiAlignCenter\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlignRightGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"TextAlignEnd\"\n      tabler=\"IconAlignRight\"\n      hugeicons=\"TextAlignRight01Icon\"\n      phosphor=\"TextAlignRightIcon\"\n      remixicon=\"RiAlignRight\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlignTopGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ArrowUpToLine\"\n      tabler=\"IconAlignBoxTopCenter\"\n      hugeicons=\"AlignBoxTopCenterIcon\"\n      phosphor=\"AlignTopIcon\"\n      remixicon=\"RiAlignTop\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlignMiddleGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"AlignVerticalJustifyCenter\"\n      tabler=\"IconAlignBoxCenterMiddle\"\n      hugeicons=\"AlignBoxMiddleCenterIcon\"\n      phosphor=\"AlignCenterVerticalIcon\"\n      remixicon=\"RiAlignVertically\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlignBottomGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ArrowDownToLine\"\n      tabler=\"IconAlignBoxBottomCenter\"\n      hugeicons=\"AlignBoxBottomCenterIcon\"\n      phosphor=\"AlignBottomIcon\"\n      remixicon=\"RiAlignBottom\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function FlagGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Flag\"\n      tabler=\"IconFlag\"\n      hugeicons=\"Flag01Icon\"\n      phosphor=\"FlagIcon\"\n      remixicon=\"RiFlagLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ClipboardGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Clipboard\"\n      tabler=\"IconClipboard\"\n      hugeicons=\"ClipboardIcon\"\n      phosphor=\"ClipboardIcon\"\n      remixicon=\"RiClipboardLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function KeyGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Key\"\n      tabler=\"IconKey\"\n      hugeicons=\"Key01Icon\"\n      phosphor=\"KeyIcon\"\n      remixicon=\"RiKey2Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ExternalLinkGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ExternalLink\"\n      tabler=\"IconExternalLink\"\n      hugeicons=\"ExternalLinkIcon\"\n      phosphor=\"ArrowSquareOutIcon\"\n      remixicon=\"RiExternalLinkLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function MoveGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Move\"\n      tabler=\"IconArrowsMove\"\n      hugeicons=\"MoveIcon\"\n      phosphor=\"ArrowsOutCardinalIcon\"\n      remixicon=\"RiDragMove2Line\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function AlertGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"TriangleAlert\"\n      tabler=\"IconAlertTriangle\"\n      hugeicons=\"Alert01Icon\"\n      phosphor=\"WarningIcon\"\n      remixicon=\"RiAlertLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ArrowUpGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ArrowUp\"\n      tabler=\"IconArrowUp\"\n      hugeicons=\"ArrowUp02Icon\"\n      phosphor=\"ArrowUpIcon\"\n      remixicon=\"RiArrowUpLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function ArrowDownGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ArrowDown\"\n      tabler=\"IconArrowDown\"\n      hugeicons=\"ArrowDown02Icon\"\n      phosphor=\"ArrowDownIcon\"\n      remixicon=\"RiArrowDownLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function EditGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Pencil\"\n      tabler=\"IconEdit\"\n      hugeicons=\"PencilEdit01Icon\"\n      phosphor=\"PencilSimpleIcon\"\n      remixicon=\"RiEditLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\nexport function SparklesGlyph({ className, style }: PdfEditorGlyphProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Sparkles\"\n      tabler=\"IconSparkles\"\n      hugeicons=\"SparklesIcon\"\n      phosphor=\"SparkleIcon\"\n      remixicon=\"RiSparklingLine\"\n      className={className}\n      style={style}\n    />\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Tool catalogs                                                              */\n/* -------------------------------------------------------------------------- */\nexport type PdfEditorToolDefinition = {\n  /** EmbedPDF annotation tool id. */\n  id: string\n  label: string\n  glyph: PdfEditorGlyph\n  /** Short hint shown in the tooltip. */\n  hint?: string\n}\nexport type PdfEditorToolGroup = {\n  id: string\n  label: string\n  tools: PdfEditorToolDefinition[]\n}\nexport const PDF_EDITOR_ANNOTATION_TOOL_GROUPS: PdfEditorToolGroup[] = [\n  {\n    id: \"markup\",\n    label: \"Text markup\",\n    tools: [\n      {\n        id: \"highlight\",\n        label: \"Highlight\",\n        glyph: HighlighterGlyph,\n        hint: \"Select text to highlight\",\n      },\n      {\n        id: \"underline\",\n        label: \"Underline\",\n        glyph: UnderlineGlyph,\n        hint: \"Select text to underline\",\n      },\n      {\n        id: \"strikeout\",\n        label: \"Strikethrough\",\n        glyph: StrikethroughGlyph,\n        hint: \"Select text to strike through\",\n      },\n      {\n        id: \"squiggly\",\n        label: \"Squiggly\",\n        glyph: SquigglyGlyph,\n        hint: \"Select text for a squiggly underline\",\n      },\n    ],\n  },\n  {\n    id: \"draw\",\n    label: \"Draw\",\n    tools: [\n      {\n        id: \"ink\",\n        label: \"Pen\",\n        glyph: PenGlyph,\n        hint: \"Draw freehand\",\n      },\n      {\n        id: \"inkHighlighter\",\n        label: \"Highlighter pen\",\n        glyph: BrushGlyph,\n        hint: \"Draw a translucent highlighter stroke\",\n      },\n    ],\n  },\n  {\n    id: \"text\",\n    label: \"Text\",\n    tools: [\n      {\n        id: \"freeText\",\n        label: \"Text box\",\n        glyph: TextGlyph,\n        hint: \"Click or drag to add text\",\n      },\n      {\n        id: \"freeTextCallout\",\n        label: \"Callout\",\n        glyph: CalloutGlyph,\n        hint: \"Drag to add a callout with a leader line\",\n      },\n      {\n        id: \"textComment\",\n        label: \"Sticky note\",\n        glyph: StickyNoteGlyph,\n        hint: \"Click to add a note\",\n      },\n    ],\n  },\n  {\n    id: \"shapes\",\n    label: \"Shapes\",\n    tools: [\n      { id: \"square\", label: \"Rectangle\", glyph: SquareGlyph },\n      { id: \"circle\", label: \"Ellipse\", glyph: CircleGlyph },\n      { id: \"line\", label: \"Line\", glyph: LineGlyph },\n      { id: \"lineArrow\", label: \"Arrow\", glyph: ArrowGlyph },\n      {\n        id: \"polyline\",\n        label: \"Polyline\",\n        glyph: PolylineGlyph,\n        hint: \"Click to add points, double-click to finish\",\n      },\n      {\n        id: \"polygon\",\n        label: \"Polygon\",\n        glyph: PolygonGlyph,\n        hint: \"Click to add points, double-click to finish\",\n      },\n    ],\n  },\n  {\n    id: \"insert\",\n    label: \"Insert\",\n    tools: [\n      {\n        id: \"stamp\",\n        label: \"Image\",\n        glyph: ImageGlyph,\n        hint: \"Click the page to place an image\",\n      },\n      {\n        id: \"link\",\n        label: \"Link area\",\n        glyph: LinkGlyph,\n        hint: \"Drag to add a link rectangle\",\n      },\n      {\n        id: \"insertText\",\n        label: \"Insert text mark\",\n        glyph: InsertTextGlyph,\n        hint: \"Select text to mark an insertion point\",\n      },\n      {\n        id: \"replaceText\",\n        label: \"Replace text mark\",\n        glyph: ReplaceTextGlyph,\n        hint: \"Select text to mark it for replacement\",\n      },\n    ],\n  },\n]\nexport const PDF_EDITOR_ANNOTATION_TOOLS: PdfEditorToolDefinition[] =\n  PDF_EDITOR_ANNOTATION_TOOL_GROUPS.flatMap((group) => group.tools)\nexport const PDF_EDITOR_FORM_TOOLS: PdfEditorToolDefinition[] = [\n  {\n    id: \"formTextField\",\n    label: \"Text field\",\n    glyph: TextFieldGlyph,\n    hint: \"Click or drag to add a text field\",\n  },\n  {\n    id: \"formCheckbox\",\n    label: \"Checkbox\",\n    glyph: CheckboxGlyph,\n    hint: \"Click to add a checkbox\",\n  },\n  {\n    id: \"formRadioButton\",\n    label: \"Radio button\",\n    glyph: RadioGlyph,\n    hint: \"Click to add a radio button\",\n  },\n  {\n    id: \"formCombobox\",\n    label: \"Dropdown\",\n    glyph: ComboboxGlyph,\n    hint: \"Click or drag to add a dropdown\",\n  },\n  {\n    id: \"formListbox\",\n    label: \"List box\",\n    glyph: ListboxGlyph,\n    hint: \"Click or drag to add a list box\",\n  },\n]\nexport const PDF_EDITOR_MODE_OPTIONS: Array<{\n  id: PdfEditorMode\n  label: string\n  glyph: PdfEditorGlyph\n  feature: keyof PdfEditorFeatureFlags | null\n}> = [\n  { id: \"view\", label: \"View\", glyph: EyeGlyph, feature: null },\n  { id: \"annotate\", label: \"Annotate\", glyph: PenGlyph, feature: \"annotate\" },\n  { id: \"sign\", label: \"Sign\", glyph: SignatureGlyph, feature: \"sign\" },\n  { id: \"forms\", label: \"Forms\", glyph: TextFieldGlyph, feature: \"forms\" },\n  { id: \"redact\", label: \"Redact\", glyph: EyeOffGlyph, feature: \"redact\" },\n]\nexport type PdfEditorAnnotationMeta = {\n  label: string\n  glyph: PdfEditorGlyph\n  /** The most representative color of the annotation, if any. */\n  color?: string\n}\nexport function getAnnotationColor(\n  annotation: PdfAnnotationObject\n): string | undefined {\n  const record = annotation as unknown as Record<string, unknown>\n  const candidates = [record.strokeColor, record.fontColor, record.color]\n  for (const candidate of candidates) {\n    if (typeof candidate === \"string\" && candidate.trim()) return candidate\n  }\n  return undefined\n}\nexport function getAnnotationMeta(\n  annotation: PdfAnnotationObject\n): PdfEditorAnnotationMeta {\n  const color = getAnnotationColor(annotation)\n  const intent = annotation.intent ?? \"\"\n  switch (annotation.type) {\n    case PdfAnnotationSubtype.HIGHLIGHT:\n      return { label: \"Highlight\", glyph: HighlighterGlyph, color }\n    case PdfAnnotationSubtype.UNDERLINE:\n      return { label: \"Underline\", glyph: UnderlineGlyph, color }\n    case PdfAnnotationSubtype.STRIKEOUT:\n      return intent === \"StrikeOutTextReplace\"\n        ? { label: \"Replace text\", glyph: ReplaceTextGlyph, color }\n        : { label: \"Strikethrough\", glyph: StrikethroughGlyph, color }\n    case PdfAnnotationSubtype.SQUIGGLY:\n      return { label: \"Squiggly\", glyph: SquigglyGlyph, color }\n    case PdfAnnotationSubtype.INK:\n      return intent === \"InkHighlight\"\n        ? { label: \"Highlighter pen\", glyph: BrushGlyph, color }\n        : { label: \"Ink\", glyph: PenGlyph, color }\n    case PdfAnnotationSubtype.SQUARE:\n      return { label: \"Rectangle\", glyph: SquareGlyph, color }\n    case PdfAnnotationSubtype.CIRCLE:\n      return { label: \"Ellipse\", glyph: CircleGlyph, color }\n    case PdfAnnotationSubtype.LINE:\n      return intent === \"LineArrow\"\n        ? { label: \"Arrow\", glyph: ArrowGlyph, color }\n        : { label: \"Line\", glyph: LineGlyph, color }\n    case PdfAnnotationSubtype.POLYLINE:\n      return { label: \"Polyline\", glyph: PolylineGlyph, color }\n    case PdfAnnotationSubtype.POLYGON:\n      return { label: \"Polygon\", glyph: PolygonGlyph, color }\n    case PdfAnnotationSubtype.FREETEXT:\n      return intent === \"FreeTextCallout\"\n        ? { label: \"Callout\", glyph: CalloutGlyph, color }\n        : { label: \"Text box\", glyph: TextGlyph, color }\n    case PdfAnnotationSubtype.TEXT:\n      return { label: \"Note\", glyph: StickyNoteGlyph, color }\n    case PdfAnnotationSubtype.STAMP:\n      return { label: \"Stamp\", glyph: StampGlyph, color }\n    case PdfAnnotationSubtype.LINK:\n      return { label: \"Link\", glyph: LinkGlyph, color }\n    case PdfAnnotationSubtype.CARET:\n      return { label: \"Insert text\", glyph: InsertTextGlyph, color }\n    case PdfAnnotationSubtype.REDACT:\n      return { label: \"Redaction\", glyph: EyeOffGlyph, color }\n    case PdfAnnotationSubtype.WIDGET:\n      return { label: \"Form field\", glyph: TextFieldGlyph, color }\n    case PdfAnnotationSubtype.FILEATTACHMENT:\n      return { label: \"Attachment\", glyph: PaperclipGlyph, color }\n    default:\n      return { label: \"Annotation\", glyph: FlagGlyph, color }\n  }\n}\n/* -------------------------------------------------------------------------- */\n/* Helpers                                                                    */\n/* -------------------------------------------------------------------------- */\nexport function clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value))\n}\nexport function ensurePdfExtension(fileName: string) {\n  return fileName.toLowerCase().endsWith(\".pdf\") ? fileName : `${fileName}.pdf`\n}\nexport function getPdfFileName(fileName: string | undefined, src?: string) {\n  if (fileName?.trim()) return ensurePdfExtension(fileName.trim())\n  if (src && !/^(blob|data):/i.test(src)) {\n    const pathname = src.split(/[?#]/)[0] ?? \"\"\n    const rawName = pathname.split(\"/\").pop() || \"\"\n    if (rawName) {\n      try {\n        return ensurePdfExtension(decodeURIComponent(rawName))\n      } catch {\n        return ensurePdfExtension(rawName)\n      }\n    }\n  }\n  return \"document.pdf\"\n}\nexport function withFileNameSuffix(fileName: string, suffix: string) {\n  return ensurePdfExtension(fileName.replace(/\\.pdf$/i, \"\") + suffix)\n}\nexport function 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}\nexport function downloadArrayBuffer(\n  buffer: ArrayBuffer | Uint8Array,\n  fileName: string,\n  type = \"application/pdf\"\n) {\n  const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)\n  const copy = new ArrayBuffer(bytes.byteLength)\n  new Uint8Array(copy).set(bytes)\n  downloadBlob(new Blob([copy], { type }), fileName)\n}\nexport function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n  const copy = new ArrayBuffer(bytes.byteLength)\n  new Uint8Array(copy).set(bytes)\n  return copy\n}\nexport function readFileAsArrayBuffer(file: Blob) {\n  return file.arrayBuffer()\n}\nexport function formatBytes(bytes: number | undefined) {\n  if (bytes === undefined || Number.isNaN(bytes)) return \"—\"\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\nexport function formatDateTime(date: Date | undefined | null) {\n  if (!date) return \"—\"\n  const value = date instanceof Date ? date : new Date(date)\n  if (Number.isNaN(value.getTime())) return \"—\"\n  return value.toLocaleString(undefined, {\n    dateStyle: \"medium\",\n    timeStyle: \"short\",\n  })\n}\nexport function formatRelativeDate(date: Date | undefined | null) {\n  if (!date) return \"\"\n  const value = date instanceof Date ? date : new Date(date)\n  if (Number.isNaN(value.getTime())) return \"\"\n  const diff = Date.now() - value.getTime()\n  const minutes = Math.round(diff / 60000)\n  if (minutes < 1) return \"just now\"\n  if (minutes < 60) return `${minutes}m ago`\n  const hours = Math.round(minutes / 60)\n  if (hours < 24) return `${hours}h ago`\n  return value.toLocaleDateString(undefined, {\n    month: \"short\",\n    day: \"numeric\",\n  })\n}\nexport function toNormalizedRect(\n  rect: Rect,\n  pageSize: Size\n): PdfEditorNormalizedRect {\n  const width = pageSize.width || 1\n  const height = pageSize.height || 1\n  return {\n    top: (rect.origin.y / height) * 100,\n    left: (rect.origin.x / width) * 100,\n    width: (rect.size.width / width) * 100,\n    height: (rect.size.height / height) * 100,\n  }\n}\nexport function isTypingTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false\n  if (target.isContentEditable) return true\n  return Boolean(\n    target.closest(\n      \"input, textarea, select, [contenteditable='true'], [contenteditable='']\"\n    )\n  )\n}\nexport function isMacPlatform() {\n  if (typeof navigator === \"undefined\") return false\n  return /mac|iphone|ipad|ipod/i.test(navigator.platform)\n}\nexport function formatShortcut(shortcut: string) {\n  const mac = isMacPlatform()\n  return shortcut\n    .replace(/mod/gi, mac ? \"⌘\" : \"Ctrl\")\n    .replace(/shift/gi, mac ? \"⇧\" : \"Shift\")\n    .replace(/alt/gi, mac ? \"⌥\" : \"Alt\")\n}\nexport function uniqueName(base: string, existing: Iterable<string>) {\n  const taken = new Set(existing)\n  if (!taken.has(base)) return base\n  let index = 2\n  while (taken.has(`${base} ${index}`)) index += 1\n  return `${base} ${index}`\n}\nexport function noop() {}\n/* -------------------------------------------------------------------------- */\n/* Small UI primitives shared by the editor files                             */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorTooltip({\n  label,\n  shortcut,\n  children,\n}: {\n  label: React.ReactNode\n  shortcut?: 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\">\n        <span className=\"inline-flex items-center gap-2\">\n          {label}\n          {shortcut ? (\n            <Kbd className=\"h-4 min-w-4 px-1 text-[10px]\">\n              {formatShortcut(shortcut)}\n            </Kbd>\n          ) : null}\n        </span>\n      </TooltipContent>\n    </Tooltip>\n  )\n}\nexport function PdfEditorToolButton({\n  label,\n  shortcut,\n  active = false,\n  className,\n  children,\n  size = \"icon-sm\",\n  variant = \"ghost\",\n  ...props\n}: Omit<ButtonProps, \"variant\" | \"size\" | \"children\"> & {\n  label: string\n  shortcut?: string\n  active?: boolean\n  size?: ButtonProps[\"size\"]\n  variant?: ButtonProps[\"variant\"]\n  children: React.ReactNode\n}) {\n  return (\n    <PdfEditorTooltip label={label} shortcut={shortcut}>\n      <Button\n        type=\"button\"\n        variant={variant}\n        size={size}\n        aria-label={label}\n        aria-pressed={active}\n        data-active={active ? \"\" : undefined}\n        className={cn(\n          active &&\n            \"bg-accent text-accent-foreground ring-1 ring-ring/40 ring-inset\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </Button>\n    </PdfEditorTooltip>\n  )\n}\nexport function PdfEditorToolbarSeparator({\n  className,\n}: {\n  className?: string\n}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={cn(\"mx-1 h-4 w-px shrink-0 self-center bg-border\", className)}\n    />\n  )\n}\nexport function PdfEditorPanelHeader({\n  title,\n  description,\n  actions,\n  onClose,\n}: {\n  title: React.ReactNode\n  description?: React.ReactNode\n  actions?: React.ReactNode\n  onClose?: () => void\n}) {\n  return (\n    <div className=\"flex shrink-0 items-start justify-between gap-2 border-b px-3 py-2.5\">\n      <div className=\"min-w-0\">\n        <div className=\"truncate text-sm font-medium text-foreground\">\n          {title}\n        </div>\n        {description ? (\n          <div className=\"mt-0.5 text-xs text-muted-foreground\">\n            {description}\n          </div>\n        ) : null}\n      </div>\n      <div className=\"flex shrink-0 items-center gap-1\">\n        {actions}\n        {onClose ? (\n          <PdfEditorToolButton\n            label=\"Close panel\"\n            size=\"icon-xs\"\n            onClick={onClose}\n          >\n            <CloseGlyph className=\"size-3.5\" />\n          </PdfEditorToolButton>\n        ) : null}\n      </div>\n    </div>\n  )\n}\nexport function PdfEditorEmptyState({\n  glyph: Glyph,\n  title,\n  description,\n  action,\n  className,\n}: {\n  glyph?: PdfEditorGlyph\n  title: React.ReactNode\n  description?: React.ReactNode\n  action?: React.ReactNode\n  className?: string\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center gap-2 px-4 py-10 text-center\",\n        className\n      )}\n    >\n      {Glyph ? (\n        <div className=\"grid size-10 place-items-center rounded-full bg-muted text-muted-foreground\">\n          <Glyph className=\"size-5\" />\n        </div>\n      ) : null}\n      <div className=\"text-sm font-medium text-foreground\">{title}</div>\n      {description ? (\n        <div className=\"max-w-56 text-xs text-muted-foreground\">\n          {description}\n        </div>\n      ) : null}\n      {action ? <div className=\"mt-2\">{action}</div> : null}\n    </div>\n  )\n}\nexport function PdfEditorSection({\n  title,\n  actions,\n  children,\n  className,\n}: {\n  title?: React.ReactNode\n  actions?: React.ReactNode\n  children: React.ReactNode\n  className?: string\n}) {\n  return (\n    <section className={cn(\"space-y-2\", className)}>\n      {title || actions ? (\n        <div className=\"flex items-center justify-between gap-2\">\n          {title ? (\n            <div className=\"text-xs font-medium text-muted-foreground\">\n              {title}\n            </div>\n          ) : (\n            <span />\n          )}\n          {actions}\n        </div>\n      ) : null}\n      {children}\n    </section>\n  )\n}\nexport function PdfEditorFieldLabel({\n  children,\n  htmlFor,\n  className,\n}: {\n  children: React.ReactNode\n  htmlFor?: string\n  className?: string\n}) {\n  return (\n    <label\n      htmlFor={htmlFor}\n      className={cn(\"block text-xs font-medium text-foreground\", className)}\n    >\n      {children}\n    </label>\n  )\n}\nexport function PdfEditorCheckbox({\n  checked,\n  disabled,\n  label,\n  description,\n  onCheckedChange,\n  className,\n}: {\n  checked: boolean\n  disabled?: boolean\n  label: React.ReactNode\n  description?: React.ReactNode\n  onCheckedChange: (checked: boolean) => void\n  className?: string\n}) {\n  return (\n    <button\n      type=\"button\"\n      role=\"checkbox\"\n      aria-checked={checked}\n      disabled={disabled}\n      onClick={() => onCheckedChange(!checked)}\n      className={cn(\n        \"flex w-full items-start gap-2 rounded-md px-1 py-1 text-left text-sm outline-none hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"mt-0.5 grid size-4 shrink-0 place-items-center rounded border transition-colors\",\n          checked\n            ? \"border-primary bg-primary text-primary-foreground\"\n            : \"border-input bg-background\"\n        )}\n      >\n        {checked ? <CheckGlyph className=\"size-3\" /> : null}\n      </span>\n      <span className=\"min-w-0\">\n        <span className=\"block leading-4\">{label}</span>\n        {description ? (\n          <span className=\"block text-xs text-muted-foreground\">\n            {description}\n          </span>\n        ) : null}\n      </span>\n    </button>\n  )\n}\nexport function PdfEditorSwatch({\n  color,\n  active,\n  label,\n  onClick,\n  size = \"md\",\n}: {\n  color: string\n  active?: boolean\n  label?: string\n  onClick?: () => void\n  size?: \"sm\" | \"md\"\n}) {\n  const isTransparent = color === \"transparent\"\n  const accessibleLabel = label ?? (isTransparent ? \"No fill\" : color)\n  const swatch = (\n    <button\n      type=\"button\"\n      aria-label={accessibleLabel}\n      aria-pressed={active}\n      onClick={onClick}\n      className={cn(\n        \"relative shrink-0 rounded-full border border-black/10 transition-transform outline-none focus-visible:ring-2 focus-visible:ring-ring dark:border-white/15\",\n        size === \"sm\" ? \"size-4\" : \"size-5\",\n        active && \"ring-2 ring-ring ring-offset-2 ring-offset-background\"\n      )}\n      style={\n        isTransparent\n          ? {\n              backgroundImage:\n                \"linear-gradient(45deg, transparent 45%, #ef4444 45%, #ef4444 55%, transparent 55%)\",\n              backgroundColor: \"#fff\",\n            }\n          : { backgroundColor: color }\n      }\n    />\n  )\n  return label ? (\n    <PdfEditorTooltip label={label}>{swatch}</PdfEditorTooltip>\n  ) : (\n    swatch\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Scroll area with a resolvable viewport (mirrors the PDF viewer)            */\n/* -------------------------------------------------------------------------- */\nexport type PdfEditorScrollAreaViewportResolver = (\n  container: HTMLDivElement\n) => HTMLDivElement | null\nconst DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR =\n  '[data-slot=\"scroll-area-viewport\"]'\nexport function resolveDefaultScrollAreaViewport(container: HTMLDivElement) {\n  return container.querySelector<HTMLDivElement>(\n    DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR\n  )\n}\nexport const PdfEditorScrollAreaResolverContext =\n  React.createContext<PdfEditorScrollAreaViewportResolver>(\n    resolveDefaultScrollAreaViewport\n  )\nfunction setRefValue<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (!ref) return\n  if (typeof ref === \"function\") {\n    ref(value)\n  } else {\n    ref.current = value\n  }\n}\nexport function PdfEditorScrollArea({\n  children,\n  className,\n  orientation = \"vertical\",\n  scrollbarGutter = true,\n  viewportClassName,\n  viewportProps,\n  viewportRef,\n}: {\n  children: React.ReactNode\n  className?: string\n  orientation?: \"vertical\" | \"horizontal\" | \"both\"\n  scrollbarGutter?: boolean\n  viewportClassName?: string\n  viewportProps?: React.HTMLAttributes<HTMLDivElement>\n  viewportRef?: React.Ref<HTMLDivElement>\n}) {\n  const resolveViewport = React.useContext(PdfEditorScrollAreaResolverContext)\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  const { className: viewportPropsClassName, ...resolvedViewportProps } =\n    viewportProps ?? {}\n  const setViewportRef = React.useCallback(\n    (viewport: HTMLDivElement | null) => {\n      setRefValue(viewportRef, viewport)\n    },\n    [viewportRef]\n  )\n  React.useLayoutEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n    const viewport = resolveViewport(container)\n    if (!viewport) {\n      console.error(\n        `PDFEditor could not resolve the scroll viewport. Add ${DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR} to your ScrollArea viewport or pass resolveScrollAreaViewport.`\n      )\n      return\n    }\n    setViewportRef(viewport)\n    return () => setViewportRef(null)\n  }, [resolveViewport, setViewportRef])\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"pdf-editor-scroll-area\"\n      className={cn(\"size-full min-h-0\", className)}\n    >\n      <InlineScrollArea2\n        className=\"size-full min-h-0\"\n        orientation={orientation}\n        scrollbarGutter={scrollbarGutter}\n      >\n        <div\n          {...resolvedViewportProps}\n          data-slot=\"pdf-editor-scroll-content\"\n          className={cn(\n            \"min-h-full\",\n            viewportPropsClassName,\n            viewportClassName\n          )}\n        >\n          {children}\n        </div>\n      </InlineScrollArea2>\n    </div>\n  )\n}\n\ntype ButtonProps = React.ComponentProps<typeof Button>\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}\n",
      "type": "registry:ui",
      "target": "@components/extend/pdf-editor-shared.tsx"
    },
    {
      "path": "components/extend/pdf-editor-dialogs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport type { DocumentState } from \"@embedpdf/core\"\nimport {\n  useDocumentPermissions,\n  useDocumentState,\n  useRegistry,\n} from \"@embedpdf/core/react\"\nimport {\n  ignore,\n  PdfActionType,\n  PdfAnnotationBorderStyle,\n  PdfAnnotationReplyType,\n  PdfAnnotationSubtype,\n  PdfBlendMode,\n  PdfErrorCode,\n  PdfPermissionFlag,\n  PdfZoomMode,\n  uuidV4,\n  type PdfLinkAnnoObject,\n  type PdfLinkTarget,\n  type PdfMetadataObject,\n  type PdfPrintOptions,\n} from \"@embedpdf/models\"\nimport { useAnnotationCapability } from \"@embedpdf/plugin-annotation/react\"\nimport { useCapture } from \"@embedpdf/plugin-capture/react\"\nimport { useDocumentManagerCapability } from \"@embedpdf/plugin-document-manager/react\"\nimport { usePrint } from \"@embedpdf/plugin-print/react\"\nimport { useScroll } from \"@embedpdf/plugin-scroll/react\"\nimport { useSelectionCapability } from \"@embedpdf/plugin-selection/react\"\nimport {\n  SignatureDrawPad,\n  SignatureMode,\n  SignatureTypePad,\n  useSignatureCapability,\n  useSignatureUpload,\n  type SignatureDrawPadHandle,\n  type SignatureFieldDefinition,\n  type SignatureTypePadHandle,\n} from \"@embedpdf/plugin-signature/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { ColorPicker } from \"@/components/extend/document-color-picker\"\nimport {\n  CopyGlyph,\n  downloadBlob,\n  DownloadGlyph,\n  formatBytes,\n  formatDateTime,\n  formatShortcut,\n  KeyGlyph,\n  LockGlyph,\n  PDF_EDITOR_SIGNATURE_FONTS,\n  PdfEditorCheckbox,\n  PdfEditorFieldLabel,\n  PdfEditorSection,\n  PdfEditorSwatch,\n  ShieldGlyph,\n  UnlockGlyph,\n  usePdfEditor,\n} from \"@/components/extend/pdf-editor-shared\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\n/* -------------------------------------------------------------------------- */\n/* Confirm                                                                    */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorConfirmDialog({\n  open,\n  onOpenChange,\n  title,\n  description,\n  confirmLabel = \"Confirm\",\n  destructive = false,\n  onConfirm,\n}: {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  title: string\n  description?: string\n  confirmLabel?: string\n  destructive?: boolean\n  onConfirm: () => void\n}) {\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n          {description ? (\n            <DialogDescription>{description}</DialogDescription>\n          ) : null}\n        </DialogHeader>\n        <DialogFooter>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={() => onOpenChange(false)}\n          >\n            Cancel\n          </Button>\n          <Button\n            type=\"button\"\n            variant={destructive ? \"destructive\" : \"default\"}\n            onClick={() => {\n              onConfirm()\n              onOpenChange(false)\n            }}\n          >\n            {confirmLabel}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Print                                                                      */\n/* -------------------------------------------------------------------------- */\ntype PrintPageSelection = \"all\" | \"current\" | \"custom\"\nexport function PdfEditorPrintDialog({\n  documentId,\n  open,\n  onOpenChange,\n}: {\n  documentId: string\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}) {\n  const { provides: print } = usePrint(documentId)\n  const { state: scrollState } = useScroll(documentId)\n  const { notify } = usePdfEditor()\n  const [selection, setSelection] = React.useState<PrintPageSelection>(\"all\")\n  const [customPages, setCustomPages] = React.useState(\"\")\n  const [includeAnnotations, setIncludeAnnotations] = React.useState(true)\n  const [isPrinting, setIsPrinting] = React.useState(false)\n  const [error, setError] = React.useState<string | null>(null)\n  const [previousOpen, setPreviousOpen] = React.useState(open)\n  if (previousOpen !== open) {\n    setPreviousOpen(open)\n    if (!open) {\n      setSelection(\"all\")\n      setCustomPages(\"\")\n      setIncludeAnnotations(true)\n      setIsPrinting(false)\n      setError(null)\n    }\n  }\n  const canSubmit =\n    !isPrinting && (selection !== \"custom\" || customPages.trim().length > 0)\n  const handlePrint = () => {\n    if (!print || !canSubmit) return\n    const options: PdfPrintOptions = {\n      includeAnnotations,\n      pageRange:\n        selection === \"current\"\n          ? String(scrollState.currentPage)\n          : selection === \"custom\"\n            ? customPages.trim()\n            : undefined,\n    }\n    setIsPrinting(true)\n    setError(null)\n    print.print(options).wait(\n      () => {\n        setIsPrinting(false)\n        onOpenChange(false)\n      },\n      (failure) => {\n        setIsPrinting(false)\n        const message =\n          failure?.reason?.message ?? \"The document could not be printed.\"\n        setError(message)\n        notify(message, \"error\")\n      }\n    )\n  }\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>Print</DialogTitle>\n          <DialogDescription>\n            Prepares a print-ready copy and opens the browser print dialog.\n          </DialogDescription>\n        </DialogHeader>\n        <InlineDialogPanel className=\"space-y-5\">\n          <PdfEditorSection title=\"Pages\">\n            <div className=\"space-y-1\">\n              <PdfEditorCheckbox\n                checked={selection === \"all\"}\n                onCheckedChange={() => setSelection(\"all\")}\n                label=\"All pages\"\n              />\n              <PdfEditorCheckbox\n                checked={selection === \"current\"}\n                onCheckedChange={() => setSelection(\"current\")}\n                label={`Current page (${scrollState.currentPage || 1})`}\n              />\n              <PdfEditorCheckbox\n                checked={selection === \"custom\"}\n                onCheckedChange={() => setSelection(\"custom\")}\n                label=\"Page range\"\n              />\n            </div>\n            <Input\n              placeholder=\"e.g. 1-3, 5, 8-10\"\n              value={customPages}\n              disabled={selection !== \"custom\"}\n              onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n                setCustomPages(event.target.value)\n              }\n              onFocus={() => setSelection(\"custom\")}\n            />\n            <div className=\"text-xs text-muted-foreground\">\n              {scrollState.totalPages} pages in this document.\n            </div>\n          </PdfEditorSection>\n          <PdfEditorSection title=\"Options\">\n            <PdfEditorCheckbox\n              checked={includeAnnotations}\n              onCheckedChange={setIncludeAnnotations}\n              label=\"Include annotations and form values\"\n            />\n          </PdfEditorSection>\n          {error ? (\n            <div className=\"text-xs text-destructive\">{error}</div>\n          ) : null}\n        </InlineDialogPanel>\n        <DialogFooter>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            disabled={isPrinting}\n            onClick={() => onOpenChange(false)}\n          >\n            Cancel\n          </Button>\n          <InlineLoadingButton\n            type=\"button\"\n            loading={isPrinting}\n            disabled={!canSubmit || !print}\n            onClick={handlePrint}\n          >\n            Print\n          </InlineLoadingButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Capture                                                                    */\n/* -------------------------------------------------------------------------- */\ntype CaptureResult = {\n  pageIndex: number\n  blob: Blob\n  imageType: string\n  url: string\n}\nexport function PdfEditorCaptureDialog({ documentId }: { documentId: string }) {\n  const { provides: capture } = useCapture(documentId)\n  const { fileName, notify, onCapture } = usePdfEditor()\n  const [result, setResult] = React.useState<CaptureResult | null>(null)\n  const onCaptureRef = React.useRef(onCapture)\n  React.useEffect(() => {\n    onCaptureRef.current = onCapture\n  })\n  React.useEffect(() => {\n    if (!capture) return\n    return capture.onCaptureArea((event) => {\n      onCaptureRef.current?.(event)\n      setResult((previous) => {\n        if (previous) URL.revokeObjectURL(previous.url)\n        return {\n          pageIndex: event.pageIndex,\n          blob: event.blob,\n          imageType: event.imageType,\n          url: URL.createObjectURL(event.blob),\n        }\n      })\n    })\n  }, [capture])\n  React.useEffect(\n    () => () => {\n      if (result) URL.revokeObjectURL(result.url)\n    },\n    [result]\n  )\n  const close = () => setResult(null)\n  const extension = result?.imageType.split(\"/\")[1] ?? \"png\"\n  const captureFileName = `${fileName.replace(/\\.pdf$/i, \"\")}-page-${(result?.pageIndex ?? 0) + 1}.${extension}`\n  const canCopy =\n    typeof navigator !== \"undefined\" &&\n    typeof window !== \"undefined\" &&\n    \"ClipboardItem\" in window &&\n    Boolean(navigator.clipboard?.write)\n  return (\n    <Dialog\n      open={result !== null}\n      onOpenChange={(open) => {\n        if (!open) close()\n      }}\n    >\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle>Captured area</DialogTitle>\n          <DialogDescription>\n            Page {(result?.pageIndex ?? 0) + 1} ·{\" \"}\n            {formatBytes(result?.blob.size)}\n          </DialogDescription>\n        </DialogHeader>\n        <InlineDialogPanel>\n          <div className=\"grid place-items-center rounded-lg border bg-muted/40 p-3\">\n            {result ? (\n              <img\n                src={result.url}\n                alt={`Captured area from page ${result.pageIndex + 1}`}\n                className=\"max-h-[52vh] max-w-full rounded-md shadow-sm\"\n              />\n            ) : null}\n          </div>\n        </InlineDialogPanel>\n        <DialogFooter>\n          {canCopy ? (\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              onClick={async () => {\n                if (!result) return\n                try {\n                  await navigator.clipboard.write([\n                    new ClipboardItem({ [result.imageType]: result.blob }),\n                  ])\n                  notify(\"Image copied to the clipboard.\", \"success\")\n                } catch {\n                  notify(\"The image could not be copied.\", \"error\")\n                }\n              }}\n            >\n              <CopyGlyph className=\"size-4\" />\n              Copy image\n            </Button>\n          ) : null}\n          <Button\n            type=\"button\"\n            onClick={() => {\n              if (!result) return\n              downloadBlob(result.blob, captureFileName)\n              close()\n            }}\n          >\n            <DownloadGlyph className=\"size-4\" />\n            Download\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Security                                                                   */\n/* -------------------------------------------------------------------------- */\nconst PERMISSION_OPTIONS: Array<{\n  flag: PdfPermissionFlag\n  label: string\n  description: string\n}> = [\n  {\n    flag: PdfPermissionFlag.Print,\n    label: \"Print\",\n    description: \"Allow printing, possibly at reduced quality.\",\n  },\n  {\n    flag: PdfPermissionFlag.PrintHighQuality,\n    label: \"Print in high quality\",\n    description: \"Allow full-resolution printing.\",\n  },\n  {\n    flag: PdfPermissionFlag.ModifyContents,\n    label: \"Modify contents\",\n    description: \"Allow editing the page content.\",\n  },\n  {\n    flag: PdfPermissionFlag.CopyContents,\n    label: \"Copy text and images\",\n    description: \"Allow copying and extracting content.\",\n  },\n  {\n    flag: PdfPermissionFlag.ModifyAnnotations,\n    label: \"Annotate\",\n    description: \"Allow adding and changing annotations.\",\n  },\n  {\n    flag: PdfPermissionFlag.FillForms,\n    label: \"Fill forms\",\n    description: \"Allow filling in existing form fields.\",\n  },\n  {\n    flag: PdfPermissionFlag.ExtractForAccessibility,\n    label: \"Extract for accessibility\",\n    description: \"Allow assistive technologies to read the content.\",\n  },\n  {\n    flag: PdfPermissionFlag.AssembleDocument,\n    label: \"Assemble pages\",\n    description: \"Allow inserting, rotating, and deleting pages.\",\n  },\n]\nfunction PasswordInput({\n  id,\n  value,\n  onChange,\n  placeholder,\n  autoFocus,\n  onEnter,\n}: {\n  id?: string\n  value: string\n  onChange: (value: string) => void\n  placeholder?: string\n  autoFocus?: boolean\n  onEnter?: () => void\n}) {\n  return (\n    <Input\n      id={id}\n      type=\"password\"\n      autoComplete=\"off\"\n      value={value}\n      placeholder={placeholder}\n      autoFocus={autoFocus}\n      onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n        onChange(event.target.value)\n      }\n      onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {\n        if (event.key === \"Enter\") onEnter?.()\n      }}\n    />\n  )\n}\nexport function PdfEditorSecurityDialog({\n  documentId,\n  open,\n  onOpenChange,\n}: {\n  documentId: string\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}) {\n  const { provides: documentManager } = useDocumentManagerCapability()\n  const documentState = useDocumentState(documentId)\n  const permissions = useDocumentPermissions(documentId)\n  const { notify } = usePdfEditor()\n  const document = documentState?.document ?? null\n  const isEncrypted = document?.isEncrypted ?? false\n  const isOwnerUnlocked = document?.isOwnerUnlocked ?? false\n  const needsOwnerUnlock = isEncrypted && !isOwnerUnlocked\n  const [unlockPassword, setUnlockPassword] = React.useState(\"\")\n  const [isUnlocking, setIsUnlocking] = React.useState(false)\n  const [requireOpenPassword, setRequireOpenPassword] = React.useState(false)\n  const [openPassword, setOpenPassword] = React.useState(\"\")\n  const [confirmOpenPassword, setConfirmOpenPassword] = React.useState(\"\")\n  const [restrictPermissions, setRestrictPermissions] = React.useState(false)\n  const [ownerPassword, setOwnerPassword] = React.useState(\"\")\n  const [confirmOwnerPassword, setConfirmOwnerPassword] = React.useState(\"\")\n  const [allowedFlags, setAllowedFlags] = React.useState<\n    Set<PdfPermissionFlag>\n  >(() => new Set(PERMISSION_OPTIONS.map((option) => option.flag)))\n  const [isApplying, setIsApplying] = React.useState(false)\n  const [error, setError] = React.useState<string | null>(null)\n  const [previousOpen, setPreviousOpen] = React.useState(open)\n  if (previousOpen !== open) {\n    setPreviousOpen(open)\n    if (open) {\n      setUnlockPassword(\"\")\n      setIsUnlocking(false)\n      setRequireOpenPassword(false)\n      setOpenPassword(\"\")\n      setConfirmOpenPassword(\"\")\n      setRestrictPermissions(false)\n      setOwnerPassword(\"\")\n      setConfirmOwnerPassword(\"\")\n      setAllowedFlags(new Set(PERMISSION_OPTIONS.map((option) => option.flag)))\n      setIsApplying(false)\n      setError(null)\n    }\n  }\n  const openPasswordValid =\n    !requireOpenPassword ||\n    (openPassword.length > 0 && openPassword === confirmOpenPassword)\n  const ownerPasswordValid =\n    !restrictPermissions ||\n    (ownerPassword.length > 0 && ownerPassword === confirmOwnerPassword)\n  const canApply =\n    !isApplying &&\n    (requireOpenPassword || restrictPermissions) &&\n    openPasswordValid &&\n    ownerPasswordValid\n  const handleUnlock = () => {\n    if (!documentManager || !unlockPassword.trim()) return\n    setIsUnlocking(true)\n    setError(null)\n    documentManager.unlockOwnerPermissions(documentId, unlockPassword).wait(\n      (success) => {\n        setIsUnlocking(false)\n        if (success) {\n          setUnlockPassword(\"\")\n          notify(\"Owner permissions unlocked.\", \"success\")\n        } else {\n          setError(\"The owner password is incorrect.\")\n        }\n      },\n      (failure) => {\n        setIsUnlocking(false)\n        setError(\n          failure?.reason?.message ?? \"The document could not be unlocked.\"\n        )\n      }\n    )\n  }\n  const handleApply = () => {\n    if (!documentManager || !canApply) return\n    let flags = PdfPermissionFlag.AllowAll as number\n    if (restrictPermissions) {\n      flags = 0\n      for (const flag of allowedFlags) flags |= flag\n    }\n    const userPassword = requireOpenPassword ? openPassword : \"\"\n    const resolvedOwnerPassword = restrictPermissions\n      ? ownerPassword\n      : requireOpenPassword\n        ? openPassword\n        : \"\"\n    setIsApplying(true)\n    setError(null)\n    documentManager\n      .setDocumentEncryption(documentId, {\n        userPassword,\n        ownerPassword: resolvedOwnerPassword,\n        allowedFlags: flags,\n      })\n      .wait(\n        (success) => {\n          setIsApplying(false)\n          if (success) {\n            notify(\n              \"Protection is applied the next time you download the document.\",\n              \"success\"\n            )\n            onOpenChange(false)\n          } else {\n            setError(\"The protection settings could not be applied.\")\n          }\n        },\n        (failure) => {\n          setIsApplying(false)\n          setError(\n            failure?.reason?.message ??\n              \"The protection settings could not be applied.\"\n          )\n        }\n      )\n  }\n  const handleRemove = () => {\n    if (!documentManager) return\n    setIsApplying(true)\n    setError(null)\n    documentManager.removeEncryption(documentId).wait(\n      (success) => {\n        setIsApplying(false)\n        if (success) {\n          notify(\n            \"Protection is removed the next time you download the document.\",\n            \"success\"\n          )\n          onOpenChange(false)\n        } else {\n          setError(\"The protection could not be removed.\")\n        }\n      },\n      (failure) => {\n        setIsApplying(false)\n        setError(\n          failure?.reason?.message ?? \"The protection could not be removed.\"\n        )\n      }\n    )\n  }\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-lg\">\n        <DialogHeader>\n          <DialogTitle>Security</DialogTitle>\n          <DialogDescription>\n            Passwords and permissions are written into the PDF when you download\n            it.\n          </DialogDescription>\n        </DialogHeader>\n        <InlineDialogPanel className=\"space-y-5\">\n          <PdfEditorSection title=\"Current status\">\n            <div className=\"flex flex-wrap items-center gap-2 text-sm\">\n              {isEncrypted ? (\n                <Badge variant=\"secondary\">\n                  <LockGlyph className=\"size-3\" />\n                  Encrypted\n                </Badge>\n              ) : (\n                <Badge variant=\"outline\">\n                  <UnlockGlyph className=\"size-3\" />\n                  Not encrypted\n                </Badge>\n              )}\n              {isEncrypted ? (\n                <Badge variant={isOwnerUnlocked ? \"secondary\" : \"outline\"}>\n                  <KeyGlyph className=\"size-3\" />\n                  {isOwnerUnlocked ? \"Owner unlocked\" : \"Owner locked\"}\n                </Badge>\n              ) : null}\n            </div>\n            <div className=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n              {PERMISSION_OPTIONS.map((option) => {\n                const allowed = permissions.hasPermission(option.flag)\n                return (\n                  <div key={option.flag} className=\"flex items-center gap-1.5\">\n                    <span\n                      aria-hidden=\"true\"\n                      className={cn(\n                        \"size-1.5 rounded-full\",\n                        allowed ? \"bg-emerald-500\" : \"bg-destructive\"\n                      )}\n                    />\n                    <span className={cn(!allowed && \"line-through\")}>\n                      {option.label}\n                    </span>\n                  </div>\n                )\n              })}\n            </div>\n          </PdfEditorSection>\n\n          {needsOwnerUnlock ? (\n            <PdfEditorSection title=\"Unlock owner permissions\">\n              <div className=\"text-xs text-muted-foreground\">\n                Enter the owner password to change protection settings.\n              </div>\n              <div className=\"flex gap-2\">\n                <PasswordInput\n                  value={unlockPassword}\n                  placeholder=\"Owner password\"\n                  onChange={setUnlockPassword}\n                  onEnter={handleUnlock}\n                />\n                <InlineLoadingButton\n                  type=\"button\"\n                  variant=\"outline\"\n                  loading={isUnlocking}\n                  disabled={!unlockPassword.trim()}\n                  onClick={handleUnlock}\n                >\n                  Unlock\n                </InlineLoadingButton>\n              </div>\n            </PdfEditorSection>\n          ) : null}\n\n          <PdfEditorSection title=\"Open password\">\n            <PdfEditorCheckbox\n              checked={requireOpenPassword}\n              disabled={needsOwnerUnlock}\n              onCheckedChange={setRequireOpenPassword}\n              label=\"Require a password to open the document\"\n            />\n            {requireOpenPassword ? (\n              <div className=\"grid gap-2 sm:grid-cols-2\">\n                <PasswordInput\n                  value={openPassword}\n                  placeholder=\"Password\"\n                  onChange={setOpenPassword}\n                />\n                <PasswordInput\n                  value={confirmOpenPassword}\n                  placeholder=\"Confirm password\"\n                  onChange={setConfirmOpenPassword}\n                />\n                {!openPasswordValid && confirmOpenPassword ? (\n                  <div className=\"text-xs text-destructive sm:col-span-2\">\n                    The passwords do not match.\n                  </div>\n                ) : null}\n              </div>\n            ) : null}\n          </PdfEditorSection>\n\n          <PdfEditorSection title=\"Permissions\">\n            <PdfEditorCheckbox\n              checked={restrictPermissions}\n              disabled={needsOwnerUnlock}\n              onCheckedChange={setRestrictPermissions}\n              label=\"Restrict what readers can do\"\n              description=\"Requires an owner password to lift the restrictions later.\"\n            />\n            {restrictPermissions ? (\n              <div className=\"space-y-3\">\n                <div className=\"grid gap-2 sm:grid-cols-2\">\n                  <PasswordInput\n                    value={ownerPassword}\n                    placeholder=\"Owner password\"\n                    onChange={setOwnerPassword}\n                  />\n                  <PasswordInput\n                    value={confirmOwnerPassword}\n                    placeholder=\"Confirm owner password\"\n                    onChange={setConfirmOwnerPassword}\n                  />\n                  {!ownerPasswordValid && confirmOwnerPassword ? (\n                    <div className=\"text-xs text-destructive sm:col-span-2\">\n                      The passwords do not match.\n                    </div>\n                  ) : null}\n                </div>\n                <div className=\"grid gap-1 sm:grid-cols-2\">\n                  {PERMISSION_OPTIONS.map((option) => (\n                    <PdfEditorCheckbox\n                      key={option.flag}\n                      checked={allowedFlags.has(option.flag)}\n                      onCheckedChange={(checked) =>\n                        setAllowedFlags((previous) => {\n                          const next = new Set(previous)\n                          if (checked) {\n                            next.add(option.flag)\n                          } else {\n                            next.delete(option.flag)\n                          }\n                          return next\n                        })\n                      }\n                      label={option.label}\n                      description={option.description}\n                    />\n                  ))}\n                </div>\n              </div>\n            ) : null}\n          </PdfEditorSection>\n\n          {error ? (\n            <div className=\"text-xs text-destructive\">{error}</div>\n          ) : null}\n        </InlineDialogPanel>\n        <DialogFooter>\n          {isEncrypted && !needsOwnerUnlock ? (\n            <Button\n              type=\"button\"\n              disabled={isApplying}\n              onClick={handleRemove}\n              variant={\"outline\"}\n              className={cn(\n                \"border-destructive/32 text-destructive hover:bg-destructive/10\",\n                \"sm:mr-auto\"\n              )}\n            >\n              Remove protection\n            </Button>\n          ) : null}\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            disabled={isApplying}\n            onClick={() => onOpenChange(false)}\n          >\n            Cancel\n          </Button>\n          <InlineLoadingButton\n            type=\"button\"\n            loading={isApplying}\n            disabled={!canApply || needsOwnerUnlock}\n            onClick={handleApply}\n          >\n            <ShieldGlyph className=\"size-4\" />\n            Apply protection\n          </InlineLoadingButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Document properties (metadata)                                             */\n/* -------------------------------------------------------------------------- */\ntype EditableMetadata = {\n  title: string\n  author: string\n  subject: string\n  keywords: string\n  creator: string\n  producer: string\n}\nconst EMPTY_METADATA: EditableMetadata = {\n  title: \"\",\n  author: \"\",\n  subject: \"\",\n  keywords: \"\",\n  creator: \"\",\n  producer: \"\",\n}\nexport function PdfEditorPropertiesDialog({\n  documentId,\n  open,\n  onOpenChange,\n}: {\n  documentId: string\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}) {\n  const { registry } = useRegistry()\n  const documentState = useDocumentState(documentId)\n  const permissions = useDocumentPermissions(documentId)\n  const { fileName, notify } = usePdfEditor()\n  const document = documentState?.document ?? null\n  const [metadata, setMetadata] =\n    React.useState<EditableMetadata>(EMPTY_METADATA)\n  const [dates, setDates] = React.useState<{\n    creationDate: Date | null\n    modificationDate: Date | null\n  }>({ creationDate: null, modificationDate: null })\n  const [isLoading, setIsLoading] = React.useState(\n    Boolean(open && registry && document)\n  )\n  const [isSaving, setIsSaving] = React.useState(false)\n  const [error, setError] = React.useState<string | null>(null)\n  const [metadataRequest, setMetadataRequest] = React.useState({\n    document,\n    open,\n    registry,\n  })\n  if (\n    metadataRequest.document !== document ||\n    metadataRequest.open !== open ||\n    metadataRequest.registry !== registry\n  ) {\n    setMetadataRequest({ document, open, registry })\n    setIsLoading(Boolean(open && registry && document))\n    setError(null)\n  }\n  React.useEffect(() => {\n    if (!open || !registry || !document) return\n    let cancelled = false\n    registry\n      .getEngine()\n      .getMetadata(document)\n      .wait(\n        (result: PdfMetadataObject) => {\n          if (cancelled) return\n          setMetadata({\n            title: result.title ?? \"\",\n            author: result.author ?? \"\",\n            subject: result.subject ?? \"\",\n            keywords: result.keywords ?? \"\",\n            creator: result.creator ?? \"\",\n            producer: result.producer ?? \"\",\n          })\n          setDates({\n            creationDate: result.creationDate ?? null,\n            modificationDate: result.modificationDate ?? null,\n          })\n          setIsLoading(false)\n        },\n        (failure) => {\n          if (cancelled) return\n          setError(\n            failure?.reason?.message ??\n              \"The document metadata could not be read.\"\n          )\n          setIsLoading(false)\n        }\n      )\n    return () => {\n      cancelled = true\n    }\n  }, [document, open, registry])\n  const updateField = (key: keyof EditableMetadata, value: string) =>\n    setMetadata((previous) => ({ ...previous, [key]: value }))\n  const handleSave = () => {\n    if (!registry || !document) return\n    setIsSaving(true)\n    setError(null)\n    registry\n      .getEngine()\n      .setMetadata(document, {\n        title: metadata.title,\n        author: metadata.author,\n        subject: metadata.subject,\n        keywords: metadata.keywords,\n        creator: metadata.creator,\n        producer: metadata.producer,\n        modificationDate: new Date(),\n      })\n      .wait(\n        () => {\n          setIsSaving(false)\n          notify(\"Document properties saved.\", \"success\")\n          onOpenChange(false)\n        },\n        (failure) => {\n          setIsSaving(false)\n          setError(\n            failure?.reason?.message ??\n              \"The document metadata could not be saved.\"\n          )\n        }\n      )\n  }\n  const firstPage = document?.pages[0]\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-lg\">\n        <DialogHeader>\n          <DialogTitle>Document properties</DialogTitle>\n          <DialogDescription>{fileName}</DialogDescription>\n        </DialogHeader>\n        <InlineDialogPanel className=\"space-y-5\">\n          {isLoading ? (\n            <div className=\"grid place-items-center py-8\">\n              <InlineSpinner className=\"size-4\" />\n            </div>\n          ) : (\n            <>\n              <PdfEditorSection title=\"Description\">\n                <div className=\"grid gap-3\">\n                  {(\n                    [\n                      [\"title\", \"Title\"],\n                      [\"author\", \"Author\"],\n                      [\"subject\", \"Subject\"],\n                      [\"keywords\", \"Keywords\"],\n                      [\"creator\", \"Creator\"],\n                      [\"producer\", \"Producer\"],\n                    ] as Array<[keyof EditableMetadata, string]>\n                  ).map(([key, label]) => (\n                    <div key={key} className=\"grid gap-1\">\n                      <PdfEditorFieldLabel htmlFor={`pdf-editor-meta-${key}`}>\n                        {label}\n                      </PdfEditorFieldLabel>\n                      <Input\n                        id={`pdf-editor-meta-${key}`}\n                        value={metadata[key]}\n                        disabled={!permissions.canModifyContents}\n                        onChange={(\n                          event: React.ChangeEvent<HTMLInputElement>\n                        ) => updateField(key, event.target.value)}\n                        className={cn(\"h-8 px-2.5\")}\n                      />\n                    </div>\n                  ))}\n                </div>\n              </PdfEditorSection>\n              <PdfEditorSection title=\"Details\">\n                <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs\">\n                  <dt className=\"text-muted-foreground\">Pages</dt>\n                  <dd>{document?.pageCount ?? \"—\"}</dd>\n                  <dt className=\"text-muted-foreground\">Page size</dt>\n                  <dd>\n                    {firstPage\n                      ? `${Math.round(firstPage.size.width)} × ${Math.round(firstPage.size.height)} pt`\n                      : \"—\"}\n                  </dd>\n                  <dt className=\"text-muted-foreground\">Created</dt>\n                  <dd>{formatDateTime(dates.creationDate)}</dd>\n                  <dt className=\"text-muted-foreground\">Modified</dt>\n                  <dd>{formatDateTime(dates.modificationDate)}</dd>\n                  <dt className=\"text-muted-foreground\">Security</dt>\n                  <dd>\n                    {document?.isEncrypted\n                      ? \"Password protected\"\n                      : \"No protection\"}\n                  </dd>\n                </dl>\n              </PdfEditorSection>\n              {error ? (\n                <div className=\"text-xs text-destructive\">{error}</div>\n              ) : null}\n            </>\n          )}\n        </InlineDialogPanel>\n        <DialogFooter>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            disabled={isSaving}\n            onClick={() => onOpenChange(false)}\n          >\n            Cancel\n          </Button>\n          <InlineLoadingButton\n            type=\"button\"\n            loading={isSaving}\n            disabled={isLoading || !permissions.canModifyContents}\n            onClick={handleSave}\n          >\n            Save\n          </InlineLoadingButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Signature creation                                                         */\n/* -------------------------------------------------------------------------- */\ntype SignatureCreationTab = \"draw\" | \"type\" | \"upload\"\ntype SignatureTarget = \"signature\" | \"initials\"\nconst SIGNATURE_COLORS = [\n  { name: \"Black\", value: \"#111827\" },\n  { name: \"Blue\", value: \"#1d4ed8\" },\n  { name: \"Red\", value: \"#dc2626\" },\n]\nfunction useSignatureFontsStylesheet(url: string | null, enabled: boolean) {\n  React.useEffect(() => {\n    if (!enabled || !url || typeof document === \"undefined\") return\n    const existing = document.querySelector<HTMLLinkElement>(\n      `link[data-pdf-editor-signature-fonts=\"${CSS.escape(url)}\"]`\n    )\n    if (existing) return\n    const link = document.createElement(\"link\")\n    link.rel = \"stylesheet\"\n    link.href = url\n    link.dataset.pdfEditorSignatureFonts = url\n    document.head.append(link)\n  }, [enabled, url])\n}\ntype SignatureUploadZoneProps = {\n  inputRef: ReturnType<typeof useSignatureUpload>[\"inputRef\"]\n  accept: string\n  isDragging: boolean\n  previewUrl: string | null\n  placeholder: string\n  onOpen: () => void\n  onFileInputChange: (event: Event) => void\n  onDrop: (event: DragEvent) => void\n  onDragOver: (event: DragEvent) => void\n  onDragLeave: () => void\n}\nfunction SignatureUploadZone({\n  inputRef,\n  accept,\n  isDragging,\n  previewUrl,\n  placeholder,\n  onOpen,\n  onFileInputChange,\n  onDrop,\n  onDragOver,\n  onDragLeave,\n}: SignatureUploadZoneProps) {\n  return (\n    <>\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept={accept}\n        className=\"sr-only\"\n        tabIndex={-1}\n        onChange={(event) => onFileInputChange(event.nativeEvent)}\n      />\n      <button\n        type=\"button\"\n        onClick={onOpen}\n        onDrop={(event) => {\n          event.preventDefault()\n          onDrop(event.nativeEvent)\n        }}\n        onDragOver={(event) => {\n          event.preventDefault()\n          onDragOver(event.nativeEvent)\n        }}\n        onDragLeave={() => onDragLeave()}\n        className={cn(\n          \"flex h-full w-full items-center justify-center rounded-md border-2 border-dashed transition-colors\",\n          isDragging\n            ? \"border-primary bg-primary/5\"\n            : \"border-input hover:border-muted-foreground/60\"\n        )}\n      >\n        {previewUrl ? (\n          <img\n            src={previewUrl}\n            alt=\"Uploaded signature\"\n            className=\"max-h-[90%] max-w-[90%] object-contain\"\n          />\n        ) : (\n          <span className=\"px-4 text-center text-xs text-muted-foreground\">\n            {placeholder}\n          </span>\n        )}\n      </button>\n    </>\n  )\n}\nfunction SignaturePad({\n  tab,\n  color,\n  fontFamily,\n  placeholder,\n  uploadPlaceholder,\n  drawRef,\n  typeRef,\n  upload,\n  onResult,\n}: {\n  tab: SignatureCreationTab\n  color: string\n  fontFamily: string\n  placeholder: string\n  uploadPlaceholder: string\n  drawRef: React.MutableRefObject<SignatureDrawPadHandle | null>\n  typeRef: React.MutableRefObject<SignatureTypePadHandle | null>\n  upload: ReturnType<typeof useSignatureUpload>\n  onResult: (result: SignatureFieldDefinition | null) => void\n}) {\n  return (\n    <div className=\"h-full w-full\">\n      {tab === \"draw\" ? (\n        <SignatureDrawPad\n          onResult={onResult}\n          padRef={(handle) => {\n            drawRef.current = handle\n          }}\n          strokeColor={color}\n          strokeWidth={3}\n          className=\"h-full w-full rounded-md border border-input bg-white\"\n        />\n      ) : null}\n      {tab === \"type\" ? (\n        <SignatureTypePad\n          onResult={onResult}\n          padRef={(handle) => {\n            typeRef.current = handle\n          }}\n          fontFamily={fontFamily}\n          fontSize={40}\n          color={color}\n          placeholder={placeholder}\n          className=\"h-full w-full rounded-md border border-input bg-white px-3 py-2 text-3xl text-slate-950 outline-none dark:bg-white dark:text-slate-950\"\n        />\n      ) : null}\n      {tab === \"upload\" ? (\n        <SignatureUploadZone\n          inputRef={upload.inputRef}\n          accept={upload.accept}\n          isDragging={upload.isDragging}\n          previewUrl={upload.previewUrl}\n          placeholder={uploadPlaceholder}\n          onOpen={upload.openFilePicker}\n          onFileInputChange={upload.handleFileInputChange}\n          onDrop={upload.handleDrop}\n          onDragOver={upload.handleDragOver}\n          onDragLeave={upload.handleDragLeave}\n        />\n      ) : null}\n    </div>\n  )\n}\nexport function PdfEditorSignatureDialog({\n  open,\n  onOpenChange,\n}: {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}) {\n  const { provides: signatureCapability } = useSignatureCapability()\n  const { notify, signatureFontsStylesheetUrl } = usePdfEditor()\n  const supportsInitials =\n    (signatureCapability?.mode ?? SignatureMode.SignatureOnly) ===\n    SignatureMode.SignatureAndInitials\n  const [tab, setTab] = React.useState<SignatureCreationTab>(\"draw\")\n  const [color, setColor] = React.useState(SIGNATURE_COLORS[0].value)\n  const [fontFamily, setFontFamily] = React.useState(\n    PDF_EDITOR_SIGNATURE_FONTS[0].family\n  )\n  const [signature, setSignature] =\n    React.useState<SignatureFieldDefinition | null>(null)\n  const [initials, setInitials] =\n    React.useState<SignatureFieldDefinition | null>(null)\n  const [activeTarget, setActiveTarget] =\n    React.useState<SignatureTarget>(\"signature\")\n  const signatureDrawRef = React.useRef<SignatureDrawPadHandle | null>(null)\n  const initialsDrawRef = React.useRef<SignatureDrawPadHandle | null>(null)\n  const signatureTypeRef = React.useRef<SignatureTypePadHandle | null>(null)\n  const initialsTypeRef = React.useRef<SignatureTypePadHandle | null>(null)\n  useSignatureFontsStylesheet(signatureFontsStylesheetUrl, open)\n  const signatureUpload = useSignatureUpload({ onResult: setSignature })\n  const initialsUpload = useSignatureUpload({ onResult: setInitials })\n  const clearSignatureUpload = signatureUpload.clear\n  const clearInitialsUpload = initialsUpload.clear\n  const clearAll = React.useCallback(() => {\n    signatureDrawRef.current?.clear()\n    initialsDrawRef.current?.clear()\n    signatureTypeRef.current?.clear()\n    initialsTypeRef.current?.clear()\n    clearSignatureUpload()\n    clearInitialsUpload()\n    setSignature(null)\n    setInitials(null)\n  }, [clearInitialsUpload, clearSignatureUpload])\n  const [previousOpen, setPreviousOpen] = React.useState(open)\n  if (previousOpen !== open) {\n    setPreviousOpen(open)\n    if (!open) {\n      setTab(\"draw\")\n      setColor(SIGNATURE_COLORS[0].value)\n      setFontFamily(PDF_EDITOR_SIGNATURE_FONTS[0].family)\n      setActiveTarget(\"signature\")\n      setSignature(null)\n      setInitials(null)\n    }\n  }\n  const handleTabChange = (nextTab: SignatureCreationTab) => {\n    clearAll()\n    setTab(nextTab)\n  }\n  const canSave = Boolean(signature || initials)\n  const handleSave = () => {\n    const entrySignature = signature ?? initials\n    if (!entrySignature || !signatureCapability) return\n    signatureCapability.addEntry({\n      signature: entrySignature,\n      ...(initials ? { initials } : {}),\n    })\n    notify(signature ? \"Signature saved.\" : \"Initials saved.\", \"success\")\n    onOpenChange(false)\n  }\n  const tabs: Array<{\n    id: SignatureCreationTab\n    label: string\n  }> = [\n    { id: \"draw\", label: \"Draw\" },\n    { id: \"type\", label: \"Type\" },\n    { id: \"upload\", label: \"Upload\" },\n  ]\n  const fontOptions = PDF_EDITOR_SIGNATURE_FONTS.map((font) => ({\n    label: font.name,\n    value: font.family,\n  }))\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle>Create signature</DialogTitle>\n          <DialogDescription>\n            Saved signatures can be placed anywhere in the document.\n          </DialogDescription>\n        </DialogHeader>\n        <InlineDialogPanel className=\"space-y-4\">\n          <div className=\"flex items-center gap-1 rounded-lg bg-muted p-0.5\">\n            {tabs.map((item) => (\n              <button\n                key={item.id}\n                type=\"button\"\n                onClick={() => handleTabChange(item.id)}\n                className={cn(\n                  \"flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n                  tab === item.id\n                    ? \"bg-background text-foreground shadow-sm\"\n                    : \"text-muted-foreground hover:text-foreground\"\n                )}\n              >\n                {item.label}\n              </button>\n            ))}\n          </div>\n\n          <div className=\"space-y-2\">\n            <div className=\"flex items-center justify-between gap-3\">\n              {supportsInitials ? (\n                <div className=\"flex items-center gap-1 rounded-lg bg-muted p-0.5\">\n                  {([\"signature\", \"initials\"] as const).map((target) => (\n                    <button\n                      key={target}\n                      type=\"button\"\n                      aria-pressed={activeTarget === target}\n                      onClick={() => setActiveTarget(target)}\n                      className={cn(\n                        \"rounded-md px-3 py-1 text-xs font-medium transition-colors\",\n                        activeTarget === target\n                          ? \"bg-background text-foreground shadow-sm\"\n                          : \"text-muted-foreground hover:text-foreground\"\n                      )}\n                    >\n                      {target === \"signature\" ? \"Signature\" : \"Initials\"}\n                    </button>\n                  ))}\n                </div>\n              ) : (\n                <PdfEditorFieldLabel>Signature</PdfEditorFieldLabel>\n              )}\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"xs\"\n                onClick={() => {\n                  if (activeTarget === \"signature\") {\n                    if (tab === \"draw\") signatureDrawRef.current?.clear()\n                    else if (tab === \"type\") signatureTypeRef.current?.clear()\n                    else clearSignatureUpload()\n                    setSignature(null)\n                  } else {\n                    if (tab === \"draw\") initialsDrawRef.current?.clear()\n                    else if (tab === \"type\") initialsTypeRef.current?.clear()\n                    else clearInitialsUpload()\n                    setInitials(null)\n                  }\n                }}\n              >\n                Clear\n              </Button>\n            </div>\n            <div\n              className={cn(\n                \"h-36 w-full\",\n                activeTarget !== \"signature\" && \"hidden\"\n              )}\n            >\n              <SignaturePad\n                tab={tab}\n                color={color}\n                fontFamily={fontFamily}\n                placeholder=\"Type your name\"\n                uploadPlaceholder=\"Click or drop an image of your signature\"\n                drawRef={signatureDrawRef}\n                typeRef={signatureTypeRef}\n                upload={signatureUpload}\n                onResult={setSignature}\n              />\n            </div>\n            {supportsInitials ? (\n              <div\n                className={cn(\n                  \"w-full\",\n                  activeTarget !== \"initials\" && \"hidden\"\n                )}\n              >\n                <div className=\"aspect-[20/7] h-36 max-w-full\">\n                  <SignaturePad\n                    tab={tab}\n                    color={color}\n                    fontFamily={fontFamily}\n                    placeholder=\"Initials\"\n                    uploadPlaceholder=\"Click or drop an image\"\n                    drawRef={initialsDrawRef}\n                    typeRef={initialsTypeRef}\n                    upload={initialsUpload}\n                    onResult={setInitials}\n                  />\n                </div>\n              </div>\n            ) : null}\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-4\">\n            {tab !== \"upload\" ? (\n              <div className=\"flex items-center gap-2\">\n                <span className=\"text-xs text-muted-foreground\">Color</span>\n                <div className=\"flex items-center gap-1.5\">\n                  {SIGNATURE_COLORS.map((option) => (\n                    <PdfEditorSwatch\n                      key={option.value}\n                      color={option.value}\n                      label={option.name}\n                      active={color === option.value}\n                      onClick={() => setColor(option.value)}\n                    />\n                  ))}\n                  <ColorPicker\n                    label=\"Custom signature color\"\n                    rainbowTrigger\n                    color={color}\n                    onChange={setColor}\n                  />\n                </div>\n              </div>\n            ) : null}\n            {tab === \"type\" ? (\n              <div className=\"flex items-center gap-2\">\n                <span className=\"text-xs text-muted-foreground\">Font</span>\n                <Select\n                  items={fontOptions}\n                  value={fontFamily}\n                  onValueChange={(value) => setFontFamily(String(value))}\n                >\n                  <SelectTrigger size=\"sm\" className=\"w-40\">\n                    <SelectValue placeholder=\"Font\" />\n                  </SelectTrigger>\n                  <SelectContent alignItemWithTrigger={false}>\n                    {fontOptions.map((option) => (\n                      <SelectItem key={option.value} value={option.value}>\n                        <span style={{ fontFamily: option.value }}>\n                          {option.label}\n                        </span>\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n            ) : null}\n          </div>\n        </InlineDialogPanel>\n        <DialogFooter>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={() => onOpenChange(false)}\n          >\n            Cancel\n          </Button>\n          <Button type=\"button\" disabled={!canSave} onClick={handleSave}>\n            Save\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Links                                                                      */\n/* -------------------------------------------------------------------------- */\ntype LinkTab = \"url\" | \"page\"\nfunction readLinkTarget(target: PdfLinkTarget | undefined): {\n  tab: LinkTab\n  url: string\n  pageNumber: number | null\n} {\n  if (!target) return { tab: \"url\", url: \"\", pageNumber: null }\n  if (target.type === \"action\") {\n    if (target.action.type === PdfActionType.URI) {\n      return { tab: \"url\", url: target.action.uri, pageNumber: null }\n    }\n    if (\n      target.action.type === PdfActionType.Goto ||\n      target.action.type === PdfActionType.RemoteGoto\n    ) {\n      return {\n        tab: \"page\",\n        url: \"\",\n        pageNumber: target.action.destination.pageIndex + 1,\n      }\n    }\n    return { tab: \"url\", url: \"\", pageNumber: null }\n  }\n  return {\n    tab: \"page\",\n    url: \"\",\n    pageNumber: target.destination.pageIndex + 1,\n  }\n}\nexport function PdfEditorLinkDialog({\n  documentId,\n  open,\n  onOpenChange,\n  source,\n}: {\n  documentId: string\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  source: \"annotation\" | \"selection\"\n}) {\n  const { provides: annotationCapability } = useAnnotationCapability()\n  const { provides: selectionCapability } = useSelectionCapability()\n  const { state: scrollState } = useScroll(documentId)\n  const { notify } = usePdfEditor()\n  const [tab, setTab] = React.useState<LinkTab>(\"url\")\n  const [url, setUrl] = React.useState(\"\")\n  const [pageNumber, setPageNumber] = React.useState(1)\n  const totalPages = Math.max(scrollState.totalPages, 1)\n  const annotationScope = React.useMemo(\n    () => annotationCapability?.forDocument(documentId) ?? null,\n    [annotationCapability, documentId]\n  )\n  const selectionScope = React.useMemo(\n    () => selectionCapability?.forDocument(documentId) ?? null,\n    [documentId, selectionCapability]\n  )\n  const [linkRequest, setLinkRequest] = React.useState({\n    annotationScope,\n    open: false,\n    source,\n  })\n  if (\n    linkRequest.annotationScope !== annotationScope ||\n    linkRequest.open !== open ||\n    linkRequest.source !== source\n  ) {\n    setLinkRequest({ annotationScope, open, source })\n    if (open) {\n      const selected =\n        source === \"annotation\"\n          ? annotationScope?.getSelectedAnnotation()\n          : null\n      const existing =\n        selected?.object.type === PdfAnnotationSubtype.LINK\n          ? readLinkTarget((selected.object as PdfLinkAnnoObject).target)\n          : { tab: \"url\" as const, url: \"\", pageNumber: 1 }\n      setTab(existing.tab)\n      setUrl(existing.url)\n      setPageNumber(existing.pageNumber ?? 1)\n    }\n  }\n  const buildTarget = (): PdfLinkTarget | null => {\n    if (tab === \"url\") {\n      const trimmed = url.trim()\n      if (!trimmed) return null\n      const normalized = /^[a-z][a-z0-9+.-]*:/i.test(trimmed)\n        ? trimmed\n        : `https://${trimmed}`\n      return {\n        type: \"action\",\n        action: { type: PdfActionType.URI, uri: normalized },\n      }\n    }\n    return {\n      type: \"destination\",\n      destination: {\n        pageIndex: Math.min(Math.max(pageNumber, 1), totalPages) - 1,\n        zoom: { mode: PdfZoomMode.FitPage },\n        view: [],\n      },\n    }\n  }\n  const handleSubmit = () => {\n    const target = buildTarget()\n    if (!target || !annotationScope) return\n    const selected =\n      source === \"annotation\" ? annotationScope.getSelectedAnnotation() : null\n    if (selected && selected.object.type === PdfAnnotationSubtype.LINK) {\n      annotationScope.updateAnnotation(\n        selected.object.pageIndex,\n        selected.object.id,\n        {\n          target,\n        } as Partial<PdfLinkAnnoObject>\n      )\n      notify(\"Link updated.\", \"success\")\n      onOpenChange(false)\n      return\n    }\n    if (selected) {\n      const rects =\n        \"segmentRects\" in selected.object &&\n        Array.isArray(selected.object.segmentRects) &&\n        selected.object.segmentRects.length > 0\n          ? selected.object.segmentRects\n          : [selected.object.rect]\n      for (const rect of rects) {\n        annotationScope.createAnnotation<PdfLinkAnnoObject>(\n          selected.object.pageIndex,\n          {\n            id: uuidV4(),\n            type: PdfAnnotationSubtype.LINK,\n            pageIndex: selected.object.pageIndex,\n            rect,\n            inReplyToId: selected.object.id,\n            replyType: PdfAnnotationReplyType.Group,\n            target,\n            strokeStyle: PdfAnnotationBorderStyle.UNDERLINE,\n            strokeColor: \"#2563eb\",\n            strokeWidth: 2,\n            created: new Date(),\n          }\n        )\n      }\n      notify(\"Link added.\", \"success\")\n      onOpenChange(false)\n      return\n    }\n    const formattedSelection = selectionScope?.getFormattedSelection() ?? []\n    if (formattedSelection.length === 0) {\n      notify(\"Select text or an annotation first.\", \"error\")\n      return\n    }\n    const applyLinks = (text: string[]) => {\n      for (const page of formattedSelection) {\n        const highlightId = uuidV4()\n        annotationScope.createAnnotation(page.pageIndex, {\n          id: highlightId,\n          created: new Date(),\n          flags: [\"print\"],\n          type: PdfAnnotationSubtype.HIGHLIGHT,\n          blendMode: PdfBlendMode.Multiply,\n          pageIndex: page.pageIndex,\n          rect: page.rect,\n          segmentRects: page.segmentRects,\n          strokeColor: \"#ffffff\",\n          opacity: 0,\n          custom: { text: text.join(\"\\n\") },\n        })\n        for (const rect of page.segmentRects) {\n          annotationScope.createAnnotation<PdfLinkAnnoObject>(page.pageIndex, {\n            id: uuidV4(),\n            type: PdfAnnotationSubtype.LINK,\n            pageIndex: page.pageIndex,\n            rect,\n            inReplyToId: highlightId,\n            replyType: PdfAnnotationReplyType.Group,\n            target,\n            strokeStyle: PdfAnnotationBorderStyle.UNDERLINE,\n            strokeColor: \"#2563eb\",\n            strokeWidth: 2,\n            created: new Date(),\n          })\n        }\n      }\n      selectionScope?.clear()\n      notify(\"Link added.\", \"success\")\n      onOpenChange(false)\n    }\n    const textTask = selectionScope?.getSelectedText()\n    if (textTask) {\n      textTask.wait(applyLinks, () => applyLinks([]))\n    } else {\n      applyLinks([])\n    }\n  }\n  const canSubmit = tab === \"url\" ? url.trim().length > 0 : pageNumber >= 1\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>Add link</DialogTitle>\n          <DialogDescription>\n            {source === \"selection\"\n              ? \"The selected text becomes a clickable link.\"\n              : \"The selected annotation becomes a clickable link.\"}\n          </DialogDescription>\n        </DialogHeader>\n        <InlineDialogPanel className=\"space-y-4\">\n          <div className=\"flex items-center gap-1 rounded-lg bg-muted p-0.5\">\n            {(\n              [\n                [\"url\", \"Web address\"],\n                [\"page\", \"Page in document\"],\n              ] as Array<[LinkTab, string]>\n            ).map(([id, label]) => (\n              <button\n                key={id}\n                type=\"button\"\n                onClick={() => setTab(id)}\n                className={cn(\n                  \"flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n                  tab === id\n                    ? \"bg-background text-foreground shadow-sm\"\n                    : \"text-muted-foreground hover:text-foreground\"\n                )}\n              >\n                {label}\n              </button>\n            ))}\n          </div>\n          {tab === \"url\" ? (\n            <div className=\"grid gap-1\">\n              <PdfEditorFieldLabel htmlFor=\"pdf-editor-link-url\">\n                URL\n              </PdfEditorFieldLabel>\n              <Input\n                id=\"pdf-editor-link-url\"\n                autoFocus\n                placeholder=\"https://example.com\"\n                value={url}\n                onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n                  setUrl(event.target.value)\n                }\n                onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {\n                  if (event.key === \"Enter\" && canSubmit) handleSubmit()\n                }}\n              />\n            </div>\n          ) : (\n            <div className=\"grid gap-1\">\n              <PdfEditorFieldLabel htmlFor=\"pdf-editor-link-page\">\n                Page number (1–{totalPages})\n              </PdfEditorFieldLabel>\n              <Input\n                id=\"pdf-editor-link-page\"\n                type=\"number\"\n                min={1}\n                max={totalPages}\n                value={pageNumber}\n                onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n                  setPageNumber(Number(event.target.value) || 1)\n                }\n              />\n            </div>\n          )}\n        </InlineDialogPanel>\n        <DialogFooter>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={() => onOpenChange(false)}\n          >\n            Cancel\n          </Button>\n          <Button type=\"button\" disabled={!canSubmit} onClick={handleSubmit}>\n            Save link\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Keyboard shortcuts                                                         */\n/* -------------------------------------------------------------------------- */\nexport const PDF_EDITOR_SHORTCUTS: Array<{\n  group: string\n  items: Array<{\n    keys: string[]\n    label: string\n  }>\n}> = [\n  {\n    group: \"Document\",\n    items: [\n      { keys: [\"mod+S\"], label: \"Download\" },\n      { keys: [\"mod+P\"], label: \"Print\" },\n      { keys: [\"mod+F\"], label: \"Search\" },\n      { keys: [\"mod+Z\"], label: \"Undo\" },\n      { keys: [\"mod+shift+Z\"], label: \"Redo\" },\n    ],\n  },\n  {\n    group: \"View\",\n    items: [\n      { keys: [\"mod+=\"], label: \"Zoom in\" },\n      { keys: [\"mod+-\"], label: \"Zoom out\" },\n      { keys: [\"mod+0\"], label: \"Fit page\" },\n      { keys: [\"H\"], label: \"Hand tool\" },\n      { keys: [\"V\"], label: \"Select tool\" },\n    ],\n  },\n  {\n    group: \"Editing\",\n    items: [\n      { keys: [\"Esc\"], label: \"Cancel tool / clear selection\" },\n      { keys: [\"Delete\"], label: \"Delete selected annotations\" },\n      { keys: [\"mod+A\"], label: \"Select all annotations on the page\" },\n    ],\n  },\n]\nexport function PdfEditorShortcutsDialog({\n  open,\n  onOpenChange,\n}: {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}) {\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>Keyboard shortcuts</DialogTitle>\n        </DialogHeader>\n        <InlineDialogPanel className=\"space-y-4\">\n          {PDF_EDITOR_SHORTCUTS.map((group) => (\n            <PdfEditorSection key={group.group} title={group.group}>\n              <div className=\"divide-y rounded-lg border\">\n                {group.items.map((item) => (\n                  <div\n                    key={item.label}\n                    className=\"flex items-center justify-between gap-3 px-3 py-1.5 text-sm\"\n                  >\n                    <span>{item.label}</span>\n                    <span className=\"flex items-center gap-1\">\n                      {item.keys.map((key) => (\n                        <Kbd key={key}>{formatShortcut(key)}</Kbd>\n                      ))}\n                    </span>\n                  </div>\n                ))}\n              </div>\n            </PdfEditorSection>\n          ))}\n        </InlineDialogPanel>\n        <DialogFooter>\n          <Button type=\"button\" onClick={() => onOpenChange(false)}>\n            Done\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Password prompt (inline)                                                   */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorPasswordPrompt({\n  documentState,\n}: {\n  documentState: DocumentState\n}) {\n  const { provides: documentManager } = useDocumentManagerCapability()\n  const [password, setPassword] = React.useState(\"\")\n  const [isRetrying, setIsRetrying] = React.useState(false)\n  const isPasswordError = documentState.errorCode === PdfErrorCode.Password\n  const isIncorrect = isPasswordError && documentState.passwordProvided\n  const handleRetry = () => {\n    if (!documentManager || !password.trim()) return\n    setIsRetrying(true)\n    documentManager.retryDocument(documentState.id, { password }).wait(\n      () => {\n        setPassword(\"\")\n        setIsRetrying(false)\n      },\n      () => setIsRetrying(false)\n    )\n  }\n  if (!isPasswordError) {\n    return (\n      <div className=\"grid h-full place-items-center p-6\">\n        <div className=\"max-w-sm space-y-2 text-center\">\n          <div className=\"text-sm font-medium text-foreground\">\n            The document could not be opened\n          </div>\n          <div className=\"text-sm text-muted-foreground\">\n            {documentState.error ?? \"An unknown error occurred.\"}\n          </div>\n        </div>\n      </div>\n    )\n  }\n  return (\n    <div className=\"grid h-full place-items-center p-6\">\n      <div className=\"w-full max-w-sm space-y-4 rounded-xl border bg-background p-5 shadow-sm\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"grid size-9 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground\">\n            <LockGlyph className=\"size-4\" />\n          </div>\n          <div className=\"space-y-1\">\n            <div className=\"text-sm font-medium text-foreground\">\n              Password required\n            </div>\n            <div className=\"text-xs text-muted-foreground\">\n              {isIncorrect\n                ? \"The password was incorrect. Try again.\"\n                : \"This document is protected. Enter the password to open it.\"}\n            </div>\n          </div>\n        </div>\n        <PasswordInput\n          value={password}\n          autoFocus\n          placeholder=\"Document password\"\n          onChange={setPassword}\n          onEnter={handleRetry}\n        />\n        <div className=\"flex justify-end\">\n          <InlineLoadingButton\n            type=\"button\"\n            loading={isRetrying}\n            disabled={!password.trim()}\n            onClick={handleRetry}\n          >\n            Open document\n          </InlineLoadingButton>\n        </div>\n      </div>\n    </div>\n  )\n}\nexport { ignore as ignorePdfTask }\nfunction InlineDialogPanel({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <ScrollArea className=\"min-h-0\">\n      <div\n        data-slot=\"dialog-panel\"\n        className={cn(\"min-h-0\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </ScrollArea>\n  )\n}\nfunction InlineLoadingButton({\n  loading,\n  disabled,\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  loading?: boolean\n}) {\n  return (\n    <Button\n      {...props}\n      className={cn(\"relative\", className)}\n      disabled={disabled || loading}\n      aria-busy={loading || undefined}\n    >\n      <span className={cn(\"contents\", loading && \"invisible\")}>{children}</span>\n      {loading ? (\n        <InlineSpinner className=\"pointer-events-none absolute\" />\n      ) : null}\n    </Button>\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/pdf-editor-dialogs.tsx"
    },
    {
      "path": "components/extend/pdf-editor-properties.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  blendModeLabel,\n  blendModeValues,\n  makeStandardFont,\n  PDF_FORM_FIELD_FLAG,\n  PDF_FORM_FIELD_TYPE,\n  PdfAnnotationBorderStyle,\n  PdfAnnotationLineEnding,\n  PdfAnnotationSubtype,\n  PdfStandardFont,\n  PdfStandardFontFamily,\n  PdfTextAlignment,\n  PdfVerticalAlignment,\n  STANDARD_FONT_FAMILIES,\n  standardFontFamily,\n  standardFontIsBold,\n  standardFontIsItalic,\n  type LineEndings,\n  type PdfAnnotationFlagName,\n  type PdfAnnotationObject,\n  type PdfTextWidgetAnnoField,\n  type PdfWidgetAnnoField,\n  type PdfWidgetAnnoObject,\n  type PdfWidgetAnnoOption,\n} from \"@embedpdf/models\"\nimport {\n  getSelectedAnnotations,\n  useAnnotation,\n  useAnnotationCapability,\n  type AnnotationTool,\n  type TrackedAnnotation,\n} from \"@embedpdf/plugin-annotation/react\"\nimport { useFormCapability } from \"@embedpdf/plugin-form/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\nimport { ColorPicker } from \"@/components/extend/document-color-picker\"\nimport {\n  AlignBottomGlyph,\n  AlignCenterGlyph,\n  AlignLeftGlyph,\n  AlignMiddleGlyph,\n  AlignRightGlyph,\n  AlignTopGlyph,\n  BoldGlyph,\n  formatDateTime,\n  getAnnotationMeta,\n  ItalicGlyph,\n  PdfEditorEmptyState,\n  PdfEditorFieldLabel,\n  PdfEditorSection,\n  PdfEditorSwatch,\n  SlidersGlyph,\n  TrashGlyph,\n  usePdfEditor,\n} from \"@/components/extend/pdf-editor-shared\"\n\n/* -------------------------------------------------------------------------- */\n/* Property schema                                                            */\n/* -------------------------------------------------------------------------- */\ntype PropertyKind =\n  | \"color\"\n  | \"colorWithTransparent\"\n  | \"opacity\"\n  | \"strokeWidth\"\n  | \"strokeStyle\"\n  | \"strokeStyleWithCloudy\"\n  | \"linkStrokeStyle\"\n  | \"lineEnding\"\n  | \"lineEndings\"\n  | \"fontFamily\"\n  | \"fontSize\"\n  | \"fontColor\"\n  | \"textAlign\"\n  | \"verticalAlign\"\n  | \"blendMode\"\n  | \"overlayText\"\n  | \"rotation\"\ntype PropertyConfig = {\n  key: string\n  kind: PropertyKind\n  label: string\n  /** Only shown while editing an existing annotation, never for tool defaults. */\n  editOnly?: boolean\n}\nconst PROPERTY_CONFIGS: Record<string, PropertyConfig> = {\n  color: { key: \"color\", kind: \"colorWithTransparent\", label: \"Fill\" },\n  strokeColor: {\n    key: \"strokeColor\",\n    kind: \"colorWithTransparent\",\n    label: \"Stroke\",\n  },\n  markupColor: { key: \"strokeColor\", kind: \"color\", label: \"Color\" },\n  opacity: { key: \"opacity\", kind: \"opacity\", label: \"Opacity\" },\n  strokeWidth: {\n    key: \"strokeWidth\",\n    kind: \"strokeWidth\",\n    label: \"Stroke width\",\n  },\n  strokeStyle: {\n    key: \"strokeStyle\",\n    kind: \"strokeStyle\",\n    label: \"Border style\",\n  },\n  strokeStyleWithCloudy: {\n    key: \"strokeStyle\",\n    kind: \"strokeStyleWithCloudy\",\n    label: \"Border style\",\n  },\n  linkStrokeStyle: {\n    key: \"strokeStyle\",\n    kind: \"linkStrokeStyle\",\n    label: \"Border style\",\n  },\n  lineEnding: {\n    key: \"lineEnding\",\n    kind: \"lineEnding\",\n    label: \"Line ending\",\n  },\n  lineEndings: {\n    key: \"lineEndings\",\n    kind: \"lineEndings\",\n    label: \"Line endings\",\n  },\n  fontFamily: { key: \"fontFamily\", kind: \"fontFamily\", label: \"Font\" },\n  fontSize: { key: \"fontSize\", kind: \"fontSize\", label: \"Font size\" },\n  fontColor: { key: \"fontColor\", kind: \"fontColor\", label: \"Text color\" },\n  textAlign: { key: \"textAlign\", kind: \"textAlign\", label: \"Alignment\" },\n  verticalAlign: {\n    key: \"verticalAlign\",\n    kind: \"verticalAlign\",\n    label: \"Vertical alignment\",\n  },\n  blendMode: { key: \"blendMode\", kind: \"blendMode\", label: \"Blend mode\" },\n  overlayText: {\n    key: \"overlayText\",\n    kind: \"overlayText\",\n    label: \"Overlay text\",\n  },\n  rotation: {\n    key: \"rotation\",\n    kind: \"rotation\",\n    label: \"Rotation\",\n    editOnly: true,\n  },\n}\n/** Ordered property lists keyed by EmbedPDF tool id. */\nconst TOOL_PROPERTIES: Record<string, string[]> = {\n  highlight: [\"markupColor\", \"opacity\", \"blendMode\"],\n  underline: [\"markupColor\", \"opacity\", \"blendMode\"],\n  strikeout: [\"markupColor\", \"opacity\", \"blendMode\"],\n  squiggly: [\"markupColor\", \"opacity\", \"blendMode\"],\n  ink: [\"markupColor\", \"opacity\", \"strokeWidth\", \"rotation\"],\n  inkHighlighter: [\n    \"markupColor\",\n    \"opacity\",\n    \"strokeWidth\",\n    \"blendMode\",\n    \"rotation\",\n  ],\n  signatureInk: [\"markupColor\", \"opacity\", \"strokeWidth\", \"rotation\"],\n  circle: [\n    \"color\",\n    \"opacity\",\n    \"strokeColor\",\n    \"strokeStyleWithCloudy\",\n    \"strokeWidth\",\n    \"rotation\",\n  ],\n  square: [\n    \"color\",\n    \"opacity\",\n    \"strokeColor\",\n    \"strokeStyleWithCloudy\",\n    \"strokeWidth\",\n    \"rotation\",\n  ],\n  polygon: [\n    \"strokeColor\",\n    \"opacity\",\n    \"strokeStyleWithCloudy\",\n    \"strokeWidth\",\n    \"color\",\n    \"rotation\",\n  ],\n  line: [\n    \"strokeColor\",\n    \"opacity\",\n    \"strokeStyle\",\n    \"strokeWidth\",\n    \"lineEndings\",\n    \"color\",\n    \"rotation\",\n  ],\n  lineArrow: [\n    \"strokeColor\",\n    \"opacity\",\n    \"strokeStyle\",\n    \"strokeWidth\",\n    \"lineEndings\",\n    \"color\",\n    \"rotation\",\n  ],\n  polyline: [\n    \"strokeColor\",\n    \"opacity\",\n    \"strokeStyle\",\n    \"strokeWidth\",\n    \"lineEndings\",\n    \"color\",\n    \"rotation\",\n  ],\n  textComment: [\"markupColor\", \"opacity\"],\n  insertText: [\"markupColor\", \"opacity\"],\n  replaceText: [\"markupColor\", \"opacity\"],\n  freeText: [\n    \"fontFamily\",\n    \"fontSize\",\n    \"fontColor\",\n    \"textAlign\",\n    \"verticalAlign\",\n    \"opacity\",\n    \"color\",\n    \"rotation\",\n  ],\n  freeTextCallout: [\n    \"fontFamily\",\n    \"fontSize\",\n    \"fontColor\",\n    \"textAlign\",\n    \"verticalAlign\",\n    \"opacity\",\n    \"color\",\n    \"markupColor\",\n    \"strokeWidth\",\n    \"lineEnding\",\n  ],\n  stamp: [\"rotation\"],\n  rubberStamp: [\"rotation\"],\n  signatureStamp: [\"rotation\"],\n  redact: [\"strokeColor\", \"color\", \"opacity\", \"overlayText\"],\n  link: [\"strokeColor\", \"strokeWidth\", \"linkStrokeStyle\"],\n  formTextField: [\n    \"fontSize\",\n    \"fontColor\",\n    \"strokeColor\",\n    \"strokeWidth\",\n    \"color\",\n  ],\n  formCheckbox: [\"strokeColor\", \"color\", \"strokeWidth\"],\n  formCombobox: [\n    \"fontSize\",\n    \"fontColor\",\n    \"strokeColor\",\n    \"strokeWidth\",\n    \"color\",\n  ],\n  formListbox: [\n    \"fontFamily\",\n    \"fontSize\",\n    \"fontColor\",\n    \"strokeColor\",\n    \"strokeWidth\",\n    \"color\",\n  ],\n  formRadioButton: [\"strokeColor\", \"color\", \"strokeWidth\"],\n}\nfunction getSharedProperties(toolIds: string[]) {\n  const unique = [...new Set(toolIds)]\n  if (unique.length === 0) return []\n  const first = new Set(TOOL_PROPERTIES[unique[0]] ?? [])\n  for (const id of unique.slice(1)) {\n    const own = new Set(TOOL_PROPERTIES[id] ?? [])\n    for (const property of first) {\n      if (!own.has(property)) first.delete(property)\n    }\n  }\n  return (TOOL_PROPERTIES[unique[0]] ?? []).filter((property) =>\n    first.has(property)\n  )\n}\nconst FONT_SIZE_PRESETS = [\n  8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72,\n]\nconst LINE_ENDING_OPTIONS: Array<{\n  value: PdfAnnotationLineEnding\n  label: string\n}> = [\n  { value: PdfAnnotationLineEnding.None, label: \"None\" },\n  { value: PdfAnnotationLineEnding.OpenArrow, label: \"Open arrow\" },\n  { value: PdfAnnotationLineEnding.ClosedArrow, label: \"Closed arrow\" },\n  { value: PdfAnnotationLineEnding.ROpenArrow, label: \"Reverse open arrow\" },\n  {\n    value: PdfAnnotationLineEnding.RClosedArrow,\n    label: \"Reverse closed arrow\",\n  },\n  { value: PdfAnnotationLineEnding.Square, label: \"Square\" },\n  { value: PdfAnnotationLineEnding.Circle, label: \"Circle\" },\n  { value: PdfAnnotationLineEnding.Diamond, label: \"Diamond\" },\n  { value: PdfAnnotationLineEnding.Butt, label: \"Butt\" },\n  { value: PdfAnnotationLineEnding.Slash, label: \"Slash\" },\n]\nconst FLAG_OPTIONS: Array<{\n  flag: PdfAnnotationFlagName\n  label: string\n  description: string\n}> = [\n  {\n    flag: \"locked\",\n    label: \"Lock position\",\n    description: \"Prevents moving, resizing, and deleting.\",\n  },\n  {\n    flag: \"lockedContents\",\n    label: \"Lock contents\",\n    description: \"Prevents editing the text.\",\n  },\n  {\n    flag: \"print\",\n    label: \"Print\",\n    description: \"Include when printing.\",\n  },\n]\n/* -------------------------------------------------------------------------- */\n/* Controls                                                                   */\n/* -------------------------------------------------------------------------- */\nfunction useDebouncedCommit<T>(\n  value: T,\n  onCommit: (value: T) => void,\n  delay = 150\n) {\n  const [draft, setDraft] = React.useState(value)\n  const timeoutRef = React.useRef<number | null>(null)\n  const onCommitRef = React.useRef(onCommit)\n  React.useEffect(() => {\n    onCommitRef.current = onCommit\n  })\n  const [previousValue, setPreviousValue] = React.useState(value)\n  if (!Object.is(previousValue, value)) {\n    setPreviousValue(value)\n    setDraft(value)\n  }\n  React.useEffect(\n    () => () => {\n      if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current)\n    },\n    []\n  )\n  const update = React.useCallback(\n    (next: T) => {\n      setDraft(next)\n      if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current)\n      timeoutRef.current = window.setTimeout(() => {\n        timeoutRef.current = null\n        onCommitRef.current(next)\n      }, delay)\n    },\n    [delay]\n  )\n  return [draft, update] as const\n}\nfunction PropertySwitch({\n  checked,\n  disabled,\n  label,\n  description,\n  onCheckedChange,\n}: {\n  checked: boolean\n  disabled?: boolean\n  label: React.ReactNode\n  description?: React.ReactNode\n  onCheckedChange: (checked: boolean) => void\n}) {\n  const id = React.useId()\n  return (\n    <label\n      htmlFor={id}\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center justify-between gap-4 rounded-md px-1 py-1\",\n        disabled ? \"cursor-not-allowed opacity-64\" : \"cursor-pointer\"\n      )}\n    >\n      <span className=\"min-w-0\">\n        <span className=\"block text-sm leading-4\">{label}</span>\n        {description ? (\n          <span className=\"block text-xs text-muted-foreground\">\n            {description}\n          </span>\n        ) : null}\n      </span>\n      <Switch\n        id={id}\n        checked={checked}\n        disabled={disabled}\n        onCheckedChange={onCheckedChange}\n      />\n    </label>\n  )\n}\nfunction ColorControl({\n  label,\n  value,\n  presets,\n  allowTransparent,\n  onChange,\n}: {\n  label: string\n  value: string | undefined\n  presets: string[]\n  allowTransparent: boolean\n  onChange: (value: string) => void\n}) {\n  const current = (value ?? \"\").toLowerCase()\n  const isTransparent = !value || current === \"transparent\"\n  const pickerSource = isTransparent ? \"#111827\" : (value ?? \"#111827\")\n  return (\n    <div className=\"space-y-1.5\">\n      <div className=\"flex items-center justify-between\">\n        <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>\n        <span className=\"text-[11px] text-muted-foreground tabular-nums\">\n          {isTransparent ? \"None\" : current}\n        </span>\n      </div>\n      <div className=\"flex flex-wrap items-center gap-1.5\">\n        {presets.map((preset) => (\n          <PdfEditorSwatch\n            key={preset}\n            color={preset}\n            active={!isTransparent && current === preset.toLowerCase()}\n            onClick={() => onChange(preset)}\n          />\n        ))}\n        {allowTransparent ? (\n          <PdfEditorSwatch\n            color=\"transparent\"\n            label=\"No fill\"\n            active={isTransparent}\n            onClick={() => onChange(\"transparent\")}\n          />\n        ) : null}\n        <ColorPicker\n          label={`Custom ${label.toLowerCase()}`}\n          rainbowTrigger\n          color={pickerSource}\n          onChange={onChange}\n        />\n      </div>\n    </div>\n  )\n}\nfunction SliderControl({\n  label,\n  value,\n  min,\n  max,\n  step,\n  format,\n  onChange,\n}: {\n  label: string\n  value: number\n  min: number\n  max: number\n  step: number\n  format: (value: number) => string\n  onChange: (value: number) => void\n}) {\n  const [draft, update] = useDebouncedCommit(value, onChange)\n  return (\n    <div className=\"space-y-1.5\">\n      <div className=\"flex items-center justify-between\">\n        <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>\n        <span className=\"text-[11px] text-muted-foreground tabular-nums\">\n          {format(draft)}\n        </span>\n      </div>\n      <input\n        type=\"range\"\n        min={min}\n        max={max}\n        step={step}\n        value={draft}\n        aria-label={label}\n        onChange={(event) => update(Number(event.target.value))}\n        className=\"h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary\"\n      />\n    </div>\n  )\n}\ntype StrokeStyleValue =\n  | \"solid\"\n  | \"dotted\"\n  | \"short-dashed\"\n  | \"dashed\"\n  | \"long-dashed\"\n  | \"dash-dot\"\n  | \"long-dash-dot\"\n  | \"dash-dot-dot\"\n  | \"cloudy-1\"\n  | \"cloudy-2\"\n  | \"underline\"\n  | \"custom\"\nconst STROKE_STYLE_PRESETS: Array<{\n  value: Exclude<\n    StrokeStyleValue,\n    \"cloudy-1\" | \"cloudy-2\" | \"underline\" | \"custom\"\n  >\n  label: string\n  dashArray?: number[]\n}> = [\n  { value: \"solid\", label: \"Solid\" },\n  { value: \"dotted\", label: \"Dotted\", dashArray: [1, 2] },\n  { value: \"short-dashed\", label: \"Short dashed\", dashArray: [2, 2] },\n  { value: \"dashed\", label: \"Dashed\", dashArray: [4, 3] },\n  { value: \"long-dashed\", label: \"Long dashed\", dashArray: [8, 4] },\n  { value: \"dash-dot\", label: \"Dash dot\", dashArray: [6, 3, 1, 3] },\n  {\n    value: \"long-dash-dot\",\n    label: \"Long dash dot\",\n    dashArray: [10, 4, 1, 4],\n  },\n  {\n    value: \"dash-dot-dot\",\n    label: \"Dash dot dot\",\n    dashArray: [6, 3, 1, 3, 1, 3],\n  },\n]\nfunction getStrokeDashArray(value: StrokeStyleValue) {\n  return STROKE_STYLE_PRESETS.find((preset) => preset.value === value)\n    ?.dashArray\n}\nfunction StrokeStylePreview({\n  value,\n  dashArray,\n}: {\n  value: StrokeStyleValue\n  dashArray?: number[]\n}) {\n  const path =\n    value === \"cloudy-2\"\n      ? \"M1 8 Q3 2 5 8 Q7 2 9 8 Q11 2 13 8 Q15 2 17 8 Q19 2 21 8 Q23 2 25 8 Q27 2 29 8 Q31 2 33 8 Q35 2 37 8 Q39 2 41 8 Q43 2 45 8 Q47 2 49 8 Q51 2 55 8\"\n      : \"M1 8 Q5 1 9 8 Q13 1 17 8 Q21 1 25 8 Q29 1 33 8 Q37 1 41 8 Q45 1 49 8 Q53 1 55 8\"\n  const resolvedDashArray = dashArray ?? getStrokeDashArray(value)\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 56 12\"\n      className=\"h-6! w-24! shrink-0 text-foreground\"\n    >\n      <path\n        d={value.startsWith(\"cloudy\") ? path : \"M1 6H55\"}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeDasharray={resolvedDashArray?.join(\" \")}\n        strokeLinecap=\"round\"\n        strokeWidth=\"2.25\"\n      />\n    </svg>\n  )\n}\nfunction SelectControl({\n  label,\n  value,\n  options,\n  onChange,\n}: {\n  label: string\n  value: string\n  options: Array<{\n    value: string\n    label: string\n    preview?: React.ReactNode\n  }>\n  onChange: (value: string) => void\n}) {\n  const selectedOption =\n    options.find((option) => option.value === value) ?? null\n  return (\n    <div className=\"space-y-1.5\">\n      <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>\n      <Select\n        items={options}\n        value={value}\n        onValueChange={(next) => {\n          if (next) onChange(String(next))\n        }}\n      >\n        <SelectTrigger size=\"sm\" className=\"w-full\">\n          <SelectValue placeholder={label}>\n            {selectedOption ? (\n              <span className=\"grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3\">\n                <span className=\"truncate\">{selectedOption.label}</span>\n                {selectedOption.preview}\n              </span>\n            ) : (\n              label\n            )}\n          </SelectValue>\n        </SelectTrigger>\n        <SelectContent alignItemWithTrigger={false}>\n          {options.map((option) => (\n            <SelectItem\n              key={option.value}\n              value={option.value}\n              className={cn(option.preview && \"py-0.5 pe-2\")}\n            >\n              <span className=\"grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3\">\n                <span className=\"truncate\">{option.label}</span>\n                {option.preview}\n              </span>\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n    </div>\n  )\n}\nfunction getStrokeStyleValue(\n  strokeStyle: PdfAnnotationBorderStyle | undefined,\n  cloudyBorderIntensity: number | undefined,\n  strokeDashArray: number[] | undefined\n): StrokeStyleValue {\n  if (\n    strokeStyle === PdfAnnotationBorderStyle.CLOUDY ||\n    (cloudyBorderIntensity ?? 0) > 0\n  ) {\n    return (cloudyBorderIntensity ?? 1) >= 2 ? \"cloudy-2\" : \"cloudy-1\"\n  }\n  if (strokeStyle === PdfAnnotationBorderStyle.UNDERLINE) return \"underline\"\n  if (strokeStyle === PdfAnnotationBorderStyle.DASHED) {\n    const preset = STROKE_STYLE_PRESETS.find((item) => {\n      if (!item.dashArray || !strokeDashArray) return false\n      if (item.dashArray.length !== strokeDashArray.length) return false\n      return item.dashArray.every(\n        (part, index) => part === strokeDashArray[index]\n      )\n    })\n    return preset?.value ?? \"custom\"\n  }\n  return \"solid\"\n}\nfunction getStrokeStylePatch(\n  value: StrokeStyleValue,\n  customDashArray?: number[]\n) {\n  const dashArray = getStrokeDashArray(value)\n  if (dashArray || value === \"custom\") {\n    return {\n      strokeStyle: PdfAnnotationBorderStyle.DASHED,\n      strokeDashArray: dashArray ?? customDashArray ?? [4, 3],\n      cloudyBorderIntensity: 0,\n    }\n  }\n  switch (value) {\n    case \"underline\":\n      return {\n        strokeStyle: PdfAnnotationBorderStyle.UNDERLINE,\n        strokeDashArray: undefined,\n        cloudyBorderIntensity: 0,\n      }\n    case \"cloudy-1\":\n      return {\n        strokeStyle: PdfAnnotationBorderStyle.CLOUDY,\n        strokeDashArray: undefined,\n        cloudyBorderIntensity: 1,\n      }\n    case \"cloudy-2\":\n      return {\n        strokeStyle: PdfAnnotationBorderStyle.CLOUDY,\n        strokeDashArray: undefined,\n        cloudyBorderIntensity: 2,\n      }\n    default:\n      return {\n        strokeStyle: PdfAnnotationBorderStyle.SOLID,\n        strokeDashArray: undefined,\n        cloudyBorderIntensity: 0,\n      }\n  }\n}\nfunction FontControl({\n  value,\n  onChange,\n}: {\n  value: PdfStandardFont | undefined\n  onChange: (value: PdfStandardFont) => void\n}) {\n  const font = value ?? PdfStandardFont.Helvetica\n  const family = standardFontFamily(font)\n  const bold = standardFontIsBold(font)\n  const italic = standardFontIsItalic(font)\n  const supportsStyles =\n    family !== PdfStandardFontFamily.Symbol &&\n    family !== PdfStandardFontFamily.ZapfDingbats\n  const familyOptions = STANDARD_FONT_FAMILIES.filter(\n    (item) => item !== PdfStandardFontFamily.Unknown\n  ).map((item) => ({ label: item, value: item }))\n  const apply = (\n    nextFamily: PdfStandardFontFamily,\n    nextBold: boolean,\n    nextItalic: boolean\n  ) => {\n    const next =\n      makeStandardFont(nextFamily, { bold: nextBold, italic: nextItalic }) ??\n      makeStandardFont(nextFamily, { bold: false, italic: false })\n    if (next !== undefined && next !== PdfStandardFont.Unknown) onChange(next)\n  }\n  return (\n    <div className=\"space-y-1.5\">\n      <PdfEditorFieldLabel>Font</PdfEditorFieldLabel>\n      <div className=\"flex items-center gap-1.5\">\n        <Select\n          items={familyOptions}\n          value={family}\n          onValueChange={(next) =>\n            apply(String(next) as PdfStandardFontFamily, bold, italic)\n          }\n        >\n          <SelectTrigger size=\"sm\" className=\"min-w-0 flex-1\">\n            <SelectValue placeholder=\"Font\" />\n          </SelectTrigger>\n          <SelectContent alignItemWithTrigger={false}>\n            {familyOptions.map((option) => (\n              <SelectItem key={option.value} value={option.value}>\n                {option.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n        <ToggleGroup\n          multiple\n          value={[bold ? \"bold\" : \"\", italic ? \"italic\" : \"\"].filter(Boolean)}\n          onValueChange={(next: string[]) =>\n            apply(family, next.includes(\"bold\"), next.includes(\"italic\"))\n          }\n          className=\"shrink-0\"\n          spacing={0}\n        >\n          <ToggleGroupItem\n            value=\"bold\"\n            size=\"sm\"\n            variant=\"outline\"\n            aria-label=\"Bold\"\n            disabled={!supportsStyles}\n          >\n            <BoldGlyph className=\"size-3.5\" />\n          </ToggleGroupItem>\n          <ToggleGroupItem\n            value=\"italic\"\n            size=\"sm\"\n            variant=\"outline\"\n            aria-label=\"Italic\"\n            disabled={!supportsStyles}\n          >\n            <ItalicGlyph className=\"size-3.5\" />\n          </ToggleGroupItem>\n        </ToggleGroup>\n      </div>\n    </div>\n  )\n}\nfunction FontSizeControl({\n  value,\n  onChange,\n}: {\n  value: number | undefined\n  onChange: (value: number) => void\n}) {\n  const [draft, update] = useDebouncedCommit(value ?? 12, onChange, 250)\n  const sizeOptions = (\n    FONT_SIZE_PRESETS.includes(draft)\n      ? FONT_SIZE_PRESETS\n      : [...FONT_SIZE_PRESETS, draft].sort((a, b) => a - b)\n  ).map((size) => ({ label: `${size} pt`, value: String(size) }))\n  return (\n    <div className=\"space-y-1.5\">\n      <PdfEditorFieldLabel>Font size</PdfEditorFieldLabel>\n      <div className=\"flex items-center gap-1.5\">\n        <Input\n          type=\"number\"\n          min={4}\n          max={200}\n          value={draft}\n          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {\n            const next = Number(event.target.value)\n            if (Number.isFinite(next) && next > 0) update(next)\n          }}\n          className={cn(\"h-8 px-2.5\", \"w-20\")}\n        />\n        <Select\n          items={sizeOptions}\n          value={String(draft)}\n          onValueChange={(next) => onChange(Number(next))}\n        >\n          <SelectTrigger size=\"sm\" className=\"min-w-0 flex-1\">\n            <SelectValue placeholder=\"Size\" />\n          </SelectTrigger>\n          <SelectContent alignItemWithTrigger={false}>\n            {sizeOptions.map((option) => (\n              <SelectItem key={option.value} value={option.value}>\n                {option.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      </div>\n    </div>\n  )\n}\nfunction AlignControl({\n  label,\n  value,\n  onChange,\n  options,\n}: {\n  label: string\n  value: string\n  onChange: (value: string) => void\n  options: Array<{\n    value: string\n    label: string\n    glyph: React.ReactNode\n  }>\n}) {\n  return (\n    <div className=\"space-y-1.5\">\n      <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>\n      <ToggleGroup\n        value={[value]}\n        onValueChange={(next: string[]) => {\n          const selected = next[0]\n          if (selected !== undefined) onChange(selected)\n        }}\n        spacing={0}\n      >\n        {options.map((option) => (\n          <ToggleGroupItem\n            key={option.value}\n            value={option.value}\n            size=\"sm\"\n            variant=\"outline\"\n            aria-label={option.label}\n          >\n            {option.glyph}\n          </ToggleGroupItem>\n        ))}\n      </ToggleGroup>\n    </div>\n  )\n}\nfunction LineEndingPreview({ value }: { value: PdfAnnotationLineEnding }) {\n  const isOpenArrow =\n    value === PdfAnnotationLineEnding.OpenArrow ||\n    value === PdfAnnotationLineEnding.ROpenArrow\n  const isClosedArrow =\n    value === PdfAnnotationLineEnding.ClosedArrow ||\n    value === PdfAnnotationLineEnding.RClosedArrow\n  const isReverse =\n    value === PdfAnnotationLineEnding.ROpenArrow ||\n    value === PdfAnnotationLineEnding.RClosedArrow\n  const lineEndX = isReverse\n    ? isClosedArrow\n      ? 58\n      : 56\n    : isClosedArrow\n      ? 68\n      : 66\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 72 16\"\n      className=\"h-6! w-24! shrink-0 text-foreground\"\n    >\n      <path\n        d={`M2 8H${lineEndX}`}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeWidth=\"2.5\"\n      />\n      {isOpenArrow ? (\n        <path\n          d={isReverse ? \"M56 8 66 2M56 8l10 6\" : \"M66 8 56 2M66 8l-10 6\"}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"2.5\"\n        />\n      ) : null}\n      {isClosedArrow ? (\n        <path\n          d={isReverse ? \"m58 8 10-6v12Z\" : \"m68 8-10-6v12Z\"}\n          fill=\"currentColor\"\n        />\n      ) : null}\n      {value === PdfAnnotationLineEnding.Square ? (\n        <rect x=\"60\" y=\"2\" width=\"12\" height=\"12\" fill=\"currentColor\" />\n      ) : null}\n      {value === PdfAnnotationLineEnding.Circle ? (\n        <circle cx=\"66\" cy=\"8\" r=\"6\" fill=\"currentColor\" />\n      ) : null}\n      {value === PdfAnnotationLineEnding.Diamond ? (\n        <path d=\"m66 2 6 6-6 6-6-6Z\" fill=\"currentColor\" />\n      ) : null}\n      {value === PdfAnnotationLineEnding.Butt ? (\n        <path d=\"M66 2v12\" stroke=\"currentColor\" strokeWidth=\"2.5\" />\n      ) : null}\n      {value === PdfAnnotationLineEnding.Slash ? (\n        <path d=\"m60 14 12-12\" stroke=\"currentColor\" strokeWidth=\"2.5\" />\n      ) : null}\n    </svg>\n  )\n}\nfunction LineEndingsControl({\n  value,\n  onChange,\n}: {\n  value: LineEndings | undefined\n  onChange: (value: LineEndings) => void\n}) {\n  const start = value?.start ?? PdfAnnotationLineEnding.None\n  const end = value?.end ?? PdfAnnotationLineEnding.None\n  const options = LINE_ENDING_OPTIONS.map((option) => ({\n    value: String(option.value),\n    label: option.label,\n    preview: <LineEndingPreview value={option.value} />,\n  }))\n  return (\n    <div className=\"space-y-2\">\n      <SelectControl\n        label=\"Line start\"\n        value={String(start)}\n        options={options}\n        onChange={(next) =>\n          onChange({ start: Number(next) as PdfAnnotationLineEnding, end })\n        }\n      />\n      <SelectControl\n        label=\"Line end\"\n        value={String(end)}\n        options={options}\n        onChange={(next) =>\n          onChange({ start, end: Number(next) as PdfAnnotationLineEnding })\n        }\n      />\n    </div>\n  )\n}\nfunction LineEndingControl({\n  label,\n  value,\n  onChange,\n}: {\n  label: string\n  value: PdfAnnotationLineEnding | undefined\n  onChange: (value: PdfAnnotationLineEnding) => void\n}) {\n  const options = LINE_ENDING_OPTIONS.map((option) => ({\n    value: String(option.value),\n    label: option.label,\n    preview: <LineEndingPreview value={option.value} />,\n  }))\n  return (\n    <SelectControl\n      label={label}\n      value={String(value ?? PdfAnnotationLineEnding.None)}\n      options={options}\n      onChange={(next) => onChange(Number(next) as PdfAnnotationLineEnding)}\n    />\n  )\n}\nfunction TextControl({\n  label,\n  value,\n  placeholder,\n  onChange,\n}: {\n  label: string\n  value: string\n  placeholder?: string\n  onChange: (value: string) => void\n}) {\n  const [draft, update] = useDebouncedCommit(value, onChange, 400)\n  return (\n    <div className=\"space-y-1.5\">\n      <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>\n      <Input\n        value={draft}\n        placeholder={placeholder}\n        onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n          update(event.target.value)\n        }\n        className={cn(\"h-8 px-2.5\")}\n      />\n    </div>\n  )\n}\nfunction RotationControl({\n  value,\n  onChange,\n}: {\n  value: number\n  onChange: (value: number) => void\n}) {\n  const [draft, update] = useDebouncedCommit(value, onChange, 250)\n  return (\n    <div className=\"space-y-1.5\">\n      <PdfEditorFieldLabel>Rotation</PdfEditorFieldLabel>\n      <div className=\"flex items-center gap-1.5\">\n        <Input\n          type=\"number\"\n          min={0}\n          max={359}\n          value={Math.round(draft)}\n          onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n            update(((Number(event.target.value) % 360) + 360) % 360)\n          }\n          className={cn(\"h-8 px-2.5\", \"w-20\")}\n        />\n        <span className=\"text-xs text-muted-foreground\">degrees</span>\n        {[0, 90, 180, 270].map((preset) => (\n          <Button\n            key={preset}\n            type=\"button\"\n            size=\"xs\"\n            variant={draft === preset ? \"secondary\" : \"ghost\"}\n            onClick={() => {\n              update(preset)\n            }}\n          >\n            {preset}°\n          </Button>\n        ))}\n      </div>\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Property list                                                              */\n/* -------------------------------------------------------------------------- */\nfunction PropertyControl({\n  config,\n  values,\n  colorPresets,\n  onPatch,\n  onRotate,\n}: {\n  config: PropertyConfig\n  values: Record<string, unknown>\n  colorPresets: string[]\n  onPatch: (patch: Record<string, unknown>) => void\n  onRotate?: (rotation: number) => void\n}) {\n  const value = values[config.key]\n  switch (config.kind) {\n    case \"color\":\n    case \"colorWithTransparent\":\n    case \"fontColor\":\n      return (\n        <ColorControl\n          label={config.label}\n          value={typeof value === \"string\" ? value : undefined}\n          presets={colorPresets}\n          allowTransparent={config.kind === \"colorWithTransparent\"}\n          onChange={(next) => onPatch({ [config.key]: next })}\n        />\n      )\n    case \"opacity\":\n      return (\n        <SliderControl\n          label={config.label}\n          value={typeof value === \"number\" ? value : 1}\n          min={0}\n          max={1}\n          step={0.05}\n          format={(item) => `${Math.round(item * 100)}%`}\n          onChange={(next) => onPatch({ [config.key]: next })}\n        />\n      )\n    case \"strokeWidth\":\n      return (\n        <SliderControl\n          label={config.label}\n          value={typeof value === \"number\" ? value : 1}\n          min={1}\n          max={30}\n          step={1}\n          format={(item) => `${item} pt`}\n          onChange={(next) => onPatch({ [config.key]: next })}\n        />\n      )\n    case \"strokeStyle\":\n    case \"strokeStyleWithCloudy\":\n    case \"linkStrokeStyle\": {\n      const strokeDashArray = values.strokeDashArray as number[] | undefined\n      const selectedValue = getStrokeStyleValue(\n        value as PdfAnnotationBorderStyle | undefined,\n        values.cloudyBorderIntensity as number | undefined,\n        strokeDashArray\n      )\n      const options: Array<{\n        value: string\n        label: string\n        preview: React.ReactNode\n      }> = STROKE_STYLE_PRESETS.map((preset) => ({\n        value: preset.value,\n        label: preset.label,\n        preview: (\n          <StrokeStylePreview\n            value={preset.value}\n            dashArray={preset.dashArray}\n          />\n        ),\n      }))\n      if (config.kind === \"linkStrokeStyle\") {\n        options.splice(1, 0, {\n          value: \"underline\",\n          label: \"Underline\",\n          preview: <StrokeStylePreview value=\"underline\" />,\n        })\n      }\n      if (config.kind === \"strokeStyleWithCloudy\") {\n        options.push(\n          {\n            value: \"cloudy-1\",\n            label: \"Cloudy\",\n            preview: <StrokeStylePreview value=\"cloudy-1\" />,\n          },\n          {\n            value: \"cloudy-2\",\n            label: \"Cloudy (dense)\",\n            preview: <StrokeStylePreview value=\"cloudy-2\" />,\n          }\n        )\n      }\n      if (selectedValue === \"custom\") {\n        options.push({\n          value: \"custom\",\n          label: \"Custom dashed\",\n          preview: (\n            <StrokeStylePreview value=\"custom\" dashArray={strokeDashArray} />\n          ),\n        })\n      }\n      return (\n        <SelectControl\n          label={config.label}\n          value={selectedValue}\n          options={options}\n          onChange={(next) =>\n            onPatch(\n              getStrokeStylePatch(next as StrokeStyleValue, strokeDashArray)\n            )\n          }\n        />\n      )\n    }\n    case \"lineEnding\":\n      return (\n        <LineEndingControl\n          label={config.label}\n          value={value as PdfAnnotationLineEnding | undefined}\n          onChange={(next) => onPatch({ [config.key]: next })}\n        />\n      )\n    case \"lineEndings\":\n      return (\n        <LineEndingsControl\n          value={value as LineEndings | undefined}\n          onChange={(next) => onPatch({ lineEndings: next })}\n        />\n      )\n    case \"fontFamily\":\n      return (\n        <FontControl\n          value={value as PdfStandardFont | undefined}\n          onChange={(next) => onPatch({ fontFamily: next })}\n        />\n      )\n    case \"fontSize\":\n      return (\n        <FontSizeControl\n          value={typeof value === \"number\" ? value : undefined}\n          onChange={(next) => onPatch({ fontSize: next })}\n        />\n      )\n    case \"textAlign\":\n      return (\n        <AlignControl\n          label={config.label}\n          value={String(value ?? PdfTextAlignment.Left)}\n          onChange={(next) => onPatch({ textAlign: Number(next) })}\n          options={[\n            {\n              value: String(PdfTextAlignment.Left),\n              label: \"Align left\",\n              glyph: <AlignLeftGlyph className=\"size-3.5\" />,\n            },\n            {\n              value: String(PdfTextAlignment.Center),\n              label: \"Align center\",\n              glyph: <AlignCenterGlyph className=\"size-3.5\" />,\n            },\n            {\n              value: String(PdfTextAlignment.Right),\n              label: \"Align right\",\n              glyph: <AlignRightGlyph className=\"size-3.5\" />,\n            },\n          ]}\n        />\n      )\n    case \"verticalAlign\":\n      return (\n        <AlignControl\n          label={config.label}\n          value={String(value ?? PdfVerticalAlignment.Top)}\n          onChange={(next) => onPatch({ verticalAlign: Number(next) })}\n          options={[\n            {\n              value: String(PdfVerticalAlignment.Top),\n              label: \"Align top\",\n              glyph: <AlignTopGlyph className=\"size-3.5\" />,\n            },\n            {\n              value: String(PdfVerticalAlignment.Middle),\n              label: \"Align middle\",\n              glyph: <AlignMiddleGlyph className=\"size-3.5\" />,\n            },\n            {\n              value: String(PdfVerticalAlignment.Bottom),\n              label: \"Align bottom\",\n              glyph: <AlignBottomGlyph className=\"size-3.5\" />,\n            },\n          ]}\n        />\n      )\n    case \"blendMode\":\n      return (\n        <SelectControl\n          label={config.label}\n          value={String(value ?? 0)}\n          options={blendModeValues.map((mode) => ({\n            value: String(mode),\n            label: blendModeLabel(mode),\n          }))}\n          onChange={(next) => onPatch({ blendMode: Number(next) })}\n        />\n      )\n    case \"overlayText\":\n      return (\n        <TextControl\n          label={config.label}\n          value={typeof value === \"string\" ? value : \"\"}\n          placeholder=\"e.g. REDACTED\"\n          onChange={(next) => onPatch({ overlayText: next })}\n        />\n      )\n    case \"rotation\":\n      return (\n        <RotationControl\n          value={typeof value === \"number\" ? value : 0}\n          onChange={(next) =>\n            onRotate ? onRotate(next) : onPatch({ rotation: next })\n          }\n        />\n      )\n    default:\n      return null\n  }\n}\n/* -------------------------------------------------------------------------- */\n/* Widget (form field) properties                                             */\n/* -------------------------------------------------------------------------- */\nconst FIELD_TYPE_LABELS: Partial<Record<PDF_FORM_FIELD_TYPE, string>> = {\n  [PDF_FORM_FIELD_TYPE.TEXTFIELD]: \"Text field\",\n  [PDF_FORM_FIELD_TYPE.CHECKBOX]: \"Checkbox\",\n  [PDF_FORM_FIELD_TYPE.RADIOBUTTON]: \"Radio button\",\n  [PDF_FORM_FIELD_TYPE.COMBOBOX]: \"Dropdown\",\n  [PDF_FORM_FIELD_TYPE.LISTBOX]: \"List box\",\n  [PDF_FORM_FIELD_TYPE.PUSHBUTTON]: \"Button\",\n  [PDF_FORM_FIELD_TYPE.SIGNATURE]: \"Signature field\",\n}\nexport function getFormFieldTypeLabel(type: PDF_FORM_FIELD_TYPE) {\n  return FIELD_TYPE_LABELS[type] ?? \"Form field\"\n}\nfunction WidgetProperties({\n  documentId,\n  widget,\n}: {\n  documentId: string\n  widget: PdfWidgetAnnoObject\n}) {\n  const { provides: annotation } = useAnnotation(documentId)\n  const { provides: form } = useFormCapability()\n  const { openDialog, notify } = usePdfEditor()\n  const [nameDraft, setNameDraft] = React.useState(widget.field.name)\n  const [nameError, setNameError] = React.useState<string | null>(null)\n  const field = widget.field\n  const fieldLabel = getFormFieldTypeLabel(field.type)\n  const [previousField, setPreviousField] = React.useState({\n    id: widget.id,\n    name: widget.field.name,\n  })\n  if (\n    previousField.id !== widget.id ||\n    previousField.name !== widget.field.name\n  ) {\n    setPreviousField({ id: widget.id, name: widget.field.name })\n    setNameDraft(widget.field.name)\n    setNameError(null)\n  }\n  const updateField = React.useCallback(\n    (patch: Partial<PdfWidgetAnnoField>) => {\n      annotation?.updateAnnotations([\n        {\n          pageIndex: widget.pageIndex,\n          id: widget.id,\n          patch: { field: { ...widget.field, ...patch } as PdfWidgetAnnoField },\n        },\n      ])\n    },\n    [annotation, widget.field, widget.id, widget.pageIndex]\n  )\n  const toggleFlag = (flag: PDF_FORM_FIELD_FLAG, enabled: boolean) => {\n    const current = field.flag ?? PDF_FORM_FIELD_FLAG.NONE\n    updateField({ flag: enabled ? current | flag : current & ~flag })\n  }\n  const commitName = () => {\n    const nextName = nameDraft.trim()\n    if (!form || !nextName || nextName === field.name) {\n      setNameDraft(field.name)\n      return\n    }\n    form.renameField(widget.id, nextName, documentId).wait(\n      (result) => {\n        setNameError(null)\n        if (result.outcome === \"conflict\") {\n          openDialog({\n            type: \"confirm\",\n            title: \"Share this field?\",\n            description: `A ${fieldLabel.toLowerCase()} named \"${result.fieldName}\" already exists. Sharing makes both widgets show the same value.`,\n            confirmLabel: \"Share field\",\n            onConfirm: () => {\n              form\n                .shareField(widget.id, result.targetAnnotationId, documentId)\n                .wait(\n                  () => notify(\"Field shared.\", \"success\"),\n                  (failure) => {\n                    setNameError(failure.reason.message)\n                    setNameDraft(field.name)\n                  }\n                )\n            },\n          })\n        }\n      },\n      (failure) => {\n        setNameError(failure.reason.message)\n        setNameDraft(field.name)\n      }\n    )\n  }\n  const options: PdfWidgetAnnoOption[] =\n    field.type === PDF_FORM_FIELD_TYPE.COMBOBOX ||\n    field.type === PDF_FORM_FIELD_TYPE.LISTBOX\n      ? ((\n          field as {\n            options?: PdfWidgetAnnoOption[]\n          }\n        ).options ?? [])\n      : []\n  const setOptions = (next: PdfWidgetAnnoOption[]) =>\n    updateField({ options: next } as Partial<PdfWidgetAnnoField>)\n  const isTextField = field.type === PDF_FORM_FIELD_TYPE.TEXTFIELD\n  const textField = field as PdfTextWidgetAnnoField\n  const maxLength = isTextField ? (textField.maxLen ?? 0) : 0\n  const isComb = Boolean(field.flag & PDF_FORM_FIELD_FLAG.TEXT_COMB)\n  return (\n    <PdfEditorSection title={`${fieldLabel} settings`}>\n      <div className=\"space-y-3\">\n        <div className=\"space-y-1.5\">\n          <PdfEditorFieldLabel htmlFor={`pdf-editor-field-name-${widget.id}`}>\n            Field name\n          </PdfEditorFieldLabel>\n          <Input\n            id={`pdf-editor-field-name-${widget.id}`}\n            value={nameDraft}\n            onChange={(event: React.ChangeEvent<HTMLInputElement>) =>\n              setNameDraft(event.target.value)\n            }\n            onBlur={commitName}\n            onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {\n              if (event.key === \"Enter\") event.currentTarget.blur()\n            }}\n            className={cn(\"h-8 px-2.5\")}\n          />\n          {nameError ? (\n            <div className=\"text-xs text-destructive\">{nameError}</div>\n          ) : null}\n        </div>\n\n        {isTextField ? (\n          <>\n            <TextControl\n              label=\"Default value\"\n              value={field.value ?? \"\"}\n              onChange={(next) => updateField({ value: next })}\n            />\n            <div className=\"space-y-1.5\">\n              <PdfEditorFieldLabel>Max length</PdfEditorFieldLabel>\n              <Input\n                type=\"number\"\n                min={0}\n                placeholder=\"Unlimited\"\n                value={maxLength || \"\"}\n                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {\n                  const parsed = Number.parseInt(event.target.value, 10)\n                  const next =\n                    Number.isNaN(parsed) || parsed <= 0 ? undefined : parsed\n                  updateField({ maxLen: next } as Partial<PdfWidgetAnnoField>)\n                  if (!next && isComb)\n                    toggleFlag(PDF_FORM_FIELD_FLAG.TEXT_COMB, false)\n                }}\n                className={cn(\"h-8 px-2.5\")}\n              />\n            </div>\n          </>\n        ) : null}\n\n        {options.length > 0 ||\n        field.type === PDF_FORM_FIELD_TYPE.COMBOBOX ||\n        field.type === PDF_FORM_FIELD_TYPE.LISTBOX ? (\n          <div className=\"space-y-1.5\">\n            <PdfEditorFieldLabel>Options</PdfEditorFieldLabel>\n            <div className=\"space-y-1.5\">\n              {options.map((option, index) => (\n                <div key={index} className=\"flex items-center gap-1.5\">\n                  <Input\n                    defaultValue={option.label}\n                    onBlur={(event: React.FocusEvent<HTMLInputElement>) => {\n                      const nextLabel = event.target.value\n                      if (nextLabel === option.label) return\n                      setOptions(\n                        options.map((item, itemIndex) =>\n                          itemIndex === index\n                            ? { ...item, label: nextLabel }\n                            : item\n                        )\n                      )\n                    }}\n                    onKeyDown={(\n                      event: React.KeyboardEvent<HTMLInputElement>\n                    ) => {\n                      if (event.key === \"Enter\") event.currentTarget.blur()\n                    }}\n                    className={cn(\"h-8 px-2.5\")}\n                  />\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    aria-label=\"Remove option\"\n                    onClick={() =>\n                      setOptions(\n                        options.filter((_, itemIndex) => itemIndex !== index)\n                      )\n                    }\n                  >\n                    <TrashGlyph className=\"size-3.5\" />\n                  </Button>\n                </div>\n              ))}\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"xs\"\n                onClick={() =>\n                  setOptions([\n                    ...options,\n                    {\n                      label: `Option ${options.length + 1}`,\n                      isSelected: false,\n                    },\n                  ])\n                }\n              >\n                Add option\n              </Button>\n            </div>\n          </div>\n        ) : null}\n\n        <div className=\"space-y-0.5\">\n          <PropertySwitch\n            checked={Boolean(field.flag & PDF_FORM_FIELD_FLAG.READONLY)}\n            onCheckedChange={(checked) =>\n              toggleFlag(PDF_FORM_FIELD_FLAG.READONLY, checked)\n            }\n            label=\"Read only\"\n          />\n          <PropertySwitch\n            checked={Boolean(field.flag & PDF_FORM_FIELD_FLAG.REQUIRED)}\n            onCheckedChange={(checked) =>\n              toggleFlag(PDF_FORM_FIELD_FLAG.REQUIRED, checked)\n            }\n            label=\"Required\"\n          />\n          {isTextField ? (\n            <>\n              <PropertySwitch\n                checked={Boolean(\n                  field.flag & PDF_FORM_FIELD_FLAG.TEXT_MULTIPLINE\n                )}\n                onCheckedChange={(checked) =>\n                  toggleFlag(PDF_FORM_FIELD_FLAG.TEXT_MULTIPLINE, checked)\n                }\n                label=\"Multiline\"\n              />\n              <PropertySwitch\n                checked={isComb}\n                disabled={maxLength <= 0}\n                onCheckedChange={(checked) =>\n                  toggleFlag(PDF_FORM_FIELD_FLAG.TEXT_COMB, checked)\n                }\n                label=\"Comb of characters\"\n                description=\"Requires a max length.\"\n              />\n            </>\n          ) : null}\n          {field.type === PDF_FORM_FIELD_TYPE.LISTBOX ? (\n            <PropertySwitch\n              checked={Boolean(\n                field.flag & PDF_FORM_FIELD_FLAG.CHOICE_MULTL_SELECT\n              )}\n              onCheckedChange={(checked) =>\n                toggleFlag(PDF_FORM_FIELD_FLAG.CHOICE_MULTL_SELECT, checked)\n              }\n              label=\"Allow multiple selections\"\n            />\n          ) : null}\n        </div>\n      </div>\n    </PdfEditorSection>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Panel                                                                      */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorPropertiesPanel({\n  documentId,\n}: {\n  documentId: string\n}) {\n  const { provides: annotationCapability } = useAnnotationCapability()\n  const { provides: annotation, state } = useAnnotation(documentId)\n  const { permissions } = usePdfEditor()\n  const [toolSnapshot, setToolSnapshot] = React.useState<{\n    source: typeof annotationCapability\n    tools: AnnotationTool[]\n  } | null>(null)\n  React.useEffect(() => {\n    if (!annotationCapability) return\n    return annotationCapability.onToolsChange(({ tools }) =>\n      setToolSnapshot({ source: annotationCapability, tools })\n    )\n  }, [annotationCapability])\n  const selected = React.useMemo(() => getSelectedAnnotations(state), [state])\n  const tools =\n    toolSnapshot?.source === annotationCapability\n      ? toolSnapshot.tools\n      : (annotationCapability?.getTools() ?? [])\n  const activeTool: AnnotationTool | null =\n    state.activeToolId && annotationCapability\n      ? (tools.find((tool) => tool.id === state.activeToolId) ?? null)\n      : null\n  const colorPresets = annotationCapability?.getColorPresets() ?? []\n  const selectedToolIds = React.useMemo(\n    () =>\n      selected\n        .map(\n          (item) => annotationCapability?.findToolForAnnotation(item.object)?.id\n        )\n        .filter((id): id is string => Boolean(id)),\n    [annotationCapability, selected]\n  )\n  const isEditing = selected.length > 0\n  const propertyKeys = isEditing\n    ? getSharedProperties(selectedToolIds)\n    : activeTool\n      ? (TOOL_PROPERTIES[activeTool.id] ?? [])\n      : []\n  const configs = propertyKeys\n    .map((key) => PROPERTY_CONFIGS[key])\n    .filter((config): config is PropertyConfig => Boolean(config))\n    .filter((config) => isEditing || !config.editOnly)\n  const values = React.useMemo<Record<string, unknown>>(() => {\n    if (isEditing)\n      return selected[0].object as unknown as Record<string, unknown>\n    if (activeTool)\n      return activeTool.defaults as unknown as Record<string, unknown>\n    return {}\n  }, [activeTool, isEditing, selected])\n  const canEdit = permissions.canModifyAnnotations\n  const structurallyLocked = selected.some((item) =>\n    annotation ? annotation.isAnnotationStructurallyLocked(item.object) : false\n  )\n  const applyPatch = React.useCallback(\n    (patch: Record<string, unknown>) => {\n      if (isEditing) {\n        annotation?.updateAnnotations(\n          selected.map((item) => ({\n            pageIndex: item.object.pageIndex,\n            id: item.object.id,\n            patch: {\n              ...patch,\n              modified: new Date(),\n            } as Partial<PdfAnnotationObject>,\n          }))\n        )\n        return\n      }\n      if (activeTool && annotationCapability) {\n        annotationCapability.setToolDefaults(\n          activeTool.id,\n          patch as Partial<PdfAnnotationObject> & Record<string, unknown>\n        )\n      }\n    },\n    [activeTool, annotation, annotationCapability, isEditing, selected]\n  )\n  const applyRotation = React.useCallback(\n    (rotation: number) => {\n      if (!annotation || !annotationCapability) return\n      for (const item of selected) {\n        const current = item.object.rotation ?? 0\n        let patch: Partial<PdfAnnotationObject>\n        try {\n          patch = annotationCapability.transformAnnotation(item.object, {\n            type: \"rotate\",\n            changes: { rotation },\n            metadata: {\n              rotationAngle: rotation,\n              rotationDelta: rotation - current,\n            },\n          })\n        } catch {\n          patch = { rotation }\n        }\n        annotation.updateAnnotation(item.object.pageIndex, item.object.id, {\n          ...patch,\n          modified: new Date(),\n        })\n      }\n    },\n    [annotation, annotationCapability, selected]\n  )\n  const single: TrackedAnnotation | null =\n    selected.length === 1 ? selected[0] : null\n  const singleMeta = single ? getAnnotationMeta(single.object) : null\n  const isWidget = single?.object.type === PdfAnnotationSubtype.WIDGET\n  const title = isEditing\n    ? selected.length > 1\n      ? `${selected.length} annotations`\n      : (singleMeta?.label ?? \"Annotation\")\n    : activeTool\n      ? `${activeTool.name} defaults`\n      : \"Properties\"\n  if (!isEditing && !activeTool) {\n    return (\n      <PdfEditorEmptyState\n        glyph={SlidersGlyph}\n        title=\"Nothing selected\"\n        description=\"Select an annotation on the page, or pick a tool to edit its default style.\"\n      />\n    )\n  }\n  const flags = single?.object.flags ?? []\n  const contents =\n    single && !isWidget && typeof single.object.contents === \"string\"\n      ? single.object.contents\n      : \"\"\n  const verticalAlignmentConfig = configs.find(\n    (config) => config.kind === \"verticalAlign\"\n  )\n  return (\n    <div className=\"space-y-5 p-3\">\n      <div>\n        <div className=\"text-sm font-medium text-foreground\">{title}</div>\n        <div className=\"text-xs text-muted-foreground\">\n          {isEditing\n            ? structurallyLocked\n              ? \"This annotation is locked. Unlock it to make changes.\"\n              : canEdit\n                ? \"Changes apply immediately.\"\n                : \"This document does not allow annotation changes.\"\n            : \"New annotations use these settings.\"}\n        </div>\n      </div>\n\n      {configs.length > 0 ? (\n        <PdfEditorSection title=\"Appearance\">\n          <fieldset\n            disabled={isEditing && (!canEdit || structurallyLocked)}\n            className=\"space-y-4 disabled:opacity-60\"\n          >\n            {configs.map((config) => {\n              if (\n                config.kind === \"verticalAlign\" &&\n                configs.some((item) => item.kind === \"textAlign\")\n              ) {\n                return null\n              }\n              if (config.kind === \"textAlign\" && verticalAlignmentConfig) {\n                return (\n                  <div\n                    key=\"alignment-controls\"\n                    className=\"grid grid-cols-2 items-start gap-3\"\n                  >\n                    <PropertyControl\n                      config={config}\n                      values={values}\n                      colorPresets={colorPresets}\n                      onPatch={applyPatch}\n                      onRotate={isEditing ? applyRotation : undefined}\n                    />\n                    <PropertyControl\n                      config={verticalAlignmentConfig}\n                      values={values}\n                      colorPresets={colorPresets}\n                      onPatch={applyPatch}\n                      onRotate={isEditing ? applyRotation : undefined}\n                    />\n                  </div>\n                )\n              }\n              return (\n                <PropertyControl\n                  key={config.key + config.kind}\n                  config={config}\n                  values={values}\n                  colorPresets={colorPresets}\n                  onPatch={applyPatch}\n                  onRotate={isEditing ? applyRotation : undefined}\n                />\n              )\n            })}\n          </fieldset>\n        </PdfEditorSection>\n      ) : null}\n\n      {single && isWidget ? (\n        <WidgetProperties\n          documentId={documentId}\n          widget={single.object as PdfWidgetAnnoObject}\n        />\n      ) : null}\n\n      {single && !isWidget ? (\n        <PdfEditorSection title=\"Note\">\n          <NoteEditor\n            key={single.object.id}\n            value={contents}\n            disabled={!canEdit}\n            onChange={(next) =>\n              annotation?.updateAnnotation(\n                single.object.pageIndex,\n                single.object.id,\n                {\n                  contents: next,\n                  modified: new Date(),\n                }\n              )\n            }\n          />\n        </PdfEditorSection>\n      ) : null}\n\n      {isEditing ? (\n        <PdfEditorSection title=\"Behavior\">\n          <div className=\"space-y-0.5\">\n            {FLAG_OPTIONS.map((option) => {\n              const checked = selected.every((item) =>\n                (item.object.flags ?? []).includes(option.flag)\n              )\n              return (\n                <PropertySwitch\n                  key={option.flag}\n                  checked={checked}\n                  disabled={!canEdit}\n                  onCheckedChange={(next) => {\n                    annotation?.updateAnnotations(\n                      selected.map((item) => {\n                        const current = item.object.flags ?? []\n                        const nextFlags = next\n                          ? current.includes(option.flag)\n                            ? current\n                            : [...current, option.flag]\n                          : current.filter((flag) => flag !== option.flag)\n                        return {\n                          pageIndex: item.object.pageIndex,\n                          id: item.object.id,\n                          patch: { flags: nextFlags },\n                        }\n                      })\n                    )\n                  }}\n                  label={option.label}\n                  description={option.description}\n                />\n              )\n            })}\n          </div>\n        </PdfEditorSection>\n      ) : null}\n\n      {single ? (\n        <PdfEditorSection title=\"Details\">\n          <dl className=\"grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs\">\n            <dt className=\"text-muted-foreground\">Type</dt>\n            <dd>{singleMeta?.label}</dd>\n            <dt className=\"text-muted-foreground\">Page</dt>\n            <dd>{single.object.pageIndex + 1}</dd>\n            <dt className=\"text-muted-foreground\">Author</dt>\n            <dd className=\"truncate\">{single.object.author || \"—\"}</dd>\n            <dt className=\"text-muted-foreground\">Created</dt>\n            <dd>{formatDateTime(single.object.created)}</dd>\n            <dt className=\"text-muted-foreground\">Modified</dt>\n            <dd>{formatDateTime(single.object.modified)}</dd>\n            {flags.length > 0 ? (\n              <>\n                <dt className=\"text-muted-foreground\">Flags</dt>\n                <dd className=\"truncate\">{flags.join(\", \")}</dd>\n              </>\n            ) : null}\n          </dl>\n        </PdfEditorSection>\n      ) : null}\n    </div>\n  )\n}\nfunction NoteEditor({\n  value,\n  disabled,\n  onChange,\n}: {\n  value: string\n  disabled?: boolean\n  onChange: (value: string) => void\n}) {\n  const [draft, setDraft] = React.useState(value)\n  const [previousValue, setPreviousValue] = React.useState(value)\n  if (!Object.is(previousValue, value)) {\n    setPreviousValue(value)\n    setDraft(value)\n  }\n  return (\n    <textarea\n      value={draft}\n      disabled={disabled}\n      rows={3}\n      placeholder=\"Add a note…\"\n      onChange={(event) => setDraft(event.target.value)}\n      onBlur={() => {\n        if (draft !== value) onChange(draft)\n      }}\n      className={cn(\n        \"w-full resize-y rounded-lg border border-input bg-background px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60\"\n      )}\n    />\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/extend/pdf-editor-properties.tsx"
    },
    {
      "path": "components/extend/pdf-editor-panels.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useDocumentState, useRegistry } from \"@embedpdf/core/react\"\nimport {\n  PDF_FORM_FIELD_TYPE,\n  PdfActionType,\n  PdfAnnotationName,\n  PdfAnnotationSubtype,\n  PdfZoomMode,\n  uuidV4,\n  type PdfAttachmentObject,\n  type PdfBookmarkObject,\n  type PdfDestinationObject,\n  type PdfWidgetAnnoObject,\n} from \"@embedpdf/models\"\nimport {\n  getGroupLeaderId,\n  getSelectedAnnotations,\n  getSidebarAnnotationsWithRepliesGroupedByPage,\n  useAnnotation,\n  type SidebarAnnotationEntry,\n  type TrackedAnnotation,\n} from \"@embedpdf/plugin-annotation/react\"\nimport { useAttachmentCapability } from \"@embedpdf/plugin-attachment/react\"\nimport { useBookmarkCapability } from \"@embedpdf/plugin-bookmark/react\"\nimport {\n  useFormCapability,\n  type FormFieldInfo,\n} from \"@embedpdf/plugin-form/react\"\nimport {\n  useRedaction,\n  type RedactionItem,\n} from \"@embedpdf/plugin-redaction/react\"\nimport { useScrollCapability } from \"@embedpdf/plugin-scroll/react\"\nimport {\n  SignatureFieldKind,\n  useActivePlacement,\n  useSignatureEntries,\n} from \"@embedpdf/plugin-signature/react\"\nimport {\n  StampImg,\n  useActiveStamp,\n  useStampCapability,\n  useStampLibraries,\n  useStampsByLibrary,\n  type StampDefinition,\n} from \"@embedpdf/plugin-stamp/react\"\nimport {\n  ThumbImg,\n  useThumbnailPlugin,\n  type ThumbMeta,\n  type WindowState,\n} from \"@embedpdf/plugin-thumbnail/react\"\nimport type { degrees, ParseSpeeds, PDFDocument } from \"pdf-lib\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { getFormFieldTypeLabel } from \"@/components/extend/pdf-editor-properties\"\nimport {\n  ArrowDownGlyph,\n  ArrowUpGlyph,\n  CheckGlyph,\n  ChevronDownGlyph,\n  ChevronRightGlyph,\n  CommentsGlyph,\n  downloadArrayBuffer,\n  downloadBlob,\n  DownloadGlyph,\n  EditGlyph,\n  EyeOffGlyph,\n  FilePlusGlyph,\n  formatBytes,\n  formatRelativeDate,\n  getAnnotationMeta,\n  GridGlyph,\n  ListTreeGlyph,\n  MergeGlyph,\n  PaperclipGlyph,\n  PdfEditorEmptyState,\n  PdfEditorScrollArea,\n  PdfEditorToolbarSeparator,\n  PdfEditorToolButton,\n  PlusGlyph,\n  RedactAreaGlyph,\n  RotateGlyph,\n  ScissorsGlyph,\n  SignatureGlyph,\n  StampGlyph,\n  TextFieldGlyph,\n  toArrayBuffer,\n  TrashGlyph,\n  UploadGlyph,\n  usePdfEditor,\n  withFileNameSuffix,\n} from \"@/components/extend/pdf-editor-shared\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\n/* -------------------------------------------------------------------------- */\n/* Thumbnails                                                                 */\n/* -------------------------------------------------------------------------- */\nconst THUMBNAIL_FOCUS_RING_CLASS =\n  \"group-focus-visible/pdf-editor-thumbnails:ring-2 group-focus-visible/pdf-editor-thumbnails:ring-ring group-focus-visible/pdf-editor-thumbnails:ring-offset-1 group-focus-visible/pdf-editor-thumbnails:ring-offset-background\"\nfunction createFormFieldStore(\n  scope: {\n    getFormFields: () => FormFieldInfo[]\n    onFormReady: (listener: (fields: FormFieldInfo[]) => void) => () => void\n    onFieldValueChange: (listener: () => void) => () => void\n  } | null\n) {\n  let fields: FormFieldInfo[] = scope?.getFormFields() ?? []\n  return {\n    getSnapshot: () => fields,\n    subscribe: (listener: () => void) => {\n      if (!scope) return () => {}\n      const update = (next: FormFieldInfo[]) => {\n        fields = next\n        listener()\n      }\n      const unsubscribeReady = scope.onFormReady(update)\n      const unsubscribeChange = scope.onFieldValueChange(() =>\n        update(scope.getFormFields())\n      )\n      update(scope.getFormFields())\n      return () => {\n        unsubscribeReady()\n        unsubscribeChange()\n      }\n    },\n  }\n}\nexport function PdfEditorThumbnailsPanel({\n  documentId,\n  activePage,\n  pageCount,\n  onSelectPage,\n}: {\n  documentId: string\n  activePage: number\n  pageCount: number\n  onSelectPage: (pageNumber: number) => void\n}) {\n  const { plugin: thumbnailPlugin } = useThumbnailPlugin()\n  const listboxId = React.useId()\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const scope = React.useMemo(\n    () => thumbnailPlugin?.provides().forDocument(documentId) ?? null,\n    [documentId, thumbnailPlugin]\n  )\n  const windowState = React.useSyncExternalStore<WindowState | null>(\n    React.useCallback(\n      (onStoreChange) => {\n        if (!scope) return () => undefined\n        return scope.onWindow(() => onStoreChange())\n      },\n      [scope]\n    ),\n    React.useCallback(() => scope?.getWindow() ?? null, [scope]),\n    () => null\n  )\n  const paddingY = thumbnailPlugin?.cfg.paddingY ?? 0\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !scope) return\n    const updateWindow = () => {\n      scope.updateWindow(viewport.scrollTop, viewport.clientHeight)\n    }\n    updateWindow()\n    viewport.addEventListener(\"scroll\", updateWindow, { passive: true })\n    const resizeObserver = new ResizeObserver(updateWindow)\n    resizeObserver.observe(viewport)\n    return () => {\n      viewport.removeEventListener(\"scroll\", updateWindow)\n      resizeObserver.disconnect()\n    }\n  }, [scope])\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !scope) return\n    return scope.onScrollTo(({ top, behavior }) => {\n      viewport.scrollTo({ top, behavior })\n    })\n  }, [scope])\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\") nextPage = Math.max(1, currentPage - 1)\n      else if (event.key === \"Home\") nextPage = 1\n      else if (event.key === \"End\") nextPage = pageCount\n      if (nextPage === null) return\n      event.preventDefault()\n      onSelectPage(nextPage)\n    },\n    [activePage, onSelectPage, pageCount]\n  )\n  return (\n    <PdfEditorScrollArea\n      className=\"h-full w-full\"\n      viewportClassName=\"group/pdf-editor-thumbnails px-4 focus-visible:ring-0 focus-visible:ring-offset-0\"\n      viewportProps={{\n        \"aria-activedescendant\":\n          activePage > 0 ? `${listboxId}-page-${activePage}` : undefined,\n        \"aria-label\": \"Pages\",\n        onKeyDown: handleKeyDown,\n        onMouseDown: (event) => {\n          event.currentTarget.focus({ preventScroll: true })\n        },\n        role: \"listbox\",\n        style: { paddingBottom: paddingY, paddingTop: paddingY },\n        tabIndex: 0,\n      }}\n      viewportRef={viewportRef}\n    >\n      <div\n        className=\"relative\"\n        style={{ height: windowState?.totalHeight ?? 0 }}\n      >\n        {windowState?.items.map((meta: ThumbMeta) => {\n          const pageNumber = meta.pageIndex + 1\n          const isActive = pageNumber === activePage\n          const imagePadding = meta.padding ?? 0\n          return (\n            <div\n              key={meta.pageIndex}\n              className={cn(\n                \"absolute right-0 left-0 flex justify-center\",\n                isActive && \"z-10\"\n              )}\n              style={{ top: meta.top, height: meta.wrapperHeight }}\n            >\n              <div\n                id={`${listboxId}-page-${pageNumber}`}\n                role=\"option\"\n                aria-selected={isActive}\n                aria-current={isActive ? \"page\" : undefined}\n                aria-label={`Page ${pageNumber}`}\n                style={{ maxWidth: meta.width + imagePadding * 2 + 16 }}\n                className={cn(\n                  \"flex h-full w-full cursor-default flex-col items-center justify-between rounded-md px-2 text-xs transition-shadow outline-none select-none hover:bg-sidebar-accent\",\n                  isActive\n                    ? cn(\n                        \"bg-sidebar-accent text-foreground\",\n                        THUMBNAIL_FOCUS_RING_CLASS\n                      )\n                    : \"text-muted-foreground\"\n                )}\n                onClick={() => onSelectPage(pageNumber)}\n              >\n                <span\n                  className=\"flex items-center justify-center overflow-hidden rounded-md\"\n                  style={{\n                    width: meta.width + imagePadding * 2,\n                    height: meta.height + imagePadding * 2,\n                    padding: imagePadding,\n                  }}\n                >\n                  <ThumbImg\n                    documentId={documentId}\n                    meta={meta}\n                    className=\"block rounded-sm object-contain shadow-xs\"\n                    style={{ width: meta.width, height: meta.height }}\n                  />\n                </span>\n                <span\n                  className=\"flex items-center justify-center tabular-nums\"\n                  style={{ height: meta.labelHeight }}\n                >\n                  {pageNumber}\n                </span>\n              </div>\n            </div>\n          )\n        })}\n      </div>\n    </PdfEditorScrollArea>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Outline                                                                    */\n/* -------------------------------------------------------------------------- */\nfunction resolveBookmarkDestination(bookmark: PdfBookmarkObject): {\n  destination: PdfDestinationObject | null\n  uri: string | null\n} {\n  const target = bookmark.target\n  if (!target) return { destination: null, uri: null }\n  if (target.type === \"destination\") {\n    return { destination: target.destination, uri: null }\n  }\n  const action = target.action\n  if (\n    action.type === PdfActionType.Goto ||\n    action.type === PdfActionType.RemoteGoto\n  ) {\n    return { destination: action.destination, uri: null }\n  }\n  if (action.type === PdfActionType.URI) {\n    return { destination: null, uri: action.uri }\n  }\n  return { destination: null, uri: null }\n}\nexport function PdfEditorOutlinePanel({ documentId }: { documentId: string }) {\n  const { provides: bookmark } = useBookmarkCapability()\n  const { provides: scroll } = useScrollCapability()\n  const documentState = useDocumentState(documentId)\n  const document = documentState?.document ?? null\n  const [bookmarks, setBookmarks] = React.useState<PdfBookmarkObject[] | null>(\n    null\n  )\n  const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set())\n  React.useEffect(() => {\n    if (!bookmark || !document) return\n    let cancelled = false\n    const task = bookmark.forDocument(documentId).getBookmarks()\n    task.wait(\n      (result) => {\n        if (cancelled) return\n        setBookmarks(result.bookmarks)\n        setExpanded(\n          new Set(result.bookmarks.map((_, index) => `bookmark-${index}`))\n        )\n      },\n      () => {\n        if (!cancelled) setBookmarks([])\n      }\n    )\n    return () => {\n      cancelled = true\n    }\n  }, [bookmark, document, documentId])\n  const navigate = React.useCallback(\n    (item: PdfBookmarkObject) => {\n      const { destination, uri } = resolveBookmarkDestination(item)\n      if (uri) {\n        window.open(uri, \"_blank\", \"noopener\")\n        return\n      }\n      if (!destination || !scroll) return\n      const scope = scroll.forDocument(documentId)\n      const page = document?.pages[destination.pageIndex]\n      if (destination.zoom.mode === PdfZoomMode.XYZ && page) {\n        scope.scrollToPage({\n          pageNumber: destination.pageIndex + 1,\n          pageCoordinates: {\n            x: destination.zoom.params.x,\n            y: page.size.height - destination.zoom.params.y,\n          },\n          behavior: \"smooth\",\n        })\n        return\n      }\n      scope.scrollToPage({\n        pageNumber: destination.pageIndex + 1,\n        behavior: \"smooth\",\n      })\n    },\n    [document, documentId, scroll]\n  )\n  if (bookmarks === null) {\n    return (\n      <div className=\"grid h-full place-items-center\">\n        <InlineSpinner className=\"size-4\" />\n      </div>\n    )\n  }\n  if (bookmarks.length === 0) {\n    return (\n      <PdfEditorEmptyState\n        glyph={ListTreeGlyph}\n        title=\"No outline\"\n        description=\"This document does not include bookmarks.\"\n      />\n    )\n  }\n  const renderBookmark = (\n    item: PdfBookmarkObject,\n    id: string,\n    level: number\n  ): React.ReactNode => {\n    const hasChildren = Boolean(item.children && item.children.length > 0)\n    const isExpanded = expanded.has(id)\n    return (\n      <div key={id}>\n        <div\n          className=\"group flex cursor-default items-center gap-1 rounded-md py-1 pr-2 text-sm hover:bg-sidebar-accent\"\n          style={{ paddingLeft: level * 12 + 4 }}\n        >\n          {hasChildren ? (\n            <button\n              type=\"button\"\n              aria-label={isExpanded ? \"Collapse\" : \"Expand\"}\n              className=\"grid size-5 shrink-0 place-items-center rounded text-muted-foreground hover:text-foreground\"\n              onClick={(event) => {\n                event.stopPropagation()\n                setExpanded((previous) => {\n                  const next = new Set(previous)\n                  if (next.has(id)) next.delete(id)\n                  else next.add(id)\n                  return next\n                })\n              }}\n            >\n              {isExpanded ? (\n                <ChevronDownGlyph className=\"size-3.5\" />\n              ) : (\n                <ChevronRightGlyph className=\"size-3.5\" />\n              )}\n            </button>\n          ) : (\n            <span className=\"size-5 shrink-0\" />\n          )}\n          <button\n            type=\"button\"\n            className=\"min-w-0 flex-1 truncate text-left text-foreground/90 outline-none\"\n            onClick={() => navigate(item)}\n          >\n            {item.title || \"Untitled\"}\n          </button>\n        </div>\n        {hasChildren && isExpanded\n          ? item.children?.map((child, index) =>\n              renderBookmark(child, `${id}-${index}`, level + 1)\n            )\n          : null}\n      </div>\n    )\n  }\n  return (\n    <PdfEditorScrollArea className=\"h-full w-full\" viewportClassName=\"p-2\">\n      {bookmarks.map((item, index) =>\n        renderBookmark(item, `bookmark-${index}`, 0)\n      )}\n    </PdfEditorScrollArea>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Attachments                                                                */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorAttachmentsPanel({\n  documentId,\n}: {\n  documentId: string\n}) {\n  const { provides: attachment } = useAttachmentCapability()\n  const { registry } = useRegistry()\n  const documentState = useDocumentState(documentId)\n  const { notify, permissions } = usePdfEditor()\n  const document = documentState?.document ?? null\n  const [attachments, setAttachments] = React.useState<\n    PdfAttachmentObject[] | null\n  >(null)\n  const [version, setVersion] = React.useState(0)\n  const [busy, setBusy] = React.useState(false)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  React.useEffect(() => {\n    if (!attachment || !document) return\n    let cancelled = false\n    attachment\n      .forDocument(documentId)\n      .getAttachments()\n      .wait(\n        (result) => {\n          if (!cancelled) setAttachments(result)\n        },\n        () => {\n          if (!cancelled) setAttachments([])\n        }\n      )\n    return () => {\n      cancelled = true\n    }\n  }, [attachment, document, documentId, version])\n  const refresh = () => setVersion((value) => value + 1)\n  const handleDownload = (item: PdfAttachmentObject) => {\n    attachment\n      ?.forDocument(documentId)\n      .downloadAttachment(item)\n      .wait(\n        (buffer) =>\n          downloadArrayBuffer(\n            buffer,\n            item.name,\n            item.mimeType || \"application/octet-stream\"\n          ),\n        () => notify(\"The attachment could not be downloaded.\", \"error\")\n      )\n  }\n  const handleRemove = (item: PdfAttachmentObject) => {\n    if (!registry || !document) return\n    setBusy(true)\n    registry\n      .getEngine()\n      .removeAttachment(document, item)\n      .wait(\n        () => {\n          setBusy(false)\n          refresh()\n          notify(\"Attachment removed.\", \"success\")\n        },\n        () => {\n          setBusy(false)\n          notify(\"The attachment could not be removed.\", \"error\")\n        }\n      )\n  }\n  const handleAdd = async (file: File) => {\n    if (!registry || !document) return\n    setBusy(true)\n    try {\n      const data = await file.arrayBuffer()\n      registry\n        .getEngine()\n        .addAttachment(document, {\n          name: file.name,\n          description: \"\",\n          mimeType: file.type || \"application/octet-stream\",\n          data,\n        })\n        .wait(\n          () => {\n            setBusy(false)\n            refresh()\n            notify(\"Attachment added.\", \"success\")\n          },\n          () => {\n            setBusy(false)\n            notify(\"The attachment could not be added.\", \"error\")\n          }\n        )\n    } catch {\n      setBusy(false)\n      notify(\"The file could not be read.\", \"error\")\n    }\n  }\n  const canModify = permissions.canModifyContents && !busy\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"flex shrink-0 items-center gap-2 border-b px-3 py-2\">\n        <input\n          ref={inputRef}\n          type=\"file\"\n          className=\"sr-only\"\n          tabIndex={-1}\n          onChange={(event) => {\n            const file = event.target.files?.[0]\n            if (file) void handleAdd(file)\n            event.currentTarget.value = \"\"\n          }}\n        />\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          disabled={!canModify}\n          onClick={() => inputRef.current?.click()}\n        >\n          <PlusGlyph className=\"size-4\" />\n          Attach file\n        </Button>\n        {busy ? <InlineSpinner className=\"size-4\" /> : null}\n      </div>\n      {attachments === null ? (\n        <div className=\"grid flex-1 place-items-center\">\n          <InlineSpinner className=\"size-4\" />\n        </div>\n      ) : attachments.length === 0 ? (\n        <PdfEditorEmptyState\n          glyph={PaperclipGlyph}\n          title=\"No attachments\"\n          description=\"Files embedded in the PDF appear here.\"\n        />\n      ) : (\n        <PdfEditorScrollArea className=\"min-h-0 flex-1\" viewportClassName=\"p-2\">\n          <div className=\"space-y-1\">\n            {attachments.map((item) => (\n              <div\n                key={`${item.index}-${item.name}`}\n                className=\"flex items-center gap-2 rounded-lg border px-2.5 py-2\"\n              >\n                <div className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n                  <PaperclipGlyph className=\"size-4\" />\n                </div>\n                <div className=\"min-w-0 flex-1\">\n                  <div className=\"truncate text-sm font-medium\">\n                    {item.name}\n                  </div>\n                  <div className=\"truncate text-xs text-muted-foreground\">\n                    {[item.mimeType, formatBytes(item.size), item.description]\n                      .filter(Boolean)\n                      .join(\" · \")}\n                  </div>\n                </div>\n                <PdfEditorToolButton\n                  label=\"Download\"\n                  size=\"icon-xs\"\n                  onClick={() => handleDownload(item)}\n                >\n                  <DownloadGlyph className=\"size-3.5\" />\n                </PdfEditorToolButton>\n                <PdfEditorToolButton\n                  label=\"Remove\"\n                  size=\"icon-xs\"\n                  disabled={!canModify}\n                  onClick={() => handleRemove(item)}\n                >\n                  <TrashGlyph className=\"size-3.5\" />\n                </PdfEditorToolButton>\n              </div>\n            ))}\n          </div>\n        </PdfEditorScrollArea>\n      )}\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Pages (organizer)                                                          */\n/* -------------------------------------------------------------------------- */\nconst PAGE_TILE_WIDTH = 76\ntype PdfLibModule = {\n  PDFDocument: typeof PDFDocument\n  ParseSpeeds: typeof ParseSpeeds\n  degrees: typeof degrees\n}\nlet pdfLibPromise: Promise<PdfLibModule> | null = null\nfunction loadPdfLib() {\n  pdfLibPromise ??= import(\"pdf-lib\")\n  return pdfLibPromise\n}\nexport function PdfEditorPagesPanel({\n  documentId,\n  activePage,\n}: {\n  documentId: string\n  activePage: number\n}) {\n  const { registry } = useRegistry()\n  const documentState = useDocumentState(documentId)\n  const { state: annotationState, provides: annotation } =\n    useAnnotation(documentId)\n  const {\n    fileName,\n    getDocumentBuffer,\n    notify,\n    openDialog,\n    permissions,\n    replaceDocument,\n    scrollToPage,\n  } = usePdfEditor()\n  const document = documentState?.document ?? null\n  const pages = React.useMemo(() => document?.pages ?? [], [document])\n  const [selected, setSelected] = React.useState<Set<number>>(() => new Set())\n  const [busy, setBusy] = React.useState<string | null>(null)\n  const anchorRef = React.useRef<number | null>(null)\n  const appendInputRef = React.useRef<HTMLInputElement>(null)\n  React.useEffect(() => {\n    void loadPdfLib()\n  }, [])\n  const metas = React.useMemo<ThumbMeta[]>(\n    () =>\n      pages.map((page) => {\n        const height = Math.max(\n          1,\n          Math.round(PAGE_TILE_WIDTH * (page.size.height / page.size.width))\n        )\n        return {\n          pageIndex: page.index,\n          width: PAGE_TILE_WIDTH,\n          height,\n          wrapperHeight: height,\n          top: 0,\n          labelHeight: 0,\n          padding: 0,\n        }\n      }),\n    [pages]\n  )\n  const selectedIndices = React.useMemo(\n    () => [...selected].sort((a, b) => a - b),\n    [selected]\n  )\n  const isEncrypted = document?.isEncrypted ?? false\n  const canAssemble =\n    permissions.canAssembleDocument && !isEncrypted && busy === null\n  const hasSelection = selectedIndices.length > 0\n  const isContiguous =\n    hasSelection &&\n    selectedIndices[selectedIndices.length - 1] - selectedIndices[0] ===\n      selectedIndices.length - 1\n  const select = (pageIndex: number, mode: \"replace\" | \"toggle\" | \"range\") => {\n    setSelected((previous) => {\n      if (mode === \"range\" && anchorRef.current !== null) {\n        const start = Math.min(anchorRef.current, pageIndex)\n        const end = Math.max(anchorRef.current, pageIndex)\n        const next = new Set<number>()\n        for (let index = start; index <= end; index += 1) next.add(index)\n        return next\n      }\n      if (mode === \"toggle\") {\n        const next = new Set(previous)\n        if (next.has(pageIndex)) next.delete(pageIndex)\n        else next.add(pageIndex)\n        anchorRef.current = pageIndex\n        return next\n      }\n      anchorRef.current = pageIndex\n      return new Set([pageIndex])\n    })\n  }\n  const runOperation = async (\n    label: string,\n    mutate: (doc: PDFDocument, lib: PdfLibModule) => Promise<void> | void,\n    options: {\n      replace?: boolean\n    } = { replace: true }\n  ) => {\n    setBusy(label)\n    try {\n      const [buffer, lib] = await Promise.all([\n        getDocumentBuffer(),\n        loadPdfLib(),\n      ])\n      const doc = await lib.PDFDocument.load(buffer, {\n        ignoreEncryption: true,\n        parseSpeed: lib.ParseSpeeds.Fastest,\n        updateMetadata: false,\n      })\n      await mutate(doc, lib)\n      if (options.replace !== false) {\n        const bytes = await doc.save({ objectsPerTick: 500 })\n        replaceDocument(toArrayBuffer(bytes), fileName)\n        notify(`${label} applied.`, \"success\")\n      }\n    } catch (error) {\n      console.error(error)\n      notify(`${label} failed.`, \"error\")\n    } finally {\n      setBusy(null)\n    }\n  }\n  const rotateSelected = (delta: 90 | -90) =>\n    runOperation(\"Rotate pages\", (doc, lib) => {\n      for (const index of selectedIndices) {\n        const page = doc.getPage(index)\n        page.setRotation(\n          lib.degrees((page.getRotation().angle + delta + 360) % 360)\n        )\n      }\n    })\n  const deleteSelected = () => {\n    if (selectedIndices.length >= pages.length) {\n      notify(\"A document needs at least one page.\", \"error\")\n      return\n    }\n    openDialog({\n      type: \"confirm\",\n      title: `Delete ${selectedIndices.length === 1 ? \"this page\" : `${selectedIndices.length} pages`}?`,\n      description:\n        \"Content and annotations on the deleted pages are removed permanently.\",\n      confirmLabel: \"Delete\",\n      destructive: true,\n      onConfirm: async () => {\n        setBusy(\"Delete pages\")\n        try {\n          if (!registry || !document) {\n            throw new Error(\"The document is not ready.\")\n          }\n          if (annotation && annotationState.hasPendingChanges) {\n            await annotation.commit().toPromise()\n          }\n          const engine = registry.getEngine()\n          for (const index of [...selectedIndices].reverse()) {\n            await engine.deletePage(document, index).toPromise()\n          }\n          const buffer = await engine.saveAsCopy(document).toPromise()\n          replaceDocument(buffer, fileName)\n          notify(\"Delete pages applied.\", \"success\")\n        } catch (error) {\n          console.error(error)\n          notify(\"Delete pages failed.\", \"error\")\n        } finally {\n          setBusy(null)\n        }\n      },\n    })\n  }\n  const moveSelected = (direction: -1 | 1) => {\n    const first = selectedIndices[0]\n    const last = selectedIndices[selectedIndices.length - 1]\n    if (direction === -1 && first === 0) return\n    if (direction === 1 && last === pages.length - 1) return\n    void runOperation(\"Move pages\", (doc) => {\n      // pdf-lib has no move API: detach the neighbouring page from the page\n      // tree and re-insert it on the other side of the selected block.\n      if (direction === -1) {\n        const page = doc.getPage(first - 1)\n        doc.removePage(first - 1)\n        doc.insertPage(last, page)\n      } else {\n        const page = doc.getPage(last + 1)\n        doc.removePage(last + 1)\n        doc.insertPage(first, page)\n      }\n    })\n  }\n  const insertBlankAfter = () => {\n    const referenceIndex = hasSelection\n      ? selectedIndices[selectedIndices.length - 1]\n      : Math.max(0, activePage - 1)\n    void runOperation(\"Insert page\", (doc) => {\n      const reference = doc.getPage(referenceIndex)\n      const { width, height } = reference.getSize()\n      doc.insertPage(referenceIndex + 1, [width, height])\n    })\n  }\n  const extractSelected = () =>\n    runOperation(\n      \"Extract pages\",\n      async (doc, lib) => {\n        const output = await lib.PDFDocument.create()\n        const copied = await output.copyPages(doc, selectedIndices)\n        copied.forEach((page) => output.addPage(page))\n        const bytes = await output.save()\n        downloadArrayBuffer(bytes, withFileNameSuffix(fileName, \"-pages\"))\n        notify(\n          `Extracted ${selectedIndices.length} page${selectedIndices.length === 1 ? \"\" : \"s\"}.`,\n          \"success\"\n        )\n      },\n      { replace: false }\n    )\n  const appendFile = (file: File) =>\n    runOperation(\"Append PDF\", async (doc, lib) => {\n      const source = await lib.PDFDocument.load(await file.arrayBuffer(), {\n        ignoreEncryption: true,\n        parseSpeed: lib.ParseSpeeds.Fastest,\n        updateMetadata: false,\n      })\n      const copied = await doc.copyPages(source, source.getPageIndices())\n      copied.forEach((page) => doc.addPage(page))\n    })\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"flex shrink-0 flex-wrap items-center gap-0.5 border-b px-2 py-1.5\">\n        <PdfEditorToolButton\n          label=\"Rotate left\"\n          disabled={!canAssemble || !hasSelection}\n          onClick={() => rotateSelected(-90)}\n        >\n          <RotateGlyph className=\"size-4 -scale-x-100\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Rotate right\"\n          disabled={!canAssemble || !hasSelection}\n          onClick={() => rotateSelected(90)}\n        >\n          <RotateGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolbarSeparator />\n        <PdfEditorToolButton\n          label=\"Move up\"\n          disabled={!canAssemble || !isContiguous || selectedIndices[0] === 0}\n          onClick={() => moveSelected(-1)}\n        >\n          <ArrowUpGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Move down\"\n          disabled={\n            !canAssemble ||\n            !isContiguous ||\n            selectedIndices[selectedIndices.length - 1] === pages.length - 1\n          }\n          onClick={() => moveSelected(1)}\n        >\n          <ArrowDownGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolbarSeparator />\n        <PdfEditorToolButton\n          label=\"Insert blank page after\"\n          disabled={!canAssemble}\n          onClick={insertBlankAfter}\n        >\n          <FilePlusGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Append PDF\"\n          disabled={!canAssemble}\n          onClick={() => appendInputRef.current?.click()}\n        >\n          <MergeGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Extract selected pages\"\n          disabled={busy !== null || !hasSelection}\n          onClick={() => void extractSelected()}\n        >\n          <ScissorsGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <PdfEditorToolButton\n          label=\"Delete pages\"\n          disabled={!canAssemble || !hasSelection}\n          onClick={deleteSelected}\n        >\n          <TrashGlyph className=\"size-4\" />\n        </PdfEditorToolButton>\n        <input\n          ref={appendInputRef}\n          type=\"file\"\n          accept=\"application/pdf,.pdf\"\n          className=\"sr-only\"\n          tabIndex={-1}\n          onChange={(event) => {\n            const file = event.target.files?.[0]\n            if (file) void appendFile(file)\n            event.currentTarget.value = \"\"\n          }}\n        />\n      </div>\n      <div className=\"flex shrink-0 items-center justify-between gap-2 border-b px-3 py-1.5 text-xs text-muted-foreground\">\n        <span>\n          {hasSelection\n            ? `${selectedIndices.length} of ${pages.length} selected`\n            : `${pages.length} page${pages.length === 1 ? \"\" : \"s\"}`}\n        </span>\n        {busy ? (\n          <span className=\"flex items-center gap-1.5\">\n            <InlineSpinner className=\"size-3\" />\n            {busy}…\n          </span>\n        ) : isEncrypted ? (\n          <span>Remove protection to edit pages.</span>\n        ) : hasSelection ? (\n          <button\n            type=\"button\"\n            className=\"underline-offset-2 hover:underline\"\n            onClick={() => setSelected(new Set())}\n          >\n            Clear\n          </button>\n        ) : null}\n      </div>\n      <PdfEditorScrollArea\n        className=\"min-h-0 flex-1\"\n        viewportClassName=\"px-2.5 py-3\"\n      >\n        <div\n          data-slot=\"pdf-editor-pages-grid\"\n          className=\"grid grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] items-start gap-2\"\n        >\n          {metas.map((meta) => {\n            const pageNumber = meta.pageIndex + 1\n            const isSelected = selected.has(meta.pageIndex)\n            const isActive = pageNumber === activePage\n            return (\n              <button\n                key={meta.pageIndex}\n                type=\"button\"\n                aria-pressed={isSelected}\n                className={cn(\n                  \"group flex min-w-0 flex-col items-center gap-1.5 rounded-lg border p-1.5 text-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n                  isSelected\n                    ? \"border-primary bg-primary/5\"\n                    : \"border-transparent hover:bg-sidebar-accent\",\n                  isActive && !isSelected && \"border-border\"\n                )}\n                onClick={(event) => {\n                  select(\n                    meta.pageIndex,\n                    event.shiftKey\n                      ? \"range\"\n                      : event.metaKey || event.ctrlKey\n                        ? \"toggle\"\n                        : \"replace\"\n                  )\n                }}\n                onDoubleClick={() => scrollToPage(pageNumber)}\n              >\n                <span className=\"grid w-full place-items-center rounded-md border border-border/60 bg-muted/30 p-1\">\n                  <span\n                    className=\"grid w-full place-items-center overflow-hidden rounded-sm bg-white shadow-xs\"\n                    style={{ aspectRatio: `${meta.width} / ${meta.height}` }}\n                  >\n                    <ThumbImg\n                      documentId={documentId}\n                      meta={meta}\n                      className=\"block size-full object-contain\"\n                    />\n                  </span>\n                </span>\n                <span\n                  className={cn(\n                    \"tabular-nums\",\n                    isSelected ? \"text-foreground\" : \"text-muted-foreground\"\n                  )}\n                >\n                  {pageNumber}\n                </span>\n              </button>\n            )\n          })}\n        </div>\n      </PdfEditorScrollArea>\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Comments                                                                   */\n/* -------------------------------------------------------------------------- */\nfunction CommentComposer({\n  placeholder,\n  initialValue = \"\",\n  autoFocus,\n  submitLabel = \"Save\",\n  onSubmit,\n  onCancel,\n}: {\n  placeholder: string\n  initialValue?: string\n  autoFocus?: boolean\n  submitLabel?: string\n  onSubmit: (value: string) => void\n  onCancel?: () => void\n}) {\n  const [value, setValue] = React.useState(initialValue)\n  return (\n    <div className=\"space-y-1.5\">\n      <textarea\n        value={value}\n        autoFocus={autoFocus}\n        rows={2}\n        placeholder={placeholder}\n        onChange={(event) => setValue(event.target.value)}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\" && (event.metaKey || event.ctrlKey)) {\n            event.preventDefault()\n            if (value.trim()) onSubmit(value.trim())\n          }\n          if (event.key === \"Escape\") onCancel?.()\n        }}\n        className=\"w-full resize-none rounded-md border border-input bg-background px-2 py-1.5 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      />\n      <div className=\"flex justify-end gap-1\">\n        {onCancel ? (\n          <Button type=\"button\" size=\"xs\" variant=\"ghost\" onClick={onCancel}>\n            Cancel\n          </Button>\n        ) : null}\n        <Button\n          type=\"button\"\n          size=\"xs\"\n          disabled={!value.trim()}\n          onClick={() => onSubmit(value.trim())}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n    </div>\n  )\n}\nfunction CommentCard({\n  entry,\n  isSelected,\n  readOnly,\n  onSelect,\n  onUpdate,\n  onDelete,\n  onReply,\n}: {\n  entry: SidebarAnnotationEntry\n  isSelected: boolean\n  readOnly: boolean\n  onSelect: () => void\n  onUpdate: (annotation: TrackedAnnotation, contents: string) => void\n  onDelete: (annotation: TrackedAnnotation) => void\n  onReply: (annotation: TrackedAnnotation, contents: string) => void\n}) {\n  const { annotation, replies } = entry\n  const [isEditing, setIsEditing] = React.useState(false)\n  const [isReplying, setIsReplying] = React.useState(false)\n  const meta = getAnnotationMeta(annotation.object)\n  const contents =\n    typeof annotation.object.contents === \"string\"\n      ? annotation.object.contents\n      : \"\"\n  const quotedText =\n    annotation.object.custom &&\n    typeof annotation.object.custom === \"object\" &&\n    typeof (\n      annotation.object.custom as {\n        text?: unknown\n      }\n    ).text === \"string\"\n      ? ((\n          annotation.object.custom as {\n            text: string\n          }\n        ).text as string)\n      : null\n  const Glyph = meta.glyph\n  return (\n    <div\n      role=\"button\"\n      tabIndex={0}\n      onClick={onSelect}\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\") onSelect()\n      }}\n      className={cn(\n        \"rounded-lg border bg-background p-3 text-left shadow-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        isSelected\n          ? \"border-primary ring-1 ring-primary/40\"\n          : \"hover:bg-accent/40\"\n      )}\n    >\n      <div className=\"flex items-start gap-2\">\n        <span\n          className=\"grid size-7 shrink-0 place-items-center rounded-full bg-muted\"\n          style={meta.color ? { color: meta.color } : undefined}\n        >\n          <Glyph className=\"size-3.5\" />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex items-center justify-between gap-2\">\n            <div className=\"min-w-0\">\n              <div className=\"truncate text-sm font-medium\">\n                {annotation.object.author || \"Guest\"}\n              </div>\n              <div className=\"text-[11px] text-muted-foreground\">\n                {meta.label} · page {annotation.object.pageIndex + 1}\n                {annotation.object.modified || annotation.object.created\n                  ? ` · ${formatRelativeDate(annotation.object.modified ?? annotation.object.created)}`\n                  : \"\"}\n              </div>\n            </div>\n            {!readOnly ? (\n              <div className=\"flex shrink-0 items-center gap-0.5\">\n                <PdfEditorToolButton\n                  label={contents ? \"Edit comment\" : \"Add comment\"}\n                  size=\"icon-xs\"\n                  onClick={(event) => {\n                    event.stopPropagation()\n                    setIsEditing(true)\n                  }}\n                >\n                  <EditGlyph className=\"size-3.5\" />\n                </PdfEditorToolButton>\n                <PdfEditorToolButton\n                  label=\"Delete\"\n                  size=\"icon-xs\"\n                  onClick={(event) => {\n                    event.stopPropagation()\n                    onDelete(annotation)\n                  }}\n                >\n                  <TrashGlyph className=\"size-3.5\" />\n                </PdfEditorToolButton>\n              </div>\n            ) : null}\n          </div>\n          {quotedText ? (\n            <blockquote className=\"mt-1.5 line-clamp-3 border-l-2 pl-2 text-xs text-muted-foreground italic\">\n              {quotedText}\n            </blockquote>\n          ) : null}\n          {isEditing ? (\n            <div className=\"mt-2\" onClick={(event) => event.stopPropagation()}>\n              <CommentComposer\n                placeholder=\"Write a comment…\"\n                initialValue={contents}\n                autoFocus\n                onSubmit={(value) => {\n                  onUpdate(annotation, value)\n                  setIsEditing(false)\n                }}\n                onCancel={() => setIsEditing(false)}\n              />\n            </div>\n          ) : contents ? (\n            <p className=\"mt-1.5 text-sm whitespace-pre-wrap\">{contents}</p>\n          ) : (\n            <p className=\"mt-1.5 text-xs text-muted-foreground\">\n              No comment yet.\n            </p>\n          )}\n        </div>\n      </div>\n      {replies.length > 0 ? (\n        <div className=\"mt-2 space-y-2 border-l pl-3\">\n          {replies.map((reply) => (\n            <div key={reply.object.id} className=\"text-sm\">\n              <div className=\"flex items-center justify-between gap-2\">\n                <span className=\"text-xs font-medium\">\n                  {reply.object.author || \"Guest\"}\n                  <span className=\"ml-1.5 font-normal text-muted-foreground\">\n                    {formatRelativeDate(\n                      reply.object.modified ?? reply.object.created\n                    )}\n                  </span>\n                </span>\n                {!readOnly ? (\n                  <PdfEditorToolButton\n                    label=\"Delete reply\"\n                    size=\"icon-xs\"\n                    onClick={(event) => {\n                      event.stopPropagation()\n                      onDelete(reply)\n                    }}\n                  >\n                    <TrashGlyph className=\"size-3\" />\n                  </PdfEditorToolButton>\n                ) : null}\n              </div>\n              <p className=\"whitespace-pre-wrap\">{reply.object.contents}</p>\n            </div>\n          ))}\n        </div>\n      ) : null}\n      {!readOnly ? (\n        <div className=\"mt-2\" onClick={(event) => event.stopPropagation()}>\n          {isReplying ? (\n            <CommentComposer\n              placeholder=\"Reply…\"\n              autoFocus\n              submitLabel=\"Reply\"\n              onSubmit={(value) => {\n                onReply(annotation, value)\n                setIsReplying(false)\n              }}\n              onCancel={() => setIsReplying(false)}\n            />\n          ) : (\n            <Button\n              type=\"button\"\n              size=\"xs\"\n              variant=\"ghost\"\n              className=\"text-muted-foreground\"\n              onClick={() => setIsReplying(true)}\n            >\n              Reply\n            </Button>\n          )}\n        </div>\n      ) : null}\n    </div>\n  )\n}\nexport function PdfEditorCommentsPanel({ documentId }: { documentId: string }) {\n  const { provides: annotation, state } = useAnnotation(documentId)\n  const { annotationAuthor, permissions, scrollToAnnotation } = usePdfEditor()\n  const grouped = React.useMemo(\n    () => getSidebarAnnotationsWithRepliesGroupedByPage(state),\n    [state]\n  )\n  const selectedLeaderIds = React.useMemo(() => {\n    const ids = new Set<string>()\n    for (const item of getSelectedAnnotations(state)) {\n      ids.add(getGroupLeaderId(state, item.object.id) ?? item.object.id)\n    }\n    return ids\n  }, [state])\n  const pageNumbers = React.useMemo(\n    () =>\n      Object.keys(grouped)\n        .map(Number)\n        .sort((a, b) => a - b),\n    [grouped]\n  )\n  const readOnly = !permissions.canModifyAnnotations\n  const cardRefs = React.useRef<Map<string, HTMLDivElement>>(new Map())\n  const firstSelectedId = selectedLeaderIds.values().next().value\n  React.useEffect(() => {\n    if (!firstSelectedId) return\n    const element = cardRefs.current.get(firstSelectedId)\n    element?.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" })\n  }, [firstSelectedId])\n  if (pageNumbers.length === 0) {\n    return (\n      <PdfEditorEmptyState\n        glyph={CommentsGlyph}\n        title=\"No comments yet\"\n        description=\"Annotations and notes you add to the document show up here.\"\n      />\n    )\n  }\n  return (\n    <PdfEditorScrollArea className=\"h-full w-full\" viewportClassName=\"p-3\">\n      <div className=\"space-y-5\">\n        {pageNumbers.map((pageIndex) => (\n          <div key={pageIndex} className=\"space-y-2\">\n            <div className=\"text-xs font-medium text-muted-foreground\">\n              Page {pageIndex + 1}\n            </div>\n            <div className=\"space-y-2\">\n              {grouped[pageIndex].map((entry) => (\n                <div\n                  key={entry.annotation.object.id}\n                  ref={(element) => {\n                    if (element) {\n                      cardRefs.current.set(entry.annotation.object.id, element)\n                    } else {\n                      cardRefs.current.delete(entry.annotation.object.id)\n                    }\n                  }}\n                >\n                  <CommentCard\n                    entry={entry}\n                    isSelected={selectedLeaderIds.has(\n                      entry.annotation.object.id\n                    )}\n                    readOnly={readOnly}\n                    onSelect={() => {\n                      annotation?.selectAnnotation(\n                        entry.annotation.object.pageIndex,\n                        entry.annotation.object.id\n                      )\n                      scrollToAnnotation(entry.annotation.object)\n                    }}\n                    onUpdate={(item, contents) =>\n                      annotation?.updateAnnotation(\n                        item.object.pageIndex,\n                        item.object.id,\n                        {\n                          contents,\n                          modified: new Date(),\n                        }\n                      )\n                    }\n                    onDelete={(item) =>\n                      annotation?.deleteAnnotation(\n                        item.object.pageIndex,\n                        item.object.id\n                      )\n                    }\n                    onReply={(parent, contents) =>\n                      annotation?.createAnnotation(parent.object.pageIndex, {\n                        id: uuidV4(),\n                        type: PdfAnnotationSubtype.TEXT,\n                        pageIndex: parent.object.pageIndex,\n                        rect: {\n                          origin: {\n                            x: parent.object.rect.origin.x,\n                            y: parent.object.rect.origin.y,\n                          },\n                          size: { width: 24, height: 24 },\n                        },\n                        contents,\n                        inReplyToId: parent.object.id,\n                        flags: [\"noRotate\", \"noZoom\", \"print\"],\n                        name: PdfAnnotationName.Comment,\n                        author: annotationAuthor,\n                        created: new Date(),\n                        modified: new Date(),\n                      })\n                    }\n                  />\n                </div>\n              ))}\n            </div>\n          </div>\n        ))}\n      </div>\n    </PdfEditorScrollArea>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Redactions                                                                 */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorRedactionsPanel({\n  documentId,\n}: {\n  documentId: string\n}) {\n  const { state, provides } = useRedaction(documentId)\n  const { provides: scroll } = useScrollCapability()\n  const { openDialog, permissions, notify } = usePdfEditor()\n  const items = React.useMemo(\n    () =>\n      Object.entries(state.pending)\n        .flatMap(([page, pageItems]) =>\n          pageItems.map((item) => ({ item, pageNumber: Number(page) + 1 }))\n        )\n        .sort((a, b) => a.pageNumber - b.pageNumber),\n    [state.pending]\n  )\n  const [isApplying, setIsApplying] = React.useState(false)\n  const canApply = permissions.canModifyContents && !isApplying\n  const selectItem = (item: RedactionItem) => {\n    provides?.selectPending(item.page, item.id)\n    scroll?.forDocument(documentId).scrollToPage({\n      pageNumber: item.page + 1,\n      pageCoordinates: { x: item.rect.origin.x, y: item.rect.origin.y },\n      alignX: 50,\n      alignY: 25,\n      behavior: \"smooth\",\n    })\n  }\n  const applyAll = () =>\n    openDialog({\n      type: \"confirm\",\n      title: `Apply ${items.length} redaction${items.length === 1 ? \"\" : \"s\"}?`,\n      description:\n        \"Redaction permanently removes the marked content from the document. This cannot be undone.\",\n      confirmLabel: \"Apply redactions\",\n      onConfirm: () => {\n        if (!provides) return\n        setIsApplying(true)\n        provides.commitAllPending().wait(\n          () => setIsApplying(false),\n          () => {\n            setIsApplying(false)\n            notify(\"The redactions could not be applied.\", \"error\")\n          }\n        )\n      },\n    })\n  const applyOne = (item: RedactionItem) =>\n    openDialog({\n      type: \"confirm\",\n      title: \"Apply this redaction?\",\n      description: \"The marked content is removed permanently.\",\n      confirmLabel: \"Apply\",\n      onConfirm: () => {\n        provides?.commitPending(item.page, item.id).wait(\n          () => undefined,\n          () => notify(\"The redaction could not be applied.\", \"error\")\n        )\n      },\n    })\n  return (\n    <div className=\"flex h-full flex-col\">\n      {items.length === 0 ? (\n        <PdfEditorEmptyState\n          glyph={EyeOffGlyph}\n          title=\"No pending redactions\"\n          description=\"Use the Redact tools to mark text or areas. Marks stay editable until you apply them.\"\n        />\n      ) : (\n        <PdfEditorScrollArea className=\"min-h-0 flex-1\" viewportClassName=\"p-2\">\n          <div className=\"space-y-1.5\">\n            {items.map(({ item, pageNumber }) => {\n              const isSelected = state.selected?.id === item.id\n              const Glyph = item.kind === \"text\" ? EyeOffGlyph : RedactAreaGlyph\n              return (\n                <div\n                  key={item.id}\n                  role=\"button\"\n                  tabIndex={0}\n                  onClick={() => selectItem(item)}\n                  onKeyDown={(event) => {\n                    if (event.key === \"Enter\") selectItem(item)\n                  }}\n                  className={cn(\n                    \"flex items-start gap-2 rounded-lg border px-2.5 py-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                    isSelected\n                      ? \"border-primary ring-1 ring-primary/40\"\n                      : \"hover:bg-accent/40\"\n                  )}\n                >\n                  <span\n                    className=\"mt-0.5 grid size-6 shrink-0 place-items-center rounded-md\"\n                    style={{\n                      backgroundColor: `${item.markColor}22`,\n                      color: item.markColor,\n                    }}\n                  >\n                    <Glyph className=\"size-3.5\" />\n                  </span>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"text-sm font-medium\">\n                      {item.kind === \"text\" ? \"Text\" : \"Area\"}\n                      <span className=\"ml-1.5 text-xs font-normal text-muted-foreground\">\n                        page {pageNumber}\n                      </span>\n                    </div>\n                    {item.kind === \"text\" && item.text ? (\n                      <div className=\"truncate text-xs text-muted-foreground italic\">\n                        “{item.text}”\n                      </div>\n                    ) : null}\n                  </div>\n                  <div className=\"flex shrink-0 items-center gap-0.5\">\n                    <PdfEditorToolButton\n                      label=\"Apply\"\n                      size=\"icon-xs\"\n                      disabled={!canApply}\n                      onClick={(event) => {\n                        event.stopPropagation()\n                        applyOne(item)\n                      }}\n                    >\n                      <CheckGlyph className=\"size-3.5\" />\n                    </PdfEditorToolButton>\n                    <PdfEditorToolButton\n                      label=\"Remove mark\"\n                      size=\"icon-xs\"\n                      onClick={(event) => {\n                        event.stopPropagation()\n                        provides?.removePending(item.page, item.id)\n                      }}\n                    >\n                      <TrashGlyph className=\"size-3.5\" />\n                    </PdfEditorToolButton>\n                  </div>\n                </div>\n              )\n            })}\n          </div>\n        </PdfEditorScrollArea>\n      )}\n      {items.length > 0 ? (\n        <div className=\"flex shrink-0 gap-2 border-t p-2\">\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            className=\"flex-1\"\n            disabled={isApplying}\n            onClick={() => provides?.clearPending()}\n          >\n            Clear all\n          </Button>\n          <InlineLoadingButton\n            type=\"button\"\n            size=\"sm\"\n            className=\"flex-1\"\n            loading={isApplying}\n            disabled={!canApply}\n            onClick={applyAll}\n          >\n            Apply all\n          </InlineLoadingButton>\n        </div>\n      ) : null}\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Stamps                                                                     */\n/* -------------------------------------------------------------------------- */\nconst CUSTOM_STAMP_LIBRARY_ID = \"custom\"\nconst STAMP_THUMB_WIDTH = 120\nconst UNSUPPORTED_STAMP_SOURCE_TYPES = new Set<PdfAnnotationSubtype>([\n  PdfAnnotationSubtype.REDACT,\n  PdfAnnotationSubtype.HIGHLIGHT,\n  PdfAnnotationSubtype.SQUIGGLY,\n  PdfAnnotationSubtype.UNDERLINE,\n  PdfAnnotationSubtype.STRIKEOUT,\n  PdfAnnotationSubtype.CARET,\n  PdfAnnotationSubtype.WIDGET,\n  PdfAnnotationSubtype.LINK,\n])\nexport function PdfEditorStampsPanel({ documentId }: { documentId: string }) {\n  const { libraries } = useStampLibraries()\n  const { provides: stampCapability } = useStampCapability()\n  const { provides: annotation, state: annotationState } =\n    useAnnotation(documentId)\n  const { notify, permissions } = usePdfEditor()\n  const [libraryId, setLibraryId] = React.useState(\"all\")\n  const stamps = useStampsByLibrary(libraryId === \"all\" ? undefined : libraryId)\n  const activeStamp = useActiveStamp(documentId)\n  const [busy, setBusy] = React.useState(false)\n  const importInputRef = React.useRef<HTMLInputElement>(null)\n  const selected = React.useMemo(\n    () => getSelectedAnnotations(annotationState),\n    [annotationState]\n  )\n  const canCreateFromSelection =\n    selected.length === 1 &&\n    !UNSUPPORTED_STAMP_SOURCE_TYPES.has(selected[0].object.type)\n  const customLibrary = libraries.find(\n    (library) => library.id === CUSTOM_STAMP_LIBRARY_ID\n  )\n  const libraryOptions = [\n    { label: \"All libraries\", value: \"all\" },\n    ...libraries.map((library) => ({\n      label: library.name,\n      value: library.id,\n    })),\n  ]\n  if (\n    libraryId !== \"all\" &&\n    !libraries.some((library) => library.id === libraryId)\n  ) {\n    setLibraryId(\"all\")\n  }\n  const placeStamp = (targetLibraryId: string, stamp: StampDefinition) => {\n    if (!stampCapability) return\n    const isActive =\n      activeStamp?.libraryId === targetLibraryId &&\n      activeStamp.stamp.id === stamp.id\n    if (isActive) {\n      annotation?.setActiveTool(null)\n      return\n    }\n    stampCapability\n      .forDocument(documentId)\n      .activateStampPlacement(targetLibraryId, stamp)\n      .wait(\n        () => undefined,\n        () => notify(\"The stamp could not be activated.\", \"error\")\n      )\n  }\n  const createFromSelection = () => {\n    if (!stampCapability || !canCreateFromSelection) return\n    const count = customLibrary?.stamps.length ?? 0\n    setBusy(true)\n    stampCapability\n      .forDocument(documentId)\n      .createStampFromAnnotation(\n        selected[0].object,\n        {\n          name: PdfAnnotationName.Custom,\n          subject: `Custom stamp ${count + 1}`,\n          categories: [\"custom\", \"sidebar\"],\n        },\n        CUSTOM_STAMP_LIBRARY_ID\n      )\n      .wait(\n        () => {\n          setBusy(false)\n          setLibraryId(CUSTOM_STAMP_LIBRARY_ID)\n          notify(\"Stamp saved to your custom library.\", \"success\")\n        },\n        () => {\n          setBusy(false)\n          notify(\"The stamp could not be created from the selection.\", \"error\")\n        }\n      )\n  }\n  const exportLibrary = (targetLibraryId: string) => {\n    stampCapability?.exportLibrary(targetLibraryId).wait(\n      (exported) => downloadArrayBuffer(exported.pdf, `${exported.name}.pdf`),\n      () => notify(\"The library could not be exported.\", \"error\")\n    )\n  }\n  const importLibrary = async (file: File) => {\n    if (!stampCapability) return\n    setBusy(true)\n    try {\n      const buffer = await file.arrayBuffer()\n      const lib = await import(\"pdf-lib\")\n      const pageCount = (\n        await lib.PDFDocument.load(buffer, { ignoreEncryption: true })\n      ).getPageCount()\n      const name = file.name.replace(/\\.pdf$/i, \"\") || \"Imported stamps\"\n      stampCapability\n        .loadLibrary({\n          name,\n          pdf: buffer,\n          categories: [\"sidebar\"],\n          stamps: Array.from({ length: pageCount }, (_, index) => ({\n            id: `page-${index + 1}`,\n            pageIndex: index,\n            name: PdfAnnotationName.Custom,\n            subject: `${name} ${index + 1}`,\n          })),\n        })\n        .wait(\n          (id) => {\n            setBusy(false)\n            setLibraryId(id)\n            notify(\n              `Imported ${pageCount} stamp${pageCount === 1 ? \"\" : \"s\"}.`,\n              \"success\"\n            )\n          },\n          () => {\n            setBusy(false)\n            notify(\"The stamp library could not be imported.\", \"error\")\n          }\n        )\n    } catch {\n      setBusy(false)\n      notify(\"The file could not be read as a PDF.\", \"error\")\n    }\n  }\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"shrink-0 space-y-2 border-b px-3 py-2\">\n        <div className=\"flex items-center gap-1.5\">\n          <Select\n            items={libraryOptions}\n            value={libraryId}\n            onValueChange={(value) => setLibraryId(String(value))}\n          >\n            <SelectTrigger size=\"sm\" className=\"min-w-0 flex-1\">\n              <SelectValue placeholder=\"Library\" />\n            </SelectTrigger>\n            <SelectContent alignItemWithTrigger={false}>\n              {libraryOptions.map((option) => (\n                <SelectItem key={option.value} value={option.value}>\n                  {option.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n          <PdfEditorToolButton\n            label=\"Import stamps from a PDF\"\n            disabled={busy}\n            onClick={() => importInputRef.current?.click()}\n          >\n            <UploadGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n          <PdfEditorToolButton\n            label=\"Export library\"\n            disabled={\n              busy ||\n              (libraryId === \"all\"\n                ? libraries.length === 0\n                : !libraries.some((library) => library.id === libraryId))\n            }\n            onClick={() => {\n              const targets =\n                libraryId === \"all\"\n                  ? libraries.map((library) => library.id)\n                  : [libraryId]\n              targets.forEach(exportLibrary)\n            }}\n          >\n            <DownloadGlyph className=\"size-4\" />\n          </PdfEditorToolButton>\n        </div>\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          className=\"w-full\"\n          disabled={\n            !canCreateFromSelection || busy || !permissions.canModifyAnnotations\n          }\n          onClick={createFromSelection}\n        >\n          <StampGlyph className=\"size-4\" />\n          Save selection as stamp\n        </Button>\n        <input\n          ref={importInputRef}\n          type=\"file\"\n          accept=\"application/pdf,.pdf\"\n          className=\"sr-only\"\n          tabIndex={-1}\n          onChange={(event) => {\n            const file = event.target.files?.[0]\n            if (file) void importLibrary(file)\n            event.currentTarget.value = \"\"\n          }}\n        />\n      </div>\n      {stamps.length === 0 ? (\n        <PdfEditorEmptyState\n          glyph={StampGlyph}\n          title={libraries.length === 0 ? \"Loading stamps…\" : \"No stamps\"}\n          description=\"Pick a library, import stamps from a PDF, or save a selected annotation as a stamp.\"\n        />\n      ) : (\n        <PdfEditorScrollArea className=\"min-h-0 flex-1\" viewportClassName=\"p-3\">\n          <div className=\"grid grid-cols-2 gap-2.5\">\n            {stamps.map(({ library, stamp }) => {\n              const isActive =\n                activeStamp?.libraryId === library.id &&\n                activeStamp.stamp.id === stamp.id\n              return (\n                <div\n                  key={`${library.id}-${stamp.id}`}\n                  className=\"group relative\"\n                >\n                  <button\n                    type=\"button\"\n                    aria-label={stamp.label ?? stamp.subject ?? \"Rubber stamp\"}\n                    aria-pressed={isActive}\n                    disabled={!permissions.canModifyAnnotations}\n                    className={cn(\n                      \"flex aspect-square w-full items-center justify-center rounded-lg border p-2 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\",\n                      isActive\n                        ? \"border-primary bg-primary/5 ring-1 ring-primary/40\"\n                        : \"hover:bg-accent/50\"\n                    )}\n                    onClick={() => placeStamp(library.id, stamp)}\n                  >\n                    <StampImg\n                      libraryId={library.id}\n                      pageIndex={stamp.pageIndex}\n                      width={STAMP_THUMB_WIDTH}\n                      style={{\n                        maxWidth: \"85%\",\n                        maxHeight: \"85%\",\n                        objectFit: \"contain\",\n                      }}\n                    />\n                  </button>\n                  {!library.readonly ? (\n                    <button\n                      type=\"button\"\n                      aria-label=\"Remove stamp\"\n                      className=\"absolute top-1 right-1 grid size-5 place-items-center rounded-full border bg-background text-muted-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 hover:text-foreground focus-visible:opacity-100\"\n                      onClick={() =>\n                        stampCapability\n                          ?.removeStampFromLibrary(library.id, stamp.id)\n                          .wait(\n                            () => undefined,\n                            () =>\n                              notify(\"The stamp could not be removed.\", \"error\")\n                          )\n                      }\n                    >\n                      <TrashGlyph className=\"size-3\" />\n                    </button>\n                  ) : null}\n                </div>\n              )\n            })}\n          </div>\n        </PdfEditorScrollArea>\n      )}\n      {activeStamp ? (\n        <div className=\"shrink-0 border-t px-3 py-2 text-xs text-muted-foreground\">\n          Click a page to place{\" \"}\n          <span className=\"font-medium text-foreground\">\n            {activeStamp.stamp.subject}\n          </span>\n          .\n        </div>\n      ) : null}\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Signatures                                                                 */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorSignaturesPanel({\n  documentId,\n}: {\n  documentId: string\n}) {\n  const { entries, provides } = useSignatureEntries()\n  const activePlacement = useActivePlacement(documentId)\n  const { openDialog, permissions } = usePdfEditor()\n  const canPlace = permissions.canModifyAnnotations\n  const isActive = (entryId: string, kind: SignatureFieldKind) =>\n    activePlacement?.entryId === entryId && activePlacement.kind === kind\n  const togglePlacement = (entryId: string, kind: SignatureFieldKind) => {\n    if (!provides) return\n    const scope = provides.forDocument(documentId)\n    if (isActive(entryId, kind)) {\n      scope.deactivatePlacement()\n      return\n    }\n    if (kind === SignatureFieldKind.Initials) {\n      scope.activateInitialsPlacement(entryId)\n    } else {\n      scope.activateSignaturePlacement(entryId)\n    }\n  }\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"shrink-0 border-b px-3 py-2\">\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          className=\"w-full\"\n          disabled={!canPlace}\n          onClick={() => openDialog({ type: \"signature\" })}\n        >\n          <PlusGlyph className=\"size-4\" />\n          Create signature\n        </Button>\n      </div>\n      {entries.length === 0 ? (\n        <PdfEditorEmptyState\n          glyph={SignatureGlyph}\n          title=\"No signatures yet\"\n          description=\"Draw, type, or upload a signature, then click it to place it on the page.\"\n        />\n      ) : (\n        <PdfEditorScrollArea className=\"min-h-0 flex-1\" viewportClassName=\"p-3\">\n          <div className=\"space-y-3\">\n            {entries.map((entry) => (\n              <div key={entry.id} className=\"space-y-2 rounded-lg border p-2.5\">\n                <div className=\"flex items-center justify-between\">\n                  <span className=\"text-[11px] font-medium text-muted-foreground\">\n                    Saved {formatRelativeDate(new Date(entry.createdAt))}\n                  </span>\n                  <PdfEditorToolButton\n                    label=\"Delete signature\"\n                    size=\"icon-xs\"\n                    onClick={() => provides?.removeEntry(entry.id)}\n                  >\n                    <TrashGlyph className=\"size-3.5\" />\n                  </PdfEditorToolButton>\n                </div>\n                <div className=\"flex gap-2\">\n                  <button\n                    type=\"button\"\n                    disabled={!canPlace}\n                    aria-pressed={isActive(\n                      entry.id,\n                      SignatureFieldKind.Signature\n                    )}\n                    className={cn(\n                      \"flex h-16 min-w-0 flex-[2] items-center justify-center rounded-md border border-dashed bg-white transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\",\n                      isActive(entry.id, SignatureFieldKind.Signature)\n                        ? \"border-primary ring-1 ring-primary/40\"\n                        : \"hover:border-muted-foreground/60\"\n                    )}\n                    onClick={() =>\n                      togglePlacement(entry.id, SignatureFieldKind.Signature)\n                    }\n                  >\n                    <img\n                      src={entry.signature.previewDataUrl}\n                      alt=\"Signature\"\n                      className=\"h-12 max-w-[90%] object-contain\"\n                    />\n                  </button>\n                  {entry.initials ? (\n                    <button\n                      type=\"button\"\n                      disabled={!canPlace}\n                      aria-pressed={isActive(\n                        entry.id,\n                        SignatureFieldKind.Initials\n                      )}\n                      className={cn(\n                        \"flex h-16 min-w-0 flex-1 items-center justify-center rounded-md border border-dashed bg-white transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\",\n                        isActive(entry.id, SignatureFieldKind.Initials)\n                          ? \"border-primary ring-1 ring-primary/40\"\n                          : \"hover:border-muted-foreground/60\"\n                      )}\n                      onClick={() =>\n                        togglePlacement(entry.id, SignatureFieldKind.Initials)\n                      }\n                    >\n                      <img\n                        src={entry.initials.previewDataUrl}\n                        alt=\"Initials\"\n                        className=\"h-10 max-w-[80%] object-contain\"\n                      />\n                    </button>\n                  ) : null}\n                </div>\n              </div>\n            ))}\n          </div>\n        </PdfEditorScrollArea>\n      )}\n      {activePlacement ? (\n        <div className=\"shrink-0 border-t px-3 py-2 text-xs text-muted-foreground\">\n          Click a page to place your{\" \"}\n          {activePlacement.kind === SignatureFieldKind.Initials\n            ? \"initials\"\n            : \"signature\"}\n          .\n        </div>\n      ) : null}\n    </div>\n  )\n}\n/* -------------------------------------------------------------------------- */\n/* Forms                                                                      */\n/* -------------------------------------------------------------------------- */\nexport function PdfEditorFormsPanel({ documentId }: { documentId: string }) {\n  const { provides: form } = useFormCapability()\n  const { provides: annotation, state: annotationState } =\n    useAnnotation(documentId)\n  const {\n    features,\n    fileName,\n    formDesignMode,\n    notify,\n    permissions,\n    scrollToAnnotation,\n    setFormDesignMode,\n  } = usePdfEditor()\n  const scope = React.useMemo(\n    () => form?.forDocument(documentId) ?? null,\n    [documentId, form]\n  )\n  const fieldStore = React.useMemo(() => createFormFieldStore(scope), [scope])\n  const fields = React.useSyncExternalStore(\n    fieldStore.subscribe,\n    fieldStore.getSnapshot,\n    fieldStore.getSnapshot\n  )\n  const importInputRef = React.useRef<HTMLInputElement>(null)\n  const designModeId = React.useId()\n  const widgetsByName = React.useMemo(() => {\n    const map = new Map<string, PdfWidgetAnnoObject>()\n    for (const tracked of Object.values(annotationState.byUid)) {\n      if (tracked.object.type !== PdfAnnotationSubtype.WIDGET) continue\n      const widget = tracked.object as PdfWidgetAnnoObject\n      if (!map.has(widget.field.name)) map.set(widget.field.name, widget)\n    }\n    return map\n  }, [annotationState.byUid])\n  const focusField = (field: FormFieldInfo) => {\n    const widget = widgetsByName.get(field.name)\n    if (!widget) return\n    if (formDesignMode) {\n      annotation?.selectAnnotation(widget.pageIndex, widget.id)\n    } else {\n      scope?.selectField(widget.id)\n    }\n    scrollToAnnotation(widget)\n  }\n  const exportValues = () => {\n    if (!scope) return\n    const values = scope.getFormValues()\n    downloadBlob(\n      new Blob([JSON.stringify(values, null, 2)], { type: \"application/json\" }),\n      `${fileName.replace(/\\.pdf$/i, \"\")}-form-values.json`\n    )\n  }\n  const importValues = async (file: File) => {\n    if (!scope) return\n    try {\n      const parsed = JSON.parse(await file.text()) as Record<string, unknown>\n      const values = Object.fromEntries(\n        Object.entries(parsed).map(([key, value]) => [key, String(value ?? \"\")])\n      )\n      scope.setFormValues(values).wait(\n        () => notify(\"Form values imported.\", \"success\"),\n        () => notify(\"The form values could not be applied.\", \"error\")\n      )\n    } catch {\n      notify(\"The file is not valid JSON.\", \"error\")\n    }\n  }\n  const clearValues = () => {\n    if (!scope || fields.length === 0) return\n    const values = Object.fromEntries(\n      fields.map((field) => [\n        field.name,\n        field.type === PDF_FORM_FIELD_TYPE.CHECKBOX ||\n        field.type === PDF_FORM_FIELD_TYPE.RADIOBUTTON\n          ? \"Off\"\n          : \"\",\n      ])\n    )\n    scope.setFormValues(values).wait(\n      () => notify(\"Form cleared.\", \"success\"),\n      () => notify(\"The form could not be cleared.\", \"error\")\n    )\n  }\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"shrink-0 space-y-2 border-b px-3 py-2\">\n        {features.forms && permissions.canModifyAnnotations ? (\n          <label\n            htmlFor={designModeId}\n            data-slot=\"label\"\n            className=\"flex cursor-pointer items-center justify-between gap-4 py-1\"\n          >\n            <span className=\"flex min-w-0 flex-col gap-0.5\">\n              <span className=\"text-sm font-medium\">Design mode</span>\n              <span className=\"text-xs text-muted-foreground\">\n                Move, resize, and configure fields instead of filling them.\n              </span>\n            </span>\n            <Switch\n              id={designModeId}\n              checked={formDesignMode}\n              onCheckedChange={setFormDesignMode}\n            />\n          </label>\n        ) : null}\n        <div className=\"flex flex-wrap gap-1.5\">\n          <Button\n            type=\"button\"\n            size=\"xs\"\n            variant=\"outline\"\n            disabled={fields.length === 0}\n            onClick={exportValues}\n          >\n            <DownloadGlyph className=\"size-3.5\" />\n            Export values\n          </Button>\n          <Button\n            type=\"button\"\n            size=\"xs\"\n            variant=\"outline\"\n            disabled={!permissions.canFillForms}\n            onClick={() => importInputRef.current?.click()}\n          >\n            <UploadGlyph className=\"size-3.5\" />\n            Import values\n          </Button>\n          <Button\n            type=\"button\"\n            size=\"xs\"\n            variant=\"outline\"\n            disabled={fields.length === 0 || !permissions.canFillForms}\n            onClick={clearValues}\n          >\n            Clear\n          </Button>\n        </div>\n        <input\n          ref={importInputRef}\n          type=\"file\"\n          accept=\"application/json,.json\"\n          className=\"sr-only\"\n          tabIndex={-1}\n          onChange={(event) => {\n            const file = event.target.files?.[0]\n            if (file) void importValues(file)\n            event.currentTarget.value = \"\"\n          }}\n        />\n      </div>\n      {fields.length === 0 ? (\n        <PdfEditorEmptyState\n          glyph={TextFieldGlyph}\n          title=\"No form fields\"\n          description={\n            features.forms\n              ? \"Switch to the Forms mode to add text fields, checkboxes, radio buttons, dropdowns, and list boxes.\"\n              : \"This document has no fillable fields.\"\n          }\n        />\n      ) : (\n        <PdfEditorScrollArea className=\"min-h-0 flex-1\" viewportClassName=\"p-2\">\n          <div className=\"space-y-1\">\n            {fields.map((field) => (\n              <button\n                key={field.name}\n                type=\"button\"\n                className=\"flex w-full items-center gap-2 rounded-lg border px-2.5 py-2 text-left outline-none hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring\"\n                onClick={() => focusField(field)}\n              >\n                <div className=\"min-w-0 flex-1\">\n                  <div className=\"flex items-center gap-1.5\">\n                    <span className=\"truncate text-sm font-medium\">\n                      {field.name}\n                    </span>\n                    {field.readOnly ? (\n                      <Badge variant=\"outline\" className=\"h-4 px-1 text-[10px]\">\n                        Read only\n                      </Badge>\n                    ) : null}\n                  </div>\n                  <div className=\"truncate text-xs text-muted-foreground\">\n                    {getFormFieldTypeLabel(field.type)}\n                    {field.value ? ` · ${field.value}` : \"\"}\n                  </div>\n                </div>\n              </button>\n            ))}\n          </div>\n        </PdfEditorScrollArea>\n      )}\n    </div>\n  )\n}\nexport { GridGlyph as PdfEditorPagesGlyph }\nfunction InlineLoadingButton({\n  loading,\n  disabled,\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  loading?: boolean\n}) {\n  return (\n    <Button\n      {...props}\n      className={cn(\"relative\", className)}\n      disabled={disabled || loading}\n      aria-busy={loading || undefined}\n    >\n      <span className={cn(\"contents\", loading && \"invisible\")}>{children}</span>\n      {loading ? (\n        <InlineSpinner className=\"pointer-events-none absolute\" />\n      ) : null}\n    </Button>\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/pdf-editor-panels.tsx"
    },
    {
      "path": "lib/pdf-thumbnail-utils.ts",
      "content": "import type { PdfDocumentObject, PdfEngine } from \"@embedpdf/models\"\n\nconst PDFIUM_VERSION = \"2.15.0\"\nconst PDFIUM_WASM_URL = `https://cdn.jsdelivr.net/npm/@embedpdf/pdfium@${PDFIUM_VERSION}/dist/pdfium.wasm`\n\nlet sharedEnginePromise: Promise<PdfEngine> | null = null\nconst pdfDocumentCache = new Map<string, Promise<PdfDocumentObject>>()\nconst thumbnailUrlCache = new Map<string, Promise<string | null>>()\n\nexport function getPdfSourceUrl(src: string) {\n  // Raw base64 must start with the encoded PDF header to distinguish it from a path.\n  if (/^\\s*JVBERi0/.test(src) && !/[^A-Za-z0-9+/=\\s]/.test(src)) {\n    return `data:application/pdf;base64,${src.replace(/\\s/g, \"\")}`\n  }\n\n  return src\n}\n\nexport function loadSharedPdfEngine() {\n  sharedEnginePromise ??= import(\"@embedpdf/engines/pdfium-worker-engine\").then(\n    ({ createPdfiumEngine }) => createPdfiumEngine(PDFIUM_WASM_URL, {})\n  )\n\n  return sharedEnginePromise\n}\n\nexport async function loadPdfDocument(url: string) {\n  let documentPromise = pdfDocumentCache.get(url)\n\n  if (!documentPromise) {\n    documentPromise = loadSharedPdfEngine().then((engine) =>\n      engine\n        .openDocumentUrl(\n          { id: url, url },\n          { mode: url.startsWith(\"blob:\") ? \"full-fetch\" : \"auto\" }\n        )\n        .toPromise()\n    )\n    pdfDocumentCache.set(url, documentPromise)\n  }\n\n  return documentPromise\n}\n\nexport async function getPdfPageCount(url: string) {\n  return (await loadPdfDocument(url)).pageCount\n}\n\nexport function renderPdfThumbnailUrl({\n  dpr = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1,\n  pageIndex,\n  url,\n  width,\n}: {\n  dpr?: number\n  pageIndex: number\n  url: string\n  width: number\n}) {\n  const cacheKey = `${url}#${pageIndex}@${width}x${dpr}`\n  let thumbnailPromise = thumbnailUrlCache.get(cacheKey)\n\n  if (!thumbnailPromise) {\n    thumbnailPromise = (async () => {\n      const [engine, document] = await Promise.all([\n        loadSharedPdfEngine(),\n        loadPdfDocument(url),\n      ])\n      const page = document.pages[pageIndex]\n\n      if (!page) return null\n\n      const blob = await engine\n        .renderThumbnail(document, page, {\n          dpr,\n          imageType: \"image/png\",\n          scaleFactor: width / page.size.width,\n          withAnnotations: true,\n        })\n        .toPromise()\n\n      return URL.createObjectURL(blob)\n    })()\n    thumbnailUrlCache.set(cacheKey, thumbnailPromise)\n  }\n\n  return thumbnailPromise\n}\n",
      "type": "registry:lib",
      "target": "@lib/pdf-thumbnail-utils.ts"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui"
}
