shadimage
extend
uipdf-editor-coresource: ok · hit

PDF Editor Core

Shared PDF editor behavior and state for the Base UI and Radix UI artifacts.

npx shadcn add "https://ui.extend.ai/r/pdf-editor-core.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@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.0pdf-lib@^1.17.1

Registry dependencies

@extend/document-viewer-sidebarresizableutilsbadgebuttondropdown-menuinputpopoverscroll-areaselecttooltip@extend/document-color-picker@extend/document-toastkbddialogswitchtoggle-group

Files7 · source cached 10/8/2026, 2:57:59 PM

components/extend/pdf-editor.tsx
"use client"

import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import { createPluginRegistration } from "@embedpdf/core"
import {
  EmbedPDF,
  PDFContext,
  useDocumentPermissions,
  useRegistry,
} from "@embedpdf/core/react"
import {
  PdfAnnotationSubtype,
  PdfErrorCode,
  PdfPageFlattenFlag,
  uuidV4,
  type PdfAnnotationObject,
  type PdfDocumentObject,
  type PdfEngine,
  type PdfLinkAnnoObject,
  type PdfPrintOptions,
} from "@embedpdf/models"
import {
  AnnotationLayer,
  AnnotationPluginPackage,
  getSelectedAnnotations,
  LockModeType,
  useAnnotation,
  useAnnotationCapability,
  type AnnotationEvent,
  type AnnotationSelectionMenuProps,
  type AnnotationTransferItem,
  type GroupSelectionMenuProps,
} from "@embedpdf/plugin-annotation/react"
import { AttachmentPluginPackage } from "@embedpdf/plugin-attachment/react"
import { BookmarkPluginPackage } from "@embedpdf/plugin-bookmark/react"
import {
  CapturePluginPackage,
  MarqueeCapture,
  useCapture,
  type CaptureAreaEvent,
} from "@embedpdf/plugin-capture/react"
import {
  DocumentManagerPluginPackage,
  useActiveDocument,
  useDocumentManagerCapability,
} from "@embedpdf/plugin-document-manager/react"
import { ExportPluginPackage, useExport } from "@embedpdf/plugin-export/react"
import {
  FormPluginPackage,
  useFormCapability,
} from "@embedpdf/plugin-form/react"
import {
  FullscreenPluginPackage,
  useFullscreen,
} from "@embedpdf/plugin-fullscreen/react"
import {
  HistoryPluginPackage,
  useHistoryCapability,
} from "@embedpdf/plugin-history/react"
import {
  GlobalPointerProvider,
  InteractionManagerPluginPackage,
  PagePointerProvider,
} from "@embedpdf/plugin-interaction-manager/react"
import { PanPluginPackage, usePan } from "@embedpdf/plugin-pan/react"
import { PrintPluginPackage, usePrint } from "@embedpdf/plugin-print/react"
import {
  RedactionLayer,
  RedactionMode,
  RedactionPluginPackage,
  useRedaction,
  type RedactionSelectionMenuProps,
} from "@embedpdf/plugin-redaction/react"
import { RenderLayer, RenderPluginPackage } from "@embedpdf/plugin-render/react"
import {
  Rotate,
  RotatePluginPackage,
  useRotate,
} from "@embedpdf/plugin-rotate/react"
import {
  Scroller,
  ScrollPluginPackage,
  ScrollStrategy,
  useScroll,
  useScrollCapability,
  type PageLayout,
} from "@embedpdf/plugin-scroll/react"
import {
  SearchLayer,
  SearchPluginPackage,
  useSearch,
} from "@embedpdf/plugin-search/react"
import {
  CopyToClipboard,
  SelectionLayer,
  SelectionPluginPackage,
  useSelectionCapability,
  type SelectionSelectionMenuProps,
} from "@embedpdf/plugin-selection/react"
import {
  deserializeEntries,
  serializeEntries,
  SignatureMode,
  SignaturePluginPackage,
  useSignatureCapability,
} from "@embedpdf/plugin-signature/react"
import {
  SpreadMode,
  SpreadPluginPackage,
  useSpread,
} from "@embedpdf/plugin-spread/react"
import { StampPluginPackage } from "@embedpdf/plugin-stamp/react"
import { ThumbnailPluginPackage } from "@embedpdf/plugin-thumbnail/react"
import { TilingLayer, TilingPluginPackage } from "@embedpdf/plugin-tiling/react"
import {
  useIsViewportGated,
  useViewportCapability,
  useViewportElement,
  useViewportRef,
  ViewportElementContext,
  ViewportPluginPackage,
} from "@embedpdf/plugin-viewport/react"
import {
  MarqueeZoom,
  useZoom,
  ZoomMode,
  ZoomPluginPackage,
} from "@embedpdf/plugin-zoom/react"

import { loadSharedPdfEngine } from "@/lib/pdf-thumbnail-utils"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import {
  ScrollArea as InlineScrollArea,
  ScrollBar,
} from "@/components/ui/scroll-area"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { TooltipProvider } from "@/components/ui/tooltip"
import { ColorPicker } from "@/components/extend/document-color-picker"
import {
  useElementWidth,
  useInlineThumbnailSidebar,
} from "@/components/extend/document-viewer-sidebar"
import {
  PdfEditorCaptureDialog,
  PdfEditorConfirmDialog,
  PdfEditorLinkDialog,
  PdfEditorPasswordPrompt,
  PdfEditorPrintDialog,
  PdfEditorPropertiesDialog,
  PdfEditorSecurityDialog,
  PdfEditorShortcutsDialog,
  PdfEditorSignatureDialog,
} from "@/components/extend/pdf-editor-dialogs"
import {
  PdfEditorAttachmentsPanel,
  PdfEditorCommentsPanel,
  PdfEditorFormsPanel,
  PdfEditorOutlinePanel,
  PdfEditorPagesPanel,
  PdfEditorRedactionsPanel,
  PdfEditorSignaturesPanel,
  PdfEditorStampsPanel,
  PdfEditorThumbnailsPanel,
} from "@/components/extend/pdf-editor-panels"
import { PdfEditorPropertiesPanel } from "@/components/extend/pdf-editor-properties"
import {
  BookOpenGlyph,
  CameraGlyph,
  ChevronLeftGlyph,
  ChevronRightGlyph,
  CloseGlyph,
  CommentGlyph,
  CommentsGlyph,
  CopyGlyph,
  CursorGlyph,
  downloadArrayBuffer,
  downloadBlob,
  DownloadGlyph,
  EditGlyph,
  ExitFullscreenGlyph,
  ExternalLinkGlyph,
  EyeOffGlyph,
  FileTextGlyph,
  FullscreenGlyph,
  getPdfFileName,
  GridGlyph,
  GroupGlyph,
  HandGlyph,
  HighlighterGlyph,
  InfoGlyph,
  isTypingTarget,
  KeyboardGlyph,
  LayersGlyph,
  LinkGlyph,
  ListTreeGlyph,
  LockGlyph,
  MarqueeZoomGlyph,
  MoreGlyph,
  noop,
  PaletteGlyph,
  PanelLeftGlyph,
  PanelRightGlyph,
  PaperclipGlyph,
  PDF_EDITOR_ANNOTATION_TOOL_GROUPS,
  PDF_EDITOR_DEFAULT_COLOR_PRESETS,
  PDF_EDITOR_DEFAULT_FEATURES,
  PDF_EDITOR_DEFAULT_SIGNATURE_FONTS_URL,
  PDF_EDITOR_DEFAULT_STAMP_MANIFEST_URL,
  PDF_EDITOR_FORM_TOOLS,
  PDF_EDITOR_MODE_OPTIONS,
  PDF_EDITOR_SELECTION_COLOR,
  PdfEditorContextProvider,
  PdfEditorScrollArea,
  PdfEditorScrollAreaResolverContext,
  PdfEditorToolbarSeparator,
  PdfEditorToolButton,
  PdfEditorTooltip,
  PlusGlyph,
  PrintGlyph,
  RedactAreaGlyph,
  RedoGlyph,
  resolveDefaultScrollAreaViewport,
  RotateGlyph,
  SearchGlyph,
  ShieldGlyph,
  SignatureGlyph,
  SlidersGlyph,
  StampGlyph,
  StrikethroughGlyph,
  TextFieldGlyph,
  TextGlyph,
  toNormalizedRect,
  TrashGlyph,
  UnderlineGlyph,
  UndoGlyph,
  UngroupGlyph,
  UnlockGlyph,
  UploadGlyph,
  usePdfEditor,
  ZoomInGlyph,
  ZoomOutGlyph,
  type PdfEditorContextValue,
  type PdfEditorDialogRequest,
  type PdfEditorFeatureFlags,
  type PdfEditorGlyph,
  type PdfEditorLeftPanel,
  type PdfEditorMode,
  type PdfEditorNoticeTone,
  type PdfEditorResolvedFeatures,
  type PdfEditorRightPanel,
  type PdfEditorScrollAreaViewportResolver,
  type PdfEditorSelectionAction,
  type PdfEditorSelectionPage,
  type PdfEditorSelectionPayload,
} from "@/components/extend/pdf-editor-shared"
import { PdfEditorWorkspace } from "@/components/extend/pdf-editor-workspace"
import { ToastPrimitive, ToastProvider } from "@/components/extend/ui-toast"
import { IconPlaceholder } from "@/components/icon-placeholder"

export type {
  PdfEditorFeatureFlags,
  PdfEditorLeftPanel,
  PdfEditorMode,
  PdfEditorNormalizedRect,
  PdfEditorRightPanel,
  PdfEditorSelectionAction,
  PdfEditorSelectionPayload,
} from "@/components/extend/pdf-editor-shared"
/* -------------------------------------------------------------------------- */
/* Public types                                                               */
/* -------------------------------------------------------------------------- */
export type PDFEditorPageOverlayProps = {
  pageNumber: number
  pageWidth: number
  pageHeight: number
  scale: number
  rotation: number
}
export type PDFEditorZoomLevel = number | "fit-page" | "fit-width" | "automatic"
export type PDFEditorToast = {
  message: string
  tone: PdfEditorNoticeTone
}
export type PDFEditorHandle = {
  /** Current PDF bytes including annotations, form values, and pending security changes. */
  getDocumentBuffer: () => Promise<ArrayBuffer>
  download: (fileName?: string) => Promise<void>
  print: (options?: PdfPrintOptions) => Promise<void>
  exportAnnotations: () => Promise<AnnotationTransferItem[]>
  importAnnotations: (items: AnnotationTransferItem[]) => void
  getFormValues: () => Record<string, string>
  setFormValues: (values: Record<string, string>) => Promise<void>
  /** Permanently applies every pending redaction mark. */
  applyRedactions: () => Promise<void>
  setMode: (mode: PdfEditorMode) => void
  /** Activates an EmbedPDF annotation tool id such as `highlight` or `ink`; `null` returns to selection. */
  setActiveTool: (toolId: string | null) => void
  scrollToPage: (pageNumber: number, options?: ScrollIntoViewOptions) => void
  undo: () => void
  redo: () => void
  getViewportElement: () => HTMLDivElement | null
}
export type PDFEditorProps = {
  className?: string
  /** A URL, or the PDF bytes as an ArrayBuffer, Uint8Array, Blob, or File. */
  src?: string | ArrayBuffer | Uint8Array | Blob
  fileName?: string
  defaultZoom?: PDFEditorZoomLevel
  defaultMode?: PdfEditorMode
  /** Author written into new annotations. Defaults to "Guest". */
  annotationAuthor?: string
  colorPresets?: string[]
  /** Turn individual editing capabilities off. Everything is enabled by default. */
  features?: PdfEditorFeatureFlags
  showToolbar?: boolean
  showUpload?: boolean
  showDownload?: boolean
  toolbarActions?: React.ReactNode
  /** Extra actions for the text selection menu. Each receives normalized page coordinates. */
  selectionActions?: PdfEditorSelectionAction[]
  /** Persist saved signatures in localStorage. Pass a string to use a custom storage key. */
  persistSignatures?: boolean | string
  /** Additional rubber stamp manifests to load next to the built-in library. */
  stampManifests?: string[]
  /** Stylesheet used for the typed-signature fonts. Pass `null` to skip loading web fonts. */
  signatureFontsStylesheetUrl?: string | null
  resolveScrollAreaViewport?: PdfEditorScrollAreaViewportResolver
  renderPageOverlay?: (props: PDFEditorPageOverlayProps) => React.ReactNode
  onDocumentLoadSuccess?: (info: {
    numPages: number
    documentId: string
    fileName: string
  }) => void
  onDocumentLoadError?: (message: string) => void
  onActivePageChange?: (pageNumber: number) => void
  onModeChange?: (mode: PdfEditorMode) => void
  onAnnotationEvent?: (event: AnnotationEvent) => void
  /** Fires with the full annotation set after every committed change (debounced). */
  onAnnotationsChange?: (items: AnnotationTransferItem[]) => void
  onFormValuesChange?: (values: Record<string, string>) => void
  onRedactionsApplied?: () => void
  onSave?: (result: { buffer: ArrayBuffer; fileName: string }) => void
  onCapture?: (event: CaptureAreaEvent) => void
  onPdfUpload?: (file: File) => void
  /** Routes editor notices to a host-owned toast system instead of the built-in viewport. */
  onToast?: (toast: PDFEditorToast) => void
  onSelectionAction?: (
    actionId: string,
    payload: PdfEditorSelectionPayload
  ) => void
}
/* -------------------------------------------------------------------------- */
/* Constants                                                                  */
/* -------------------------------------------------------------------------- */
const PAGE_GAP = 24
const THUMBNAIL_PAGE_WIDTH = 92
const THUMBNAIL_IMAGE_PADDING = 8
const THUMBNAIL_WIDTH = THUMBNAIL_PAGE_WIDTH + THUMBNAIL_IMAGE_PADDING * 2
const THUMBNAIL_LABEL_HEIGHT = 24
const THUMBNAIL_GAP = 12
const THUMBNAIL_PANE_PADDING_Y = 16
const RIGHT_PANEL_INLINE_MIN_WIDTH = 1100
const TOOLBAR_STACK_MAX_WIDTH = 640
const ZOOM_PRESETS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4]
const MIN_ZOOM = 0.1
const MAX_ZOOM = 8
const PAGE_BASE_RENDER_MAX_SCALE = 1
const PAGE_BASE_RENDER_DPR = 1
const SEARCH_DEBOUNCE_MS = 300
const NOTICE_DURATION_MS = 4200
const TOOLTIP_DELAY_MS = 5
const SELECTION_MENU_OFFSET = 8
const SELECTION_MENU_HEIGHT = 36
const DEFAULT_SIGNATURE_STORAGE_KEY = "pdf-editor:signatures"
const DEFAULT_ANNOTATION_AUTHOR = "Guest"
const ZOOM_MODE_LABELS: Record<ZoomMode, string> = {
  [ZoomMode.FitPage]: "Fit page",
  [ZoomMode.FitWidth]: "Fit width",
  [ZoomMode.Automatic]: "Automatic",
}
const SPREAD_MODE_OPTIONS = [
  { label: "Single page", value: SpreadMode.None },
  { label: "Two pages", value: SpreadMode.Odd },
  { label: "Two pages, cover", value: SpreadMode.Even },
]
const SCROLL_DIRECTION_OPTIONS = [
  { label: "Scroll down", value: "vertical" },
  { label: "Scroll sideways", value: "horizontal" },
]
function toZoomLevel(level: PDFEditorZoomLevel): ZoomMode | number {
  if (level === "fit-page") return ZoomMode.FitPage
  if (level === "fit-width") return ZoomMode.FitWidth
  if (level === "automatic") return ZoomMode.Automatic
  return level
}
function isZoomMode(value: unknown): value is ZoomMode {
  return (
    value === ZoomMode.FitPage ||
    value === ZoomMode.FitWidth ||
    value === ZoomMode.Automatic
  )
}
/* -------------------------------------------------------------------------- */
/* Engine + document source                                                   */
/* -------------------------------------------------------------------------- */
function useSharedPdfEngine() {
  const [engine, setEngine] = React.useState<PdfEngine | null>(null)
  const [error, setError] = React.useState<Error | null>(null)
  React.useEffect(() => {
    let cancelled = false
    loadSharedPdfEngine().then(
      (loadedEngine) => {
        if (!cancelled) setEngine(loadedEngine)
      },
      (loadError: Error) => {
        if (!cancelled) setError(loadError)
      }
    )
    return () => {
      cancelled = true
    }
  }, [])
  return { engine, error }
}
type EditorSource =
  | {
      key: string
      kind: "url"
      url: string
      name: string
    }
  | {
      key: string
      kind: "buffer"
      buffer: ArrayBuffer
      name: string
    }
let sourceCounter = 0
function nextSourceKey() {
  sourceCounter += 1
  return `pdf-editor-source-${sourceCounter}`
}
function bytesToArrayBuffer(bytes: Uint8Array) {
  const copy = new ArrayBuffer(bytes.byteLength)
  new Uint8Array(copy).set(bytes)
  return copy
}
function useEditorSource(
  src: PDFEditorProps["src"],
  fileName: string | undefined
) {
  const sourceId = React.useId()
  const [input, setInput] = React.useState({ src, fileName, version: 0 })
  if (input.src !== src || input.fileName !== fileName) {
    setInput({ src, fileName, version: input.version + 1 })
  }
  const immediateSource = React.useMemo<EditorSource | null>(() => {
    const key = `${sourceId}-${input.version}`
    if (typeof input.src === "string") {
      return {
        key,
        kind: "url",
        url: input.src,
        name: getPdfFileName(input.fileName, input.src),
      }
    }
    if (input.src instanceof ArrayBuffer || input.src instanceof Uint8Array) {
      return {
        key,
        kind: "buffer",
        buffer:
          input.src instanceof ArrayBuffer
            ? input.src.slice(0)
            : bytesToArrayBuffer(input.src),
        name: getPdfFileName(input.fileName),
      }
    }
    return null
  }, [input, sourceId])
  const [resolved, setResolved] = React.useState<{
    input: typeof input
    source: EditorSource
  } | null>(null)
  React.useEffect(() => {
    if (!(input.src instanceof Blob)) return
    let cancelled = false
    const blobName =
      input.fileName ?? (input.src instanceof File ? input.src.name : undefined)
    input.src.arrayBuffer().then((buffer) => {
      if (cancelled) return
      setResolved({
        input,
        source: {
          key: nextSourceKey(),
          kind: "buffer",
          buffer,
          name: getPdfFileName(blobName),
        },
      })
    })
    return () => {
      cancelled = true
    }
  }, [input])
  const replace = React.useCallback(
    (buffer: ArrayBuffer, name: string) => {
      setResolved({
        input,
        source: { key: nextSourceKey(), kind: "buffer", buffer, name },
      })
    },
    [input]
  )
  const source = resolved?.input === input ? resolved.source : immediateSource
  return { source, loading: input.src instanceof Blob && !source, replace }
}
/* -------------------------------------------------------------------------- */
/* Annotation JSON serialization                                              */
/* -------------------------------------------------------------------------- */
function arrayBufferToBase64(buffer: ArrayBuffer) {
  const bytes = new Uint8Array(buffer)
  let binary = ""
  for (let index = 0; index < bytes.length; index += 0x8000) {
    binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000))
  }
  return btoa(binary)
}
function base64ToArrayBuffer(value: string) {
  const binary = atob(value)
  const bytes = new Uint8Array(binary.length)
  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index)
  }
  return bytes.buffer
}
type SerializedAnnotationItem = {
  annotation: PdfAnnotationObject
  ctx?: {
    data: string
    mimeType?: string
  }
}
export function serializeAnnotationItems(items: AnnotationTransferItem[]) {
  return {
    format: "extend-ui/pdf-editor-annotations",
    version: 1,
    exportedAt: new Date().toISOString(),
    annotations: items.map((item): SerializedAnnotationItem => {
      const context = item.ctx as
        | {
            data?: ArrayBuffer
            mimeType?: string
          }
        | undefined
      return {
        annotation: item.annotation,
        ...(context?.data instanceof ArrayBuffer
          ? {
              ctx: {
                data: arrayBufferToBase64(context.data),
                mimeType: context.mimeType,
              },
            }
          : {}),
      }
    }),
  }
}
export function deserializeAnnotationItems(
  payload: unknown
): AnnotationTransferItem[] {
  const list = Array.isArray(payload)
    ? payload
    : payload &&
        typeof payload === "object" &&
        Array.isArray(
          (
            payload as {
              annotations?: unknown
            }
          ).annotations
        )
      ? (
          payload as {
            annotations: unknown[]
          }
        ).annotations
      : null
  if (!list) throw new Error("Unrecognized annotation export.")
  return list.map((entry) => {
    const item = entry as SerializedAnnotationItem
    if (!item || typeof item !== "object" || !item.annotation) {
      throw new Error("Unrecognized annotation export.")
    }
    return {
      annotation: item.annotation,
      ...(item.ctx?.data
        ? {
            ctx: {
              data: base64ToArrayBuffer(item.ctx.data),
              mimeType: item.ctx.mimeType,
            } as AnnotationTransferItem["ctx"],
          }
        : {}),
    }
  })
}
/* -------------------------------------------------------------------------- */
/* Viewport                                                                   */
/* -------------------------------------------------------------------------- */
function PdfEditorViewport({
  children,
  className,
  documentId,
}: {
  children: React.ReactNode
  className?: string
  documentId: string
}) {
  const viewportRef = useViewportRef(documentId)
  const { provides: viewport } = useViewportCapability()
  const isGated = useIsViewportGated(documentId)
  const viewportGap = viewport?.getViewportGap() ?? 0
  return (
    <ViewportElementContext.Provider value={viewportRef}>
      <PdfEditorScrollArea
        className={className}
        orientation="both"
        scrollbarGutter={false}
        viewportClassName="relative select-none selection:bg-transparent selection:text-inherit"
        viewportProps={{ style: { padding: viewportGap } }}
        viewportRef={viewportRef}
      >
        {isGated ? null : children}
      </PdfEditorScrollArea>
    </ViewportElementContext.Provider>
  )
}
function PdfEditorViewportBridge({
  viewportElementRef,
}: {
  viewportElementRef: React.MutableRefObject<HTMLDivElement | null>
}) {
  const elementRef = useViewportElement()
  React.useEffect(() => {
    viewportElementRef.current = elementRef?.current ?? null
  })
  return null
}
/* -------------------------------------------------------------------------- */
/* Selection menus rendered inside the page layers                            */
/* -------------------------------------------------------------------------- */
function SelectionMenuShell({
  rect,
  placement,
  menuWrapperProps,
  children,
}: {
  rect: SelectionSelectionMenuProps["rect"]
  placement: SelectionSelectionMenuProps["placement"]
  menuWrapperProps: SelectionSelectionMenuProps["menuWrapperProps"]
  children: React.ReactNode
}) {
  return (
    <div {...menuWrapperProps}>
      <div
        data-slot="pdf-editor-selection-menu"
        className="flex items-center gap-0.5 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg"
        style={{
          position: "absolute",
          left: 0,
          top: placement.suggestTop
            ? -(SELECTION_MENU_HEIGHT + SELECTION_MENU_OFFSET)
            : rect.size.height + SELECTION_MENU_OFFSET,
          pointerEvents: "auto",
          cursor: "default",
          whiteSpace: "nowrap",
        }}
        onPointerDown={(event) => event.stopPropagation()}
      >
        <TooltipProvider delay={TOOLTIP_DELAY_MS}>{children}</TooltipProvider>
      </div>
    </div>
  )
}
function MenuButton({
  label,
  glyph: Glyph,
  onClick,
  disabled,
  destructive,
}: {
  label: string
  glyph: PdfEditorGlyph
  onClick: () => void
  disabled?: boolean
  destructive?: boolean
}) {
  return (
    <PdfEditorToolButton
      label={label}
      size="icon-xs"
      disabled={disabled}
      className={cn(destructive && "text-destructive hover:text-destructive")}
      onClick={(event) => {
        event.stopPropagation()
        onClick()
      }}
    >
      <Glyph className="size-3.5" />
    </PdfEditorToolButton>
  )
}
async function buildSelectionPayload(
  documentId: string,
  document: PdfDocumentObject | null,
  selectionScope: ReturnType<
    NonNullable<
      ReturnType<typeof useSelectionCapability>["provides"]
    >["forDocument"]
  >
): Promise<PdfEditorSelectionPayload | null> {
  const formatted = selectionScope.getFormattedSelection()
  if (formatted.length === 0) return null
  let text = ""
  try {
    const lines = await selectionScope.getSelectedText().toPromise()
    text = lines.join("\n")
  } catch {
    text = ""
  }
  const pages: PdfEditorSelectionPage[] = formatted.map((page) => {
    const pageSize = document?.pages[page.pageIndex]?.size ?? {
      width: 1,
      height: 1,
    }
    return {
      pageIndex: page.pageIndex,
      pageNumber: page.pageIndex + 1,
      pageSize,
      rect: page.rect,
      rects: page.segmentRects,
      normalizedRect: toNormalizedRect(page.rect, pageSize),
      normalizedRects: page.segmentRects.map((rect) =>
        toNormalizedRect(rect, pageSize)
      ),
    }
  })
  return { documentId, text, pages, ...pages[0] }
}
function PdfEditorTextSelectionMenu({
  documentId,
  rect,
  placement,
  menuWrapperProps,
}: SelectionSelectionMenuProps & {
  documentId: string
}) {
  const { provides: selectionCapability } = useSelectionCapability()
  const { provides: annotationCapability } = useAnnotationCapability()
  const { provides: annotation } = useAnnotation(documentId)
  const { provides: redaction } = useRedaction(documentId)
  const editor = usePdfEditor()
  const selectionScope = React.useMemo(
    () => selectionCapability?.forDocument(documentId) ?? null,
    [documentId, selectionCapability]
  )
  const canAnnotate =
    editor.features.annotate && editor.permissions.canModifyAnnotations
  const canRedact =
    editor.features.redact && editor.permissions.canModifyContents
  const createMarkup = (
    toolId: string,
    subtype:
      | PdfAnnotationSubtype.HIGHLIGHT
      | PdfAnnotationSubtype.UNDERLINE
      | PdfAnnotationSubtype.STRIKEOUT,
    options: {
      select?: boolean
      openComments?: boolean
    } = {}
  ) => {
    if (!selectionScope || !annotation) return
    const formatted = selectionScope.getFormattedSelection()
    if (formatted.length === 0) return
    const defaults = (annotationCapability?.getTool(toolId)?.defaults ??
      {}) as Record<string, unknown>
    const finish = (text: string[]) => {
      let firstId: {
        pageIndex: number
        id: string
      } | null = null
      for (const page of formatted) {
        const id = uuidV4()
        annotation.createAnnotation(page.pageIndex, {
          ...defaults,
          id,
          type: subtype,
          pageIndex: page.pageIndex,
          rect: page.rect,
          segmentRects: page.segmentRects,
          author: editor.annotationAuthor,
          created: new Date(),
          modified: new Date(),
          flags: ["print"],
          custom: text.length ? { text: text.join("\n") } : undefined,
        } as PdfAnnotationObject)
        firstId ??= { pageIndex: page.pageIndex, id }
      }
      selectionScope.clear()
      if (options.select && firstId) {
        annotation.selectAnnotation(firstId.pageIndex, firstId.id)
      }
      if (options.openComments) editor.setRightPanel("comments")
    }
    selectionScope.getSelectedText().wait(finish, () => finish([]))
  }
  const runCustomAction = async (action: PdfEditorSelectionAction) => {
    if (!selectionScope) return
    const payload = await buildSelectionPayload(
      documentId,
      editor.document,
      selectionScope
    )
    if (!payload) return
    action.onSelect?.(payload)
    editor.onSelectionAction?.(action.id, payload)
    if (!action.keepSelection) selectionScope.clear()
  }
  return (
    <SelectionMenuShell
      rect={rect}
      placement={placement}
      menuWrapperProps={menuWrapperProps}
    >
      <MenuButton
        label="Copy"
        glyph={CopyGlyph}
        onClick={() => {
          selectionScope?.copyToClipboard()
          selectionScope?.clear()
        }}
      />
      {canAnnotate ? (
        <>
          <PdfEditorToolbarSeparator />
          <MenuButton
            label="Highlight"
            glyph={HighlighterGlyph}
            onClick={() =>
              createMarkup("highlight", PdfAnnotationSubtype.HIGHLIGHT)
            }
          />
          <MenuButton
            label="Underline"
            glyph={UnderlineGlyph}
            onClick={() =>
              createMarkup("underline", PdfAnnotationSubtype.UNDERLINE)
            }
          />
          <MenuButton
            label="Strikethrough"
            glyph={StrikethroughGlyph}
            onClick={() =>
              createMarkup("strikeout", PdfAnnotationSubtype.STRIKEOUT)
            }
          />
          {editor.features.comments ? (
            <MenuButton
              label="Comment"
              glyph={CommentGlyph}
              onClick={() =>
                createMarkup("highlight", PdfAnnotationSubtype.HIGHLIGHT, {
                  select: true,
                  openComments: true,
                })
              }
            />
          ) : null}
          <MenuButton
            label="Add link"
            glyph={LinkGlyph}
            onClick={() =>
              editor.openDialog({ type: "link", source: "selection" })
            }
          />
        </>
      ) : null}
      {canRedact ? (
        <>
          <PdfEditorToolbarSeparator />
          <MenuButton
            label="Mark for redaction"
            glyph={EyeOffGlyph}
            onClick={() => {
              redaction?.queueCurrentSelectionAsPending().wait(
                () => {
                  selectionScope?.clear()
                  editor.setRightPanel("redactions")
                },
                () =>
                  editor.notify("The selection could not be marked.", "error")
              )
            }}
          />
        </>
      ) : null}
      {editor.selectionActions.length > 0 ? (
        <>
          <PdfEditorToolbarSeparator />
          {editor.selectionActions.map((action) => (
            <PdfEditorTooltip key={action.id} label={action.label}>
              <Button
                type="button"
                size="xs"
                variant="ghost"
                onClick={(event) => {
                  event.stopPropagation()
                  void runCustomAction(action)
                }}
              >
                {action.icon}
                {action.label}
              </Button>
            </PdfEditorTooltip>
          ))}
        </>
      ) : null}
    </SelectionMenuShell>
  )
}
const STAMP_SOURCE_TYPES = new Set<PdfAnnotationSubtype>([
  PdfAnnotationSubtype.INK,
  PdfAnnotationSubtype.SQUARE,
  PdfAnnotationSubtype.CIRCLE,
  PdfAnnotationSubtype.LINE,
  PdfAnnotationSubtype.POLYLINE,
  PdfAnnotationSubtype.POLYGON,
  PdfAnnotationSubtype.FREETEXT,
  PdfAnnotationSubtype.STAMP,
  PdfAnnotationSubtype.TEXT,
])
function PdfEditorAnnotationMenu({
  documentId,
  selected,
  context,
  rect,
  placement,
  menuWrapperProps,
}: AnnotationSelectionMenuProps & {
  documentId: string
}) {
  const { provides: annotation } = useAnnotation(documentId)
  const { provides: redaction } = useRedaction(documentId)
  const editor = usePdfEditor()
  if (!selected || !annotation) return null
  const object = context.annotation.object
  const { pageIndex, id } = object
  const isRedact = object.type === PdfAnnotationSubtype.REDACT
  const isWidget = object.type === PdfAnnotationSubtype.WIDGET
  const isLink = object.type === PdfAnnotationSubtype.LINK
  const isLocked = (object.flags ?? []).includes("locked")
  const canModify = editor.permissions.canModifyAnnotations
  const canDelete = canModify && !context.structurallyLocked
  const remove = () => annotation.deleteAnnotation(pageIndex, id)
  if (isRedact) {
    return (
      <SelectionMenuShell
        rect={rect}
        placement={placement}
        menuWrapperProps={menuWrapperProps}
      >
        <MenuButton
          label="Apply redaction"
          glyph={EyeOffGlyph}
          disabled={!editor.permissions.canModifyContents}
          onClick={() =>
            editor.openDialog({
              type: "confirm",
              title: "Apply this redaction?",
              description: "The marked content is removed permanently.",
              confirmLabel: "Apply",
              onConfirm: () => {
                redaction
                  ?.commitPending(pageIndex, id)
                  .wait(noop, () =>
                    editor.notify(
                      "The redaction could not be applied.",
                      "error"
                    )
                  )
              },
            })
          }
        />
        <MenuButton
          label="Style"
          glyph={SlidersGlyph}
          onClick={() => editor.setRightPanel("properties")}
        />
        <MenuButton
          label="Remove mark"
          glyph={TrashGlyph}
          destructive
          onClick={remove}
        />
      </SelectionMenuShell>
    )
  }
  if (isLink) {
    const target = (object as PdfLinkAnnoObject).target
    return (
      <SelectionMenuShell
        rect={rect}
        placement={placement}
        menuWrapperProps={menuWrapperProps}
      >
        <MenuButton
          label="Open link"
          glyph={ExternalLinkGlyph}
          disabled={!target}
          onClick={() => {
            if (!target) return
            annotation.navigateTarget(target).wait((result) => {
              if (result.outcome === "uri") {
                window.open(result.uri, "_blank", "noopener")
              }
            }, noop)
          }}
        />
        <MenuButton
          label="Edit link"
          glyph={EditGlyph}
          disabled={!canModify}
          onClick={() =>
            editor.openDialog({ type: "link", source: "annotation" })
          }
        />
        <MenuButton
          label="Delete"
          glyph={TrashGlyph}
          destructive
          disabled={!canDelete}
          onClick={remove}
        />
      </SelectionMenuShell>
    )
  }
  return (
    <SelectionMenuShell
      rect={rect}
      placement={placement}
      menuWrapperProps={menuWrapperProps}
    >
      {!isWidget && editor.features.comments ? (
        <MenuButton
          label="Comment"
          glyph={CommentGlyph}
          onClick={() => editor.setRightPanel("comments")}
        />
      ) : null}
      <MenuButton
        label="Properties"
        glyph={SlidersGlyph}
        onClick={() => editor.setRightPanel("properties")}
      />
      {!isWidget && canModify ? (
        <MenuButton
          label="Add link"
          glyph={LinkGlyph}
          onClick={() =>
            editor.openDialog({ type: "link", source: "annotation" })
          }
        />
      ) : null}
      {!isWidget &&
      editor.features.stamps &&
      canModify &&
      STAMP_SOURCE_TYPES.has(object.type) ? (
        <MenuButton
          label="Save as stamp"
          glyph={StampGlyph}
          onClick={() => editor.setRightPanel("stamps")}
        />
      ) : null}
      {!isWidget && canModify ? (
        <MenuButton
          label={isLocked ? "Unlock" : "Lock"}
          glyph={isLocked ? UnlockGlyph : LockGlyph}
          onClick={() => {
            const flags = object.flags ?? []
            annotation.updateAnnotation(pageIndex, id, {
              flags: isLocked
                ? flags.filter((flag) => flag !== "locked")
                : [...flags, "locked"],
            })
          }}
        />
      ) : null}
      <MenuButton
        label="Delete"
        glyph={TrashGlyph}
        destructive
        disabled={!canDelete}
        onClick={remove}
      />
    </SelectionMenuShell>
  )
}
function PdfEditorGroupMenu({
  documentId,
  selected,
  context,
  rect,
  placement,
  menuWrapperProps,
}: GroupSelectionMenuProps & {
  documentId: string
}) {
  const { provides: annotation } = useAnnotation(documentId)
  const editor = usePdfEditor()
  if (!selected || !annotation) return null
  const groupingAction = annotation.getGroupingAction()
  const canModify = editor.permissions.canModifyAnnotations
  return (
    <SelectionMenuShell
      rect={rect}
      placement={placement}
      menuWrapperProps={menuWrapperProps}
    >
      <span className="px-1.5 text-xs text-muted-foreground">
        {context.annotations.length} selected
      </span>
      <MenuButton
        label={groupingAction === "ungroup" ? "Ungroup" : "Group"}
        glyph={groupingAction === "ungroup" ? UngroupGlyph : GroupGlyph}
        disabled={!canModify || groupingAction === "disabled"}
        onClick={() => {
          if (groupingAction === "ungroup") {
            annotation.ungroupAnnotations(context.annotations[0].object.id)
          } else {
            annotation.groupAnnotations()
          }
        }}
      />
      <MenuButton
        label="Properties"
        glyph={SlidersGlyph}
        onClick={() => editor.setRightPanel("properties")}
      />
      <MenuButton
        label="Delete all"
        glyph={TrashGlyph}
        destructive
        disabled={!canModify}
        onClick={() =>
          annotation.deleteAnnotations(
            context.annotations.map((item) => ({
              pageIndex: item.object.pageIndex,
              id: item.object.id,
            }))
          )
        }
      />
    </SelectionMenuShell>
  )
}
function PdfEditorRedactionMenu({
  documentId,
  selected,
  context,
  rect,
  placement,
  menuWrapperProps,
}: RedactionSelectionMenuProps & {
  documentId: string
}) {
  const { provides: redaction } = useRedaction(documentId)
  const editor = usePdfEditor()
  if (!selected || !redaction) return null
  const { page, id } = context.item
  return (
    <SelectionMenuShell
      rect={rect}
      placement={placement}
      menuWrapperProps={menuWrapperProps}
    >
      <MenuButton
        label="Apply redaction"
        glyph={EyeOffGlyph}
        disabled={!editor.permissions.canModifyContents}
        onClick={() =>
          editor.openDialog({
            type: "confirm",
            title: "Apply this redaction?",
            description: "The marked content is removed permanently.",
            confirmLabel: "Apply",
            onConfirm: () => {
              redaction
                .commitPending(page, id)
                .wait(noop, () =>
                  editor.notify("The redaction could not be applied.", "error")
                )
            },
          })
        }
      />
      <MenuButton
        label="Remove mark"
        glyph={TrashGlyph}
        destructive
        onClick={() => redaction.removePending(page, id)}
      />
    </SelectionMenuShell>
  )
}
/* -------------------------------------------------------------------------- */
/* Toolbar controls                                                           */
/* -------------------------------------------------------------------------- */
function PdfEditorPageControl({
  activePage,
  numPages,
  disabled,
  onPageChange,
}: {
  activePage: number
  numPages: number
  disabled: boolean
  onPageChange: (pageNumber: number) => void
}) {
  const inputRef = React.useRef<HTMLInputElement>(null)
  const displayPage = numPages ? activePage : 1
  const [isEditing, setIsEditing] = React.useState(false)
  const [draftPage, setDraftPage] = React.useState(() => String(displayPage))
  React.useEffect(() => {
    if (!isEditing) return
    inputRef.current?.focus()
    inputRef.current?.select()
  }, [isEditing])
  const applyPageDraft = React.useCallback(
    (value: string) => {
      const trimmedValue = value.trim()
      if (!trimmedValue) return
      const parsedPage = Number(trimmedValue)
      if (!Number.isInteger(parsedPage)) return
      onPageChange(Math.min(Math.max(parsedPage, 1), Math.max(numPages, 1)))
    },
    [numPages, onPageChange]
  )
  return (
    <div className="flex items-center text-sm whitespace-nowrap text-primary">
      <span>Page</span>
      {isEditing ? (
        <Input
          ref={inputRef}
          aria-label="Page number"
          inputMode="numeric"
          pattern="[0-9]*"
          value={draftPage}
          onBlur={() => setIsEditing(false)}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            const nextValue = event.target.value
            setDraftPage(nextValue)
            applyPageDraft(nextValue)
          }}
          onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
            if (event.key === "Enter" || event.key === "Escape") {
              event.currentTarget.blur()
            }
          }}
          className={cn(
            "h-8 px-2.5",
            "mx-1 w-14 min-w-14 rounded-md [&_[data-slot=input]]:text-center"
          )}
        />
      ) : (
        <Button
          type="button"
          variant="ghost"
          size="sm"
          className="font-normal"
          aria-label={`Current page ${displayPage}. Edit page number`}
          disabled={disabled || !numPages}
          onClick={() => {
            setDraftPage(String(displayPage))
            setIsEditing(true)
          }}
        >
          {displayPage}
        </Button>
      )}
      <span>of {numPages || "–"}</span>
    </div>
  )
}
function PdfEditorZoomControl({
  documentId,
  disabled,
}: {
  documentId: string
  disabled: boolean
}) {
  const { state, provides: zoom } = useZoom(documentId)
  const current = state.currentZoomLevel
  const selectValue = isZoomMode(state.zoomLevel)
    ? state.zoomLevel
    : String(Number(current.toFixed(2)))
  const presets = ZOOM_PRESETS.includes(Number(current.toFixed(2)))
    ? ZOOM_PRESETS
    : [...ZOOM_PRESETS, Number(current.toFixed(2))].sort((a, b) => a - b)
  return (
    <div className="flex items-center gap-0.5">
      <PdfEditorToolButton
        label="Zoom out"
        shortcut="mod+-"
        disabled={disabled || current <= MIN_ZOOM}
        onClick={() => zoom?.zoomOut()}
      >
        <ZoomOutGlyph className="size-4" />
      </PdfEditorToolButton>
      <Select
        value={selectValue}
        disabled={disabled}
        modal={false}
        onValueChange={(value) => {
          const next = String(value)
          zoom?.requestZoom(isZoomMode(next) ? next : Number(next))
        }}
      >
        <SelectTrigger
          size="sm"
          className="w-[104px] min-w-[104px]"
          aria-label="Zoom level"
        >
          <SelectValue placeholder="Zoom">
            {Math.round(current * 100)}%
          </SelectValue>
        </SelectTrigger>
        <SelectContent alignItemWithTrigger={false}>
          {(Object.keys(ZOOM_MODE_LABELS) as ZoomMode[]).map((mode) => (
            <SelectItem key={mode} value={mode}>
              {ZOOM_MODE_LABELS[mode]}
            </SelectItem>
          ))}
          {presets.map((preset) => (
            <SelectItem key={preset} value={String(preset)}>
              {Math.round(preset * 100)}%
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
      <PdfEditorToolButton
        label="Zoom in"
        shortcut="mod+="
        disabled={disabled || current >= MAX_ZOOM}
        onClick={() => zoom?.zoomIn()}
      >
        <ZoomInGlyph className="size-4" />
      </PdfEditorToolButton>
    </div>
  )
}
function PdfEditorSearchControl({
  documentId,
  disabled,
  open,
  onOpenChange,
}: {
  documentId: string
  disabled: boolean
  open: boolean
  onOpenChange: (open: boolean) => void
}) {
  const { state, provides } = useSearch(documentId)
  const { provides: scroll } = useScroll(documentId)
  const [draft, setDraft] = React.useState("")
  const [query, setQuery] = React.useState("")
  const [isSearching, setIsSearching] = React.useState(false)
  const providesRef = React.useRef(provides)
  const scrollRef = React.useRef(scroll)
  const requestIdRef = React.useRef(0)
  const inputRef = React.useRef<HTMLInputElement>(null)
  React.useEffect(() => {
    providesRef.current = provides
    scrollRef.current = scroll
  }, [provides, scroll])
  React.useEffect(() => {
    if (!open) return
    const frame = window.requestAnimationFrame(() => inputRef.current?.focus())
    return () => window.cancelAnimationFrame(frame)
  }, [open])
  const scrollToResult = React.useCallback(
    (index: number) => {
      const result = state.results[index]
      if (!result || !scroll) return
      const firstRect = result.rects[0]
      scroll.scrollToPage({
        pageNumber: result.pageIndex + 1,
        ...(firstRect
          ? {
              pageCoordinates: { x: firstRect.origin.x, y: firstRect.origin.y },
              alignY: 30,
            }
          : {}),
        behavior: "auto",
      })
    },
    [scroll, state.results]
  )
  const runSearch = React.useCallback((rawQuery: string) => {
    const nextQuery = rawQuery.trim()
    const requestId = requestIdRef.current + 1
    requestIdRef.current = requestId
    setQuery(nextQuery)
    const searchProvider = providesRef.current
    const scrollProvider = scrollRef.current
    if (!searchProvider) {
      setIsSearching(false)
      return
    }
    if (!nextQuery) {
      searchProvider.stopSearch()
      setIsSearching(false)
      return
    }
    setIsSearching(true)
    searchProvider.startSearch()
    searchProvider.searchAllPages(nextQuery).wait(
      (result) => {
        if (requestIdRef.current !== requestId) return
        const first = result.results[0]
        if (first && scrollProvider) {
          searchProvider.goToResult(0)
          const firstRect = first.rects[0]
          scrollProvider.scrollToPage({
            pageNumber: first.pageIndex + 1,
            ...(firstRect
              ? {
                  pageCoordinates: {
                    x: firstRect.origin.x,
                    y: firstRect.origin.y,
                  },
                  alignY: 30,
                }
              : {}),
            behavior: "auto",
          })
        }
        setIsSearching(false)
      },
      () => {
        if (requestIdRef.current === requestId) setIsSearching(false)
      }
    )
  }, [])
  React.useEffect(() => {
    if (!draft.trim()) return
    const timeout = window.setTimeout(
      () => runSearch(draft),
      SEARCH_DEBOUNCE_MS
    )
    return () => window.clearTimeout(timeout)
  }, [draft, runSearch])
  const clear = () => {
    requestIdRef.current += 1
    setDraft("")
    setQuery("")
    setIsSearching(false)
    provides?.stopSearch()
  }
  const navigate = (direction: 1 | -1) => {
    if (!provides || state.total === 0) return
    scrollToResult(
      direction === 1 ? provides.nextResult() : provides.previousResult()
    )
  }
  const resultLabel = isSearching
    ? "Searching…"
    : !query
      ? "Type to search"
      : state.total
        ? `${state.activeResultIndex + 1} / ${state.total}`
        : "No results"
  return (
    <Popover open={open} onOpenChange={onOpenChange}>
      <PdfEditorTooltip label="Search" shortcut="mod+F">
        <PopoverTrigger
          render={
            <Button
              type="button"
              variant="ghost"
              size="icon-sm"
              aria-label="Search"
              disabled={disabled}
              className={cn(query && "text-primary")}
            >
              <SearchGlyph className="size-4" />
            </Button>
          }
        ></PopoverTrigger>
      </PdfEditorTooltip>
      <PopoverContent
        align="end"
        className="w-72 p-0 [&>[data-slot=popover-viewport]]:p-3.5!"
      >
        <div className="space-y-3">
          <Input
            ref={inputRef}
            placeholder="Search text"
            value={draft}
            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
              const next = event.target.value
              setDraft(next)
              if (next.trim()) {
                setIsSearching(true)
              } else {
                clear()
              }
            }}
            onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
              if (event.key !== "Enter") return
              event.preventDefault()
              if (state.total) {
                navigate(event.shiftKey ? -1 : 1)
              } else if (draft.trim()) {
                runSearch(draft)
              }
            }}
          />
          <div className="flex items-center justify-between gap-2">
            <div className="min-w-0 truncate text-xs text-muted-foreground">
              {resultLabel}
            </div>
            <div className="flex shrink-0 items-center gap-1">
              <Button
                type="button"
                variant="outline"
                size="icon-sm"
                aria-label="Previous result"
                disabled={isSearching || state.total === 0}
                onClick={() => navigate(-1)}
              >
                <ChevronLeftGlyph className="size-4" />
              </Button>
              <Button
                type="button"
                variant="outline"
                size="icon-sm"
                aria-label="Next result"
                disabled={isSearching || state.total === 0}
                onClick={() => navigate(1)}
              >
                <ChevronRightGlyph className="size-4" />
              </Button>
              <Button type="button" variant="outline" size="sm" onClick={clear}>
                Clear
              </Button>
            </div>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}
function PdfEditorModeSwitch({
  mode,
  features,
  compact = false,
  onModeChange,
}: {
  mode: PdfEditorMode
  features: PdfEditorResolvedFeatures
  compact?: boolean
  onModeChange: (mode: PdfEditorMode) => void
}) {
  const options = PDF_EDITOR_MODE_OPTIONS.filter(
    (option) => option.feature === null || features[option.feature]
  )
  return (
    <div
      role="group"
      aria-label="Editor mode"
      className="flex shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5"
    >
      {options.map((option) => {
        const Glyph = option.glyph
        const active = option.id === mode
        return (
          <Button
            key={option.id}
            type="button"
            size={compact ? "icon-xs" : "xs"}
            variant={active ? "default" : "ghost"}
            aria-pressed={active}
            aria-label={option.label}
            className={cn("gap-1.5", !active && "text-muted-foreground")}
            onClick={() => onModeChange(option.id)}
          >
            <Glyph className="size-3.5" />
            {compact ? null : (
              <span className="hidden md:inline">{option.label}</span>
            )}
          </Button>
        )
      })}
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Side panel shells                                                          */
/* -------------------------------------------------------------------------- */
function PanelTabStrip<T extends string>({
  tabs,
  value,
  onChange,
  onClose,
  label,
  disabled = false,
}: {
  tabs: Array<{
    id: T
    label: string
    glyph: PdfEditorGlyph
  }>
  value: T | null
  onChange: (value: T) => void
  onClose: () => void
  label: string
  disabled?: boolean
}) {
  return (
    <div className="flex shrink-0 items-center justify-between gap-1 border-b px-1.5 py-1">
      <TooltipProvider delay={TOOLTIP_DELAY_MS}>
        <div
          className="flex min-w-0 items-center gap-0.5 overflow-x-auto overflow-y-hidden"
          role="tablist"
          aria-label={label}
        >
          {tabs.map((tab) => {
            const Glyph = tab.glyph
            return (
              <PdfEditorToolButton
                key={tab.id}
                label={tab.label}
                active={value === tab.id}
                role="tab"
                aria-selected={value === tab.id}
                disabled={disabled}
                onClick={() => onChange(tab.id)}
              >
                <Glyph className="size-4" />
              </PdfEditorToolButton>
            )
          })}
        </div>
        <PdfEditorToolButton
          label="Close panel"
          size="icon-xs"
          disabled={disabled}
          onClick={onClose}
        >
          <CloseGlyph className="size-3.5" />
        </PdfEditorToolButton>
      </TooltipProvider>
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Signature persistence + engine warm-up utilities                           */
/* -------------------------------------------------------------------------- */
function PdfEditorSignaturePersistence({
  storageKey,
}: {
  storageKey: string | null
}) {
  const { provides: signatureCapability } = useSignatureCapability()
  React.useEffect(() => {
    if (!signatureCapability || !storageKey) return
    try {
      const raw = window.localStorage.getItem(storageKey)
      if (raw && signatureCapability.getEntries().length === 0) {
        signatureCapability.loadEntries(deserializeEntries(JSON.parse(raw)))
      }
    } catch {
      // Ignore corrupt or unavailable storage.
    }
    return signatureCapability.onEntriesChange((entries) => {
      try {
        window.localStorage.setItem(
          storageKey,
          JSON.stringify(serializeEntries(entries))
        )
      } catch {
        // Storage may be unavailable (private mode, quota).
      }
    })
  }, [signatureCapability, storageKey])
  return null
}
/* -------------------------------------------------------------------------- */
/* Fallback shell (engine loading, document loading, errors, empty)           */
/* -------------------------------------------------------------------------- */
function PdfEditorDocumentStatus({
  state,
  errorMessage,
  onUploadFile,
}: {
  state: "loading" | "error" | "empty"
  errorMessage?: string
  onUploadFile?: (file: File) => void
}) {
  const inputRef = React.useRef<HTMLInputElement>(null)
  return (
    <div
      data-slot="pdf-editor-document-status"
      data-state={state}
      className="grid min-h-0 flex-1 place-items-center p-6 text-center text-sm text-muted-foreground"
    >
      {state === "loading" ? (
        <InlineSpinner className="size-4" />
      ) : (
        <div className="flex max-w-sm flex-col items-center gap-3">
          <div role={state === "error" ? "alert" : undefined}>
            {state === "error"
              ? (errorMessage ?? "The PDF could not be loaded.")
              : "Open a PDF to start editing"}
          </div>
          {onUploadFile ? (
            <>
              <input
                ref={inputRef}
                type="file"
                accept="application/pdf,.pdf"
                className="sr-only"
                tabIndex={-1}
                onChange={(event) => {
                  const file = event.target.files?.[0]
                  if (file) onUploadFile(file)
                  event.currentTarget.value = ""
                }}
              />
              <Button
                type="button"
                size="sm"
                variant="outline"
                onClick={() => inputRef.current?.click()}
              >
                <UploadGlyph className="size-4" />
                Open PDF
              </Button>
            </>
          ) : null}
        </div>
      )}
    </div>
  )
}
const EMPTY_EDITOR_CONTEXT: React.ContextType<typeof PDFContext> = {
  registry: null,
  coreState: null,
  isInitializing: true,
  pluginsReady: false,
  activeDocumentId: null,
  activeDocument: null,
  documents: {},
  documentStates: [],
}
/* -------------------------------------------------------------------------- */
/* Editor (per loaded document)                                               */
/* -------------------------------------------------------------------------- */
type EditorHandleImpl = PDFEditorHandle
type PdfEditorInnerProps = {
  documentId: string
  document: PdfDocumentObject | null
  documentState: "loading" | "ready" | "empty" | "error" | "password"
  unavailableContent?: React.ReactNode
  fileName: string
  className?: string
  defaultZoom: PDFEditorZoomLevel
  defaultMode: PdfEditorMode
  leftPanel: PdfEditorLeftPanel | null
  setLeftPanel: React.Dispatch<React.SetStateAction<PdfEditorLeftPanel | null>>
  formDesignMode: boolean
  setFormDesignMode: React.Dispatch<React.SetStateAction<boolean>>
  annotationAuthor: string
  features: PdfEditorResolvedFeatures
  showToolbar: boolean
  showUpload: boolean
  showDownload: boolean
  toolbarActions?: React.ReactNode
  selectionActions: PdfEditorSelectionAction[]
  signatureFontsStylesheetUrl: string | null
  renderPageOverlay?: (props: PDFEditorPageOverlayProps) => React.ReactNode
  handleRef: React.MutableRefObject<EditorHandleImpl | null>
  onReplaceDocument: (buffer: ArrayBuffer, fileName: string) => void
  onUploadFile: (file: File) => void
  onActivePageChange?: (pageNumber: number) => void
  onModeChange?: (mode: PdfEditorMode) => void
  onAnnotationEvent?: (event: AnnotationEvent) => void
  onAnnotationsChange?: (items: AnnotationTransferItem[]) => void
  onFormValuesChange?: (values: Record<string, string>) => void
  onRedactionsApplied?: () => void
  onSave?: (result: { buffer: ArrayBuffer; fileName: string }) => void
  onCapture?: (event: CaptureAreaEvent) => void
  onToast?: (toast: PDFEditorToast) => void
  onSelectionAction?: (
    actionId: string,
    payload: PdfEditorSelectionPayload
  ) => void
}
const LEFT_PANEL_TABS: Array<{
  id: PdfEditorLeftPanel
  label: string
  glyph: PdfEditorGlyph
  feature: keyof PdfEditorFeatureFlags | null
}> = [
  { id: "thumbnails", label: "Thumbnails", glyph: GridGlyph, feature: null },
  { id: "outline", label: "Outline", glyph: ListTreeGlyph, feature: "outline" },
  {
    id: "attachments",
    label: "Attachments",
    glyph: PaperclipGlyph,
    feature: "attachments",
  },
  {
    id: "pages",
    label: "Organize pages",
    glyph: LayersGlyph,
    feature: "pages",
  },
]
const RIGHT_PANEL_TABS: Array<{
  id: PdfEditorRightPanel
  label: string
  glyph: PdfEditorGlyph
  feature: keyof PdfEditorFeatureFlags | null
}> = [
  { id: "properties", label: "Properties", glyph: SlidersGlyph, feature: null },
  {
    id: "comments",
    label: "Comments",
    glyph: CommentsGlyph,
    feature: "comments",
  },
  {
    id: "redactions",
    label: "Redactions",
    glyph: EyeOffGlyph,
    feature: "redact",
  },
  { id: "stamps", label: "Stamps", glyph: StampGlyph, feature: "stamps" },
  {
    id: "signatures",
    label: "Signatures",
    glyph: SignatureGlyph,
    feature: "sign",
  },
  {
    id: "forms",
    label: "Form fields",
    glyph: TextFieldGlyph,
    feature: "forms",
  },
]
function PdfEditorInner({
  documentId,
  document: pdfDocument,
  documentState,
  unavailableContent,
  fileName,
  className,
  defaultZoom,
  defaultMode,
  leftPanel,
  setLeftPanel,
  formDesignMode,
  setFormDesignMode,
  annotationAuthor,
  features,
  showToolbar,
  showUpload,
  showDownload,
  toolbarActions,
  selectionActions,
  signatureFontsStylesheetUrl,
  renderPageOverlay,
  handleRef,
  onReplaceDocument,
  onUploadFile,
  onActivePageChange,
  onModeChange,
  onAnnotationEvent,
  onAnnotationsChange,
  onFormValuesChange,
  onRedactionsApplied,
  onSave,
  onCapture,
  onToast,
  onSelectionAction,
}: PdfEditorInnerProps) {
  const { registry } = useRegistry()
  const permissions = useDocumentPermissions(documentId)
  const { state: scrollState, provides: scroll } = useScroll(documentId)
  const { provides: scrollCapability } = useScrollCapability()
  const { state: zoomState, provides: zoom } = useZoom(documentId)
  const { provides: rotate } = useRotate(documentId)
  const { spreadMode, provides: spread } = useSpread(documentId)
  const { provides: pan, isPanning } = usePan(documentId)
  const { state: captureState, provides: capture } = useCapture(documentId)
  const { provides: fullscreen, state: fullscreenState } = useFullscreen()
  const { provides: historyCapability } = useHistoryCapability()
  const { provides: annotationCapability } = useAnnotationCapability()
  const { state: annotationState, provides: annotation } =
    useAnnotation(documentId)
  const { state: redactionState, provides: redaction } =
    useRedaction(documentId)
  const { provides: exportScope } = useExport(documentId)
  const { provides: print } = usePrint(documentId)
  const { provides: formCapability } = useFormCapability()
  const { provides: selectionCapability } = useSelectionCapability()
  const [mode, setModeState] = React.useState<PdfEditorMode>(defaultMode)
  const [rightPanel, setRightPanel] =
    React.useState<PdfEditorRightPanel | null>(null)
  const [dialog, setDialog] = React.useState<PdfEditorDialogRequest | null>(
    null
  )
  const [searchOpen, setSearchOpen] = React.useState(false)
  const [historyState, setHistoryState] = React.useState({
    canUndo: false,
    canRedo: false,
  })
  const [horizontalScroll, setHorizontalScroll] = React.useState(false)
  const [isDownloading, setIsDownloading] = React.useState(false)
  const rootRef = React.useRef<HTMLDivElement | null>(null)
  const [toastContainer, setToastContainer] =
    React.useState<HTMLDivElement | null>(null)
  const [editorToastManager] = React.useState(() =>
    ToastPrimitive.createToastManager()
  )
  const viewportElementRef = React.useRef<HTMLDivElement | null>(null)
  const uploadInputRef = React.useRef<HTMLInputElement>(null)
  const annotationImportInputRef = React.useRef<HTMLInputElement>(null)
  const [shellRef, shellWidth] = useElementWidth<HTMLDivElement>()
  const leftInline = useInlineThumbnailSidebar(shellWidth)
  const rightInline = shellWidth >= RIGHT_PANEL_INLINE_MIN_WIDTH
  const compactToolbar = shellWidth > 0 && shellWidth <= TOOLBAR_STACK_MAX_WIDTH
  const activePage = scrollState.currentPage
  const numPages = pdfDocument?.pageCount ?? 0
  const controlsDisabled = !numPages
  const currentZoom = zoomState.currentZoomLevel
  const selectedAnnotations = React.useMemo(
    () => getSelectedAnnotations(annotationState),
    [annotationState]
  )
  const activeToolId = annotationState.activeToolId
  const activeTool = React.useMemo(
    () =>
      activeToolId && annotationCapability
        ? annotationCapability.getTool(activeToolId)
        : null,
    [activeToolId, annotationCapability]
  )
  const historyScope = React.useMemo(
    () => historyCapability?.forDocument(documentId) ?? null,
    [documentId, historyCapability]
  )
  const formScope = React.useMemo(
    () => formCapability?.forDocument(documentId) ?? null,
    [documentId, formCapability]
  )
  const selectionScope = React.useMemo(
    () => selectionCapability?.forDocument(documentId) ?? null,
    [documentId, selectionCapability]
  )
  /* ---- notices ---------------------------------------------------------- */
  const notify = React.useCallback(
    (message: string, tone: PdfEditorNoticeTone = "info") => {
      if (onToast) {
        onToast({ message, tone })
        return
      }
      editorToastManager.add({ title: message, type: tone })
    },
    [editorToastManager, onToast]
  )
  const setRootElement = React.useCallback((element: HTMLDivElement | null) => {
    rootRef.current = element
    setToastContainer(element)
  }, [])
  /* ---- tool helpers ----------------------------------------------------- */
  const deactivateTools = React.useCallback(() => {
    annotation?.setActiveTool(null)
    if (redactionState.isRedacting) redaction?.endRedact()
    if (isPanning) pan?.disablePan()
    if (captureState.isMarqueeCaptureActive) capture?.disableMarqueeCapture()
    if (zoomState.isMarqueeZoomActive) zoom?.disableMarqueeZoom()
  }, [
    annotation,
    capture,
    captureState.isMarqueeCaptureActive,
    isPanning,
    pan,
    redaction,
    redactionState.isRedacting,
    zoom,
    zoomState.isMarqueeZoomActive,
  ])
  const setActiveTool = React.useCallback(
    (toolId: string | null) => {
      if (!annotation) return
      if (isPanning) pan?.disablePan()
      if (captureState.isMarqueeCaptureActive) capture?.disableMarqueeCapture()
      if (zoomState.isMarqueeZoomActive) zoom?.disableMarqueeZoom()
      if (redactionState.isRedacting) redaction?.endRedact()
      annotation.setActiveTool(
        toolId !== null && annotationState.activeToolId === toolId
          ? null
          : toolId
      )
    },
    [
      annotation,
      annotationState.activeToolId,
      capture,
      captureState.isMarqueeCaptureActive,
      isPanning,
      pan,
      redaction,
      redactionState.isRedacting,
      zoom,
      zoomState.isMarqueeZoomActive,
    ]
  )
  const setMode = React.useCallback(
    (nextMode: PdfEditorMode) => {
      setModeState((previous) => {
        if (previous === nextMode) return previous
        onModeChange?.(nextMode)
        return nextMode
      })
      const defaultPanel: Record<PdfEditorMode, PdfEditorRightPanel | null> = {
        view: null,
        annotate: "properties",
        sign: "signatures",
        forms: "forms",
        redact: "redactions",
      }
      const panel = defaultPanel[nextMode]
      if (panel) setRightPanel(panel)
      else setRightPanel(null)
    },
    [onModeChange]
  )
  React.useEffect(() => {
    deactivateTools()
    annotation?.deselectAnnotation()
    // Only react to mode changes here; tool state is otherwise managed by the toolbar.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mode])
  React.useEffect(() => {
    annotation?.setLocked(
      mode === "forms" && formDesignMode
        ? { type: LockModeType.None }
        : { type: LockModeType.Include, categories: ["form"] }
    )
  }, [annotation, formDesignMode, mode])
  React.useEffect(() => {
    if (mode !== "forms" && formDesignMode) setFormDesignMode(false)
  }, [formDesignMode, mode, setFormDesignMode])
  /* ---- history ---------------------------------------------------------- */
  React.useEffect(() => {
    if (!historyScope) return
    const sync = () => {
      const state = historyScope.getHistoryState()
      setHistoryState({
        canUndo: state.global.canUndo,
        canRedo: state.global.canRedo,
      })
    }
    sync()
    return historyScope.onHistoryChange(sync)
  }, [historyScope])
  /* ---- callbacks: page, annotations, forms, redactions ------------------ */
  React.useEffect(() => {
    if (!controlsDisabled && activePage > 0) onActivePageChange?.(activePage)
  }, [activePage, controlsDisabled, onActivePageChange])
  const onAnnotationEventRef = React.useRef(onAnnotationEvent)
  const onAnnotationsChangeRef = React.useRef(onAnnotationsChange)
  const onFormValuesChangeRef = React.useRef(onFormValuesChange)
  const onRedactionsAppliedRef = React.useRef(onRedactionsApplied)
  React.useEffect(() => {
    onAnnotationEventRef.current = onAnnotationEvent
    onAnnotationsChangeRef.current = onAnnotationsChange
    onFormValuesChangeRef.current = onFormValuesChange
    onRedactionsAppliedRef.current = onRedactionsApplied
  })
  React.useEffect(() => {
    if (!annotation) return
    let timeout: number | null = null
    const unsubscribe = annotation.onAnnotationEvent((event) => {
      onAnnotationEventRef.current?.(event)
      if (!onAnnotationsChangeRef.current) return
      if (event.type === "loaded" || !event.committed) return
      if (timeout !== null) window.clearTimeout(timeout)
      timeout = window.setTimeout(() => {
        timeout = null
        annotation.exportAnnotations().wait((items) => {
          onAnnotationsChangeRef.current?.(items)
        }, noop)
      }, 400)
    })
    return () => {
      if (timeout !== null) window.clearTimeout(timeout)
      unsubscribe()
    }
  }, [annotation])
  React.useEffect(() => {
    if (!formScope) return
    return formScope.onFieldValueChange(() => {
      onFormValuesChangeRef.current?.(formScope.getFormValues())
    })
  }, [formScope])
  React.useEffect(() => {
    if (!redaction) return
    return redaction.onRedactionEvent((event) => {
      if (event.type !== "commit") return
      if (event.success) {
        notify(
          "Redactions applied. The content was removed from the document.",
          "success"
        )
        onRedactionsAppliedRef.current?.()
      } else {
        notify("The redactions could not be applied.", "error")
      }
    })
  }, [notify, redaction])
  /* ---- initial zoom (numeric defaults never lift the viewport gate) ----- */
  const initialZoomDocumentRef = React.useRef<string | null>(null)
  React.useEffect(() => {
    if (!pdfDocument || !zoom) return
    if (initialZoomDocumentRef.current === documentId) return
    initialZoomDocumentRef.current = documentId
    zoom.requestZoom(toZoomLevel(defaultZoom))
  }, [defaultZoom, documentId, pdfDocument, zoom])
  /* ---- document actions ------------------------------------------------- */
  const scrollToPage = React.useCallback(
    (
      pageNumber: number,
      options?:
        | {
            smooth?: boolean
          }
        | ScrollIntoViewOptions
    ) => {
      const smooth =
        options && "smooth" in options
          ? options.smooth
          : options && "behavior" in options
            ? options.behavior === "smooth"
            : false
      scroll?.scrollToPage({ pageNumber, behavior: smooth ? "smooth" : "auto" })
    },
    [scroll]
  )
  const scrollToAnnotation = React.useCallback(
    (target: PdfAnnotationObject) => {
      scroll?.scrollToPage({
        pageNumber: target.pageIndex + 1,
        pageCoordinates: { x: target.rect.origin.x, y: target.rect.origin.y },
        alignX: 50,
        alignY: 25,
        behavior: "smooth",
      })
    },
    [scroll]
  )
  const getDocumentBuffer = React.useCallback(async () => {
    if (!exportScope) throw new Error("The document is not ready.")
    if (annotation && annotationState.hasPendingChanges) {
      try {
        await annotation.commit().toPromise()
      } catch {
        // Continue with whatever the engine currently holds.
      }
    }
    return exportScope.saveAsCopy().toPromise()
  }, [annotation, annotationState.hasPendingChanges, exportScope])
  const downloadDocument = React.useCallback(
    async (name?: string) => {
      const targetName = getPdfFileName(name ?? fileName)
      setIsDownloading(true)
      try {
        const buffer = await getDocumentBuffer()
        downloadArrayBuffer(buffer, targetName)
        onSave?.({ buffer, fileName: targetName })
      } catch (error) {
        console.error(error)
        notify("The document could not be saved.", "error")
      } finally {
        setIsDownloading(false)
      }
    },
    [fileName, getDocumentBuffer, notify, onSave]
  )
  const flattenDocument = React.useCallback(() => {
    setDialog({
      type: "confirm",
      title: "Flatten annotations and form fields?",
      description:
        "Annotations and filled form fields become part of the page content and can no longer be edited.",
      confirmLabel: "Flatten",
      destructive: true,
      onConfirm: async () => {
        if (!registry || !pdfDocument) return
        try {
          if (annotation && annotationState.hasPendingChanges) {
            await annotation.commit().toPromise()
          }
          const engine = registry.getEngine()
          for (const page of pdfDocument.pages) {
            await engine
              .flattenPage(pdfDocument, page, {
                flag: PdfPageFlattenFlag.Display,
              })
              .toPromise()
          }
          const buffer = await engine.saveAsCopy(pdfDocument).toPromise()
          onReplaceDocument(buffer, fileName)
          notify("Annotations flattened into the page content.", "success")
        } catch (error) {
          console.error(error)
          notify("The document could not be flattened.", "error")
        }
      },
    })
  }, [
    annotation,
    annotationState.hasPendingChanges,
    fileName,
    notify,
    onReplaceDocument,
    pdfDocument,
    registry,
  ])
  const exportAnnotationsJson = React.useCallback(() => {
    if (!annotation) return
    annotation.exportAnnotations().wait(
      (items) => {
        downloadBlob(
          new Blob([JSON.stringify(serializeAnnotationItems(items), null, 2)], {
            type: "application/json",
          }),
          `${fileName.replace(/\.pdf$/i, "")}-annotations.json`
        )
        notify(
          `Exported ${items.length} annotation${items.length === 1 ? "" : "s"}.`,
          "success"
        )
      },
      () => notify("The annotations could not be exported.", "error")
    )
  }, [annotation, fileName, notify])
  const importAnnotationsFile = React.useCallback(
    async (file: File) => {
      if (!annotation) return
      try {
        const items = deserializeAnnotationItems(JSON.parse(await file.text()))
        annotation.importAnnotations(items)
        notify(
          `Imported ${items.length} annotation${items.length === 1 ? "" : "s"}.`,
          "success"
        )
      } catch {
        notify("The file is not a valid annotation export.", "error")
      }
    },
    [annotation, notify]
  )
  const deleteSelectedAnnotations = React.useCallback(() => {
    if (!annotation || selectedAnnotations.length === 0) return
    const deletable = selectedAnnotations.filter(
      (item) => !annotation.isAnnotationStructurallyLocked(item.object)
    )
    if (deletable.length === 0) return
    annotation.deleteAnnotations(
      deletable.map((item) => ({
        pageIndex: item.object.pageIndex,
        id: item.object.id,
      }))
    )
  }, [annotation, selectedAnnotations])
  const applyAllRedactions = React.useCallback(() => {
    if (!redaction || redactionState.pendingCount === 0) return
    setDialog({
      type: "confirm",
      title: `Apply ${redactionState.pendingCount} redaction${redactionState.pendingCount === 1 ? "" : "s"}?`,
      description:
        "Redaction permanently removes the marked content from the document. This cannot be undone.",
      confirmLabel: "Apply redactions",
      onConfirm: () => {
        redaction.commitAllPending().wait(noop, noop)
      },
    })
  }, [redaction, redactionState.pendingCount])
  /* ---- imperative handle ------------------------------------------------ */
  React.useEffect(() => {
    if (controlsDisabled) return
    handleRef.current = {
      getDocumentBuffer,
      download: downloadDocument,
      print: async (options) => {
        if (!print) throw new Error("Printing is not available.")
        await print.print(options).toPromise()
      },
      exportAnnotations: async () => {
        if (!annotation) return []
        return annotation.exportAnnotations().toPromise()
      },
      importAnnotations: (items) => annotation?.importAnnotations(items),
      getFormValues: () => formScope?.getFormValues() ?? {},
      setFormValues: async (values) => {
        if (!formScope) return
        await formScope.setFormValues(values).toPromise()
      },
      applyRedactions: async () => {
        if (!redaction) return
        await redaction.commitAllPending().toPromise()
      },
      setMode,
      setActiveTool: (toolId) => annotation?.setActiveTool(toolId),
      scrollToPage: (pageNumber, options) => scrollToPage(pageNumber, options),
      undo: () => historyScope?.undo(),
      redo: () => historyScope?.redo(),
      getViewportElement: () => viewportElementRef.current,
    }
    return () => {
      handleRef.current = null
    }
  }, [
    annotation,
    controlsDisabled,
    downloadDocument,
    formScope,
    getDocumentBuffer,
    handleRef,
    historyScope,
    print,
    redaction,
    scrollToPage,
    setMode,
  ])
  /* ---- keyboard shortcuts ----------------------------------------------- */
  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (controlsDisabled) return
      const key = event.key
      const mod = event.metaKey || event.ctrlKey
      if (isTypingTarget(event.target)) {
        if (key === "Escape") (event.target as HTMLElement).blur()
        return
      }
      if (key === "Escape") {
        deactivateTools()
        annotation?.deselectAnnotation()
        selectionScope?.clear()
        setSearchOpen(false)
        event.preventDefault()
        return
      }
      if (
        (key === "Delete" || key === "Backspace") &&
        selectedAnnotations.length > 0
      ) {
        deleteSelectedAnnotations()
        event.preventDefault()
        return
      }
      if (mod && key.toLowerCase() === "z") {
        if (event.shiftKey) historyScope?.redo()
        else historyScope?.undo()
        event.preventDefault()
        return
      }
      if (mod && key.toLowerCase() === "y") {
        historyScope?.redo()
        event.preventDefault()
        return
      }
      if (mod && (key === "=" || key === "+")) {
        zoom?.zoomIn()
        event.preventDefault()
        return
      }
      if (mod && key === "-") {
        zoom?.zoomOut()
        event.preventDefault()
        return
      }
      if (mod && key === "0") {
        zoom?.requestZoom(ZoomMode.FitPage)
        event.preventDefault()
        return
      }
      if (mod && key.toLowerCase() === "p" && features.print) {
        setDialog({ type: "print" })
        event.preventDefault()
        return
      }
      if (mod && key.toLowerCase() === "s" && showDownload) {
        void downloadDocument()
        event.preventDefault()
        return
      }
      if (mod && key.toLowerCase() === "f") {
        setSearchOpen(true)
        event.preventDefault()
        return
      }
      if (mod && key.toLowerCase() === "a" && annotation && mode !== "view") {
        const ids = annotationState.pages[Math.max(0, activePage - 1)] ?? []
        if (ids.length > 0) {
          annotation.setSelection(ids)
          event.preventDefault()
        }
        return
      }
      if (!mod && !event.altKey && key.toLowerCase() === "h") {
        pan?.togglePan()
        event.preventDefault()
        return
      }
      if (!mod && !event.altKey && key.toLowerCase() === "v") {
        deactivateTools()
        event.preventDefault()
      }
    },
    [
      activePage,
      annotation,
      annotationState.pages,
      controlsDisabled,
      deactivateTools,
      deleteSelectedAnnotations,
      downloadDocument,
      features.print,
      historyScope,
      mode,
      pan,
      selectedAnnotations.length,
      selectionScope,
      showDownload,
      zoom,
    ]
  )
  const handlePointerDownCapture = React.useCallback(
    (event: React.PointerEvent<HTMLDivElement>) => {
      const root = rootRef.current
      if (!root) return
      const active = window.document.activeElement
      if (active && root.contains(active) && active !== root) return
      if (!(event.target instanceof Node) || !root.contains(event.target))
        return
      if (isTypingTarget(event.target)) return
      root.focus({ preventScroll: true })
    },
    []
  )
  /* ---- context ---------------------------------------------------------- */
  const openDialog = React.useCallback(
    (request: PdfEditorDialogRequest) => setDialog(request),
    []
  )
  const toggleLeftPanel = React.useCallback(
    (panel: PdfEditorLeftPanel) =>
      setLeftPanel((previous) => (previous === panel ? null : panel)),
    [setLeftPanel]
  )
  const toggleRightPanel = React.useCallback(
    (panel: PdfEditorRightPanel) =>
      setRightPanel((previous) => (previous === panel ? null : panel)),
    []
  )
  const contextValue = React.useMemo<PdfEditorContextValue>(
    () => ({
      documentId,
      document: pdfDocument,
      fileName,
      mode,
      setMode,
      leftPanel,
      setLeftPanel,
      toggleLeftPanel,
      rightPanel,
      setRightPanel,
      toggleRightPanel,
      openDialog,
      notify,
      features,
      permissions,
      annotationAuthor,
      selectionActions,
      formDesignMode,
      setFormDesignMode,
      replaceDocument: (buffer, name) =>
        onReplaceDocument(buffer, name ?? fileName),
      downloadDocument,
      getDocumentBuffer,
      scrollToPage,
      scrollToAnnotation,
      onCapture,
      onSelectionAction,
      signatureFontsStylesheetUrl,
    }),
    [
      annotationAuthor,
      documentId,
      downloadDocument,
      features,
      fileName,
      formDesignMode,
      getDocumentBuffer,
      leftPanel,
      mode,
      notify,
      onCapture,
      onReplaceDocument,
      onSelectionAction,
      openDialog,
      pdfDocument,
      permissions,
      rightPanel,
      scrollToAnnotation,
      scrollToPage,
      selectionActions,
      setFormDesignMode,
      setLeftPanel,
      setMode,
      signatureFontsStylesheetUrl,
      toggleLeftPanel,
      toggleRightPanel,
    ]
  )
  /* ---- page renderer ---------------------------------------------------- */
  const renderPage = React.useCallback(
    (page: PageLayout) => (
      <Rotate documentId={documentId} pageIndex={page.pageIndex}>
        <PagePointerProvider
          documentId={documentId}
          pageIndex={page.pageIndex}
          data-pdf-editor-page={page.pageNumber}
          className="relative border border-transparent bg-transparent shadow-xs select-none selection:bg-transparent selection:text-inherit"
          style={{ backgroundColor: "transparent" }}
        >
          <div
            aria-hidden="true"
            className="pointer-events-none absolute inset-0 bg-white"
          />
          <RenderLayer
            documentId={documentId}
            pageIndex={page.pageIndex}
            scale={Math.min(currentZoom, PAGE_BASE_RENDER_MAX_SCALE)}
            dpr={PAGE_BASE_RENDER_DPR}
            className="pointer-events-none absolute inset-0 h-full w-full object-fill opacity-100 blur-[0.35px] transition-none"
          />
          <TilingLayer
            documentId={documentId}
            pageIndex={page.pageIndex}
            className="pointer-events-none opacity-100 transition-none [&_img]:opacity-100 [&_img]:transition-none"
          />
          <SearchLayer
            documentId={documentId}
            pageIndex={page.pageIndex}
            className="pointer-events-none"
            highlightColor="rgba(253, 224, 71, 0.45)"
            activeHighlightColor="rgba(249, 115, 22, 0.55)"
          />
          <MarqueeZoom documentId={documentId} pageIndex={page.pageIndex} />
          <MarqueeCapture documentId={documentId} pageIndex={page.pageIndex} />
          <SelectionLayer
            documentId={documentId}
            pageIndex={page.pageIndex}
            textStyle={{ background: "rgba(59, 130, 246, 0.3)" }}
            selectionMenu={(props) => (
              <PdfEditorTextSelectionMenu {...props} documentId={documentId} />
            )}
          />
          <AnnotationLayer
            documentId={documentId}
            pageIndex={page.pageIndex}
            selectionOutline={{
              color: PDF_EDITOR_SELECTION_COLOR,
              style: "solid",
              width: 1,
              offset: 2,
            }}
            groupSelectionOutline={{
              color: PDF_EDITOR_SELECTION_COLOR,
              style: "dashed",
              width: 1,
              offset: 3,
            }}
            resizeUI={{ size: 10, color: PDF_EDITOR_SELECTION_COLOR }}
            vertexUI={{ size: 10, color: PDF_EDITOR_SELECTION_COLOR }}
            rotationUI={{
              size: 18,
              color: "#ffffff",
              iconColor: PDF_EDITOR_SELECTION_COLOR,
              connectorColor: PDF_EDITOR_SELECTION_COLOR,
              showConnector: true,
            }}
            selectionMenu={(props) => (
              <PdfEditorAnnotationMenu {...props} documentId={documentId} />
            )}
            groupSelectionMenu={(props) => (
              <PdfEditorGroupMenu {...props} documentId={documentId} />
            )}
          />
          <RedactionLayer
            documentId={documentId}
            pageIndex={page.pageIndex}
            selectionMenu={(props) => (
              <PdfEditorRedactionMenu {...props} documentId={documentId} />
            )}
          />
          {renderPageOverlay?.({
            pageNumber: page.pageNumber,
            pageWidth: page.width,
            pageHeight: page.height,
            scale: currentZoom,
            rotation: 0,
          })}
        </PagePointerProvider>
      </Rotate>
    ),
    [currentZoom, documentId, renderPageOverlay]
  )
  /* ---- toolbar pieces --------------------------------------------------- */
  const activeToolColorKey = React.useMemo(() => {
    if (!activeTool) return null
    const defaults = activeTool.defaults as Record<string, unknown>
    if ("fontColor" in defaults) return "fontColor"
    if ("strokeColor" in defaults) return "strokeColor"
    if ("color" in defaults) return "color"
    return null
  }, [activeTool])
  const activeToolColor = activeToolColorKey
    ? String(
        (activeTool?.defaults as Record<string, unknown>)[activeToolColorKey] ??
          "#111827"
      )
    : null
  const visibleLeftTabs = LEFT_PANEL_TABS.filter(
    (tab) => tab.feature === null || features[tab.feature]
  )
  const visibleRightTabs = RIGHT_PANEL_TABS.filter(
    (tab) => tab.feature === null || features[tab.feature]
  )
  const canAnnotate = features.annotate && permissions.canModifyAnnotations
  const canRedact = features.redact && permissions.canModifyContents
  const canDesignForms = features.forms && permissions.canModifyAnnotations
  const selectToolButton = (
    <PdfEditorToolButton
      label="Select"
      shortcut="V"
      active={
        !activeToolId &&
        !isPanning &&
        !captureState.isMarqueeCaptureActive &&
        !zoomState.isMarqueeZoomActive &&
        !redactionState.isRedacting
      }
      onClick={deactivateTools}
    >
      <CursorGlyph className="size-4" />
    </PdfEditorToolButton>
  )
  const renderToolButton = (
    tool: {
      id: string
      label: string
      glyph: PdfEditorGlyph
      hint?: string
    },
    disabled: boolean
  ) => {
    const Glyph = tool.glyph
    return (
      <PdfEditorToolButton
        key={tool.id}
        label={tool.hint ? `${tool.label} · ${tool.hint}` : tool.label}
        active={activeToolId === tool.id}
        disabled={disabled}
        onClick={() => setActiveTool(tool.id)}
      >
        <Glyph className="size-4" />
      </PdfEditorToolButton>
    )
  }
  let ribbon: React.ReactNode = null
  if (mode === "view") {
    ribbon = (
      <>
        {selectToolButton}
        <PdfEditorToolButton
          label="Hand tool"
          shortcut="H"
          active={isPanning}
          onClick={() => pan?.togglePan()}
        >
          <HandGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Zoom to area"
          active={zoomState.isMarqueeZoomActive}
          onClick={() => zoom?.toggleMarqueeZoom()}
        >
          <MarqueeZoomGlyph className="size-4" />
        </PdfEditorToolButton>
        {features.capture ? (
          <PdfEditorToolButton
            label="Capture area as image"
            active={captureState.isMarqueeCaptureActive}
            onClick={() => capture?.toggleMarqueeCapture()}
          >
            <CameraGlyph className="size-4" />
          </PdfEditorToolButton>
        ) : null}
        <PdfEditorToolbarSeparator />
        <Select
          items={SPREAD_MODE_OPTIONS}
          value={spreadMode}
          disabled={controlsDisabled}
          modal={false}
          onValueChange={(value) =>
            spread?.setSpreadMode(String(value) as SpreadMode)
          }
        >
          <SelectTrigger
            size="sm"
            className="w-[132px]"
            aria-label="Page layout"
          >
            <BookOpenGlyph className="size-4 text-muted-foreground" />
            <SelectValue placeholder="Layout" />
          </SelectTrigger>
          <SelectContent alignItemWithTrigger={false}>
            {SPREAD_MODE_OPTIONS.map((option) => (
              <SelectItem key={option.value} value={option.value}>
                {option.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <Select
          items={SCROLL_DIRECTION_OPTIONS}
          value={horizontalScroll ? "horizontal" : "vertical"}
          disabled={controlsDisabled}
          modal={false}
          onValueChange={(value) => {
            const horizontal = value === "horizontal"
            setHorizontalScroll(horizontal)
            scrollCapability?.setScrollStrategy(
              horizontal ? ScrollStrategy.Horizontal : ScrollStrategy.Vertical,
              documentId
            )
          }}
        >
          <SelectTrigger
            size="sm"
            className="w-[128px]"
            aria-label="Scroll direction"
          >
            <SelectValue placeholder="Scroll" />
          </SelectTrigger>
          <SelectContent alignItemWithTrigger={false}>
            {SCROLL_DIRECTION_OPTIONS.map((option) => (
              <SelectItem key={option.value} value={option.value}>
                {option.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </>
    )
  } else if (mode === "annotate") {
    ribbon = (
      <>
        {selectToolButton}
        {PDF_EDITOR_ANNOTATION_TOOL_GROUPS.map((group) => (
          <React.Fragment key={group.id}>
            <PdfEditorToolbarSeparator />
            {group.tools.map((tool) => renderToolButton(tool, !canAnnotate))}
          </React.Fragment>
        ))}
        {features.stamps ? (
          <PdfEditorToolButton
            label="Rubber stamps"
            active={rightPanel === "stamps"}
            disabled={!canAnnotate}
            onClick={() => toggleRightPanel("stamps")}
          >
            <StampGlyph className="size-4" />
          </PdfEditorToolButton>
        ) : null}
        {activeTool && activeToolColorKey && activeToolColor ? (
          <>
            <PdfEditorToolbarSeparator />
            <ColorPicker
              label={`${activeTool.name} color`}
              icon={PaletteGlyph}
              color={activeToolColor}
              onChange={(color) =>
                annotationCapability?.setToolDefaults(activeTool.id, {
                  [activeToolColorKey]: color,
                })
              }
            />
          </>
        ) : null}
        <div className="ml-auto flex items-center gap-0.5">
          <PdfEditorToolButton
            label="Delete selected"
            shortcut="Delete"
            disabled={selectedAnnotations.length === 0}
            onClick={deleteSelectedAnnotations}
          >
            <TrashGlyph className="size-4" />
          </PdfEditorToolButton>
          {features.comments ? (
            <PdfEditorToolButton
              label="Comments"
              active={rightPanel === "comments"}
              onClick={() => toggleRightPanel("comments")}
            >
              <CommentsGlyph className="size-4" />
            </PdfEditorToolButton>
          ) : null}
          <PdfEditorToolButton
            label="Properties"
            active={rightPanel === "properties"}
            onClick={() => toggleRightPanel("properties")}
          >
            <SlidersGlyph className="size-4" />
          </PdfEditorToolButton>
        </div>
      </>
    )
  } else if (mode === "sign") {
    ribbon = (
      <>
        {selectToolButton}
        <PdfEditorToolbarSeparator />
        <Button
          type="button"
          size="sm"
          variant="outline"
          disabled={!canAnnotate}
          onClick={() => setDialog({ type: "signature" })}
        >
          <PlusGlyph className="size-4" />
          Create signature
        </Button>
        <PdfEditorToolButton
          label="Saved signatures"
          active={rightPanel === "signatures"}
          onClick={() => toggleRightPanel("signatures")}
        >
          <SignatureGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolbarSeparator />
        {renderToolButton(
          {
            id: "freeText",
            label: "Text box",
            glyph: TextGlyph,
            hint: "Click to add text",
          },
          !canAnnotate
        )}
        <PdfEditorToolButton
          label="Date · Click to add today's date"
          disabled={!canAnnotate}
          active={
            activeToolId === "freeText" &&
            Boolean(annotationState.activeToolContext?.date)
          }
          onClick={() => {
            if (!annotation) return
            annotationCapability?.setToolDefaults("freeText", {
              contents: new Date().toLocaleDateString(),
            })
            annotation.setActiveTool("freeText", { date: true })
          }}
        >
          <FileTextGlyph className="size-4" />
        </PdfEditorToolButton>
        {renderToolButton(
          { id: "ink", label: "Draw", glyph: EditGlyph, hint: "Sign by hand" },
          !canAnnotate
        )}
      </>
    )
  } else if (mode === "forms") {
    ribbon = (
      <>
        {selectToolButton}
        <PdfEditorToolbarSeparator />
        <div
          role="group"
          aria-label="Form mode"
          className="flex shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5"
        >
          <Button
            type="button"
            size="xs"
            variant={formDesignMode ? "ghost" : "default"}
            aria-pressed={!formDesignMode}
            onClick={() => setFormDesignMode(false)}
          >
            Fill
          </Button>
          <Button
            type="button"
            size="xs"
            variant={formDesignMode ? "default" : "ghost"}
            aria-pressed={formDesignMode}
            disabled={!canDesignForms}
            onClick={() => setFormDesignMode(true)}
          >
            Design
          </Button>
        </div>
        {formDesignMode ? (
          <>
            <PdfEditorToolbarSeparator />
            {PDF_EDITOR_FORM_TOOLS.map((tool) =>
              renderToolButton(tool, !canDesignForms)
            )}
          </>
        ) : null}
        <div className="ml-auto flex items-center gap-0.5">
          <PdfEditorToolButton
            label="Delete selected"
            shortcut="Delete"
            disabled={selectedAnnotations.length === 0 || !formDesignMode}
            onClick={deleteSelectedAnnotations}
          >
            <TrashGlyph className="size-4" />
          </PdfEditorToolButton>
          <PdfEditorToolButton
            label="Form fields"
            active={rightPanel === "forms"}
            onClick={() => toggleRightPanel("forms")}
          >
            <TextFieldGlyph className="size-4" />
          </PdfEditorToolButton>
          <PdfEditorToolButton
            label="Properties"
            active={rightPanel === "properties"}
            onClick={() => toggleRightPanel("properties")}
          >
            <SlidersGlyph className="size-4" />
          </PdfEditorToolButton>
        </div>
      </>
    )
  } else if (mode === "redact") {
    const activeType = redactionState.activeType
    const pendingCount = redactionState.pendingCount
    ribbon = (
      <>
        {selectToolButton}
        <PdfEditorToolbarSeparator />
        <PdfEditorToolButton
          label="Redact · Select text or drag an area"
          active={activeType === RedactionMode.Redact}
          disabled={!canRedact}
          onClick={() => {
            deactivateTools()
            if (activeType !== RedactionMode.Redact) redaction?.enableRedact()
          }}
        >
          <EyeOffGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Mark text"
          active={activeType === RedactionMode.RedactSelection}
          disabled={!canRedact}
          onClick={() => {
            deactivateTools()
            if (activeType !== RedactionMode.RedactSelection)
              redaction?.enableRedactSelection()
          }}
        >
          <StrikethroughGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Mark area"
          active={activeType === RedactionMode.MarqueeRedact}
          disabled={!canRedact}
          onClick={() => {
            deactivateTools()
            if (activeType !== RedactionMode.MarqueeRedact)
              redaction?.enableMarqueeRedact()
          }}
        >
          <RedactAreaGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolbarSeparator />
        <Badge
          variant={pendingCount > 0 ? "secondary" : "outline"}
          className="me-1 tabular-nums"
        >
          {pendingCount} pending
        </Badge>
        <Button
          type="button"
          size="sm"
          disabled={!canRedact || pendingCount === 0}
          onClick={applyAllRedactions}
        >
          Apply all
        </Button>
        <Button
          type="button"
          size="sm"
          variant="ghost"
          disabled={pendingCount === 0}
          onClick={() => redaction?.clearPending()}
        >
          Clear
        </Button>
        <div className="ml-auto flex items-center gap-0.5">
          <PdfEditorToolButton
            label="Redactions"
            active={rightPanel === "redactions"}
            onClick={() => toggleRightPanel("redactions")}
          >
            <EyeOffGlyph className="size-4" />
          </PdfEditorToolButton>
          <PdfEditorToolButton
            label="Properties"
            active={rightPanel === "properties"}
            onClick={() => toggleRightPanel("properties")}
          >
            <SlidersGlyph className="size-4" />
          </PdfEditorToolButton>
        </div>
      </>
    )
  }
  let rightPanelContent: React.ReactNode = null
  switch (rightPanel) {
    case "properties":
      rightPanelContent = <PdfEditorPropertiesPanel documentId={documentId} />
      break
    case "comments":
      rightPanelContent = <PdfEditorCommentsPanel documentId={documentId} />
      break
    case "redactions":
      rightPanelContent = <PdfEditorRedactionsPanel documentId={documentId} />
      break
    case "stamps":
      rightPanelContent = <PdfEditorStampsPanel documentId={documentId} />
      break
    case "signatures":
      rightPanelContent = <PdfEditorSignaturesPanel documentId={documentId} />
      break
    case "forms":
      rightPanelContent = <PdfEditorFormsPanel documentId={documentId} />
      break
    default:
      rightPanelContent = null
  }
  let leftPanelContent: React.ReactNode = null
  switch (leftPanel) {
    case "thumbnails":
      leftPanelContent = (
        <PdfEditorThumbnailsPanel
          documentId={documentId}
          activePage={activePage}
          pageCount={numPages}
          onSelectPage={(pageNumber) => scrollToPage(pageNumber)}
        />
      )
      break
    case "outline":
      leftPanelContent = <PdfEditorOutlinePanel documentId={documentId} />
      break
    case "attachments":
      leftPanelContent = <PdfEditorAttachmentsPanel documentId={documentId} />
      break
    case "pages":
      leftPanelContent = (
        <PdfEditorPagesPanel documentId={documentId} activePage={activePage} />
      )
      break
    default:
      leftPanelContent = null
  }
  return (
    <PdfEditorContextProvider value={contextValue}>
      <ToastProvider
        limit={3}
        timeout={NOTICE_DURATION_MS}
        toastManager={editorToastManager}
        portalProps={{ container: toastContainer }}
        viewportProps={{ className: "absolute" }}
      >
        <div
          data-slot="pdf-editor"
          data-mode={mode}
          data-state={documentState}
          aria-busy={documentState === "loading"}
          data-fullscreen={fullscreenState.isFullscreen ? "" : undefined}
          className={cn(
            "flex h-full max-h-full min-h-0 w-full flex-col overflow-hidden bg-background",
            className,
            // The fullscreen plugin auto-mounts a wrapper around the EmbedPDF
            // tree; when that wrapper is fullscreen, fill it instead of the
            // consumer-provided height.
            fullscreenState.isFullscreen && "h-full max-h-full"
          )}
        >
          <div
            ref={setRootElement}
            tabIndex={-1}
            className="relative flex h-full min-h-0 flex-1 flex-col outline-none"
            onKeyDown={handleKeyDown}
            onPointerDownCapture={handlePointerDownCapture}
          >
            <input
              ref={uploadInputRef}
              type="file"
              accept="application/pdf,.pdf"
              className="sr-only"
              tabIndex={-1}
              onChange={(event) => {
                const file = event.target.files?.[0]
                if (file) onUploadFile(file)
                event.currentTarget.value = ""
              }}
            />
            <input
              ref={annotationImportInputRef}
              type="file"
              accept="application/json,.json"
              className="sr-only"
              tabIndex={-1}
              onChange={(event) => {
                const file = event.target.files?.[0]
                if (file) void importAnnotationsFile(file)
                event.currentTarget.value = ""
              }}
            />

            {show
[truncated — showing 100000 of 140309 characters]
components/extend/pdf-editor-workspace.tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

function PanelBorder({ side }: { side: "left" | "right" }) {
  return (
    <ResizableHandle
      aria-label={`Resize ${side} panel`}
      className={cn(
        "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",
        side === "left" ? "before:-left-px" : "before:left-full"
      )}
    />
  )
}
function SidebarPanel({
  defaultSize,
  ...props
}: React.ComponentProps<typeof ResizablePanel>) {
  // Keep the registration size stable until the panel is mounted again.
  const [initialSize] = React.useState(defaultSize)
  return <ResizablePanel {...props} defaultSize={initialSize} />
}
export function PdfEditorWorkspace({
  children,
  left,
  right,
  leftInline,
  rightInline,
}: {
  children: React.ReactNode
  left: React.ReactNode
  right: React.ReactNode
  leftInline: boolean
  rightInline: boolean
}) {
  const id = React.useId()
  const [leftSize, setLeftSize] = React.useState<string | number>("14rem")
  const [rightSize, setRightSize] = React.useState<string | number>("21rem")
  const leftElement = React.useRef<HTMLDivElement>(null)
  const rightElement = React.useRef<HTMLDivElement>(null)
  const leftId = `${id}-left`
  const rightId = `${id}-right`
  const rememberSizes = () => {
    if (leftElement.current)
      setLeftSize(leftElement.current.getBoundingClientRect().width)
    if (rightElement.current)
      setRightSize(rightElement.current.getBoundingClientRect().width)
  }
  const sidebar = (side: "left" | "right", content: React.ReactNode) => (
    <SidebarPanel
      id={side === "left" ? leftId : rightId}
      defaultSize={side === "left" ? leftSize : rightSize}
      minSize={side === "left" ? "14rem" : "21rem"}
      maxSize={side === "left" ? "28rem" : "32rem"}
      groupResizeBehavior="preserve-pixel-size"
      elementRef={side === "left" ? leftElement : rightElement}
      className="pointer-events-auto h-full min-h-0 overflow-hidden bg-sidebar"
    >
      <aside
        data-slot={`pdf-editor-${side}-panel`}
        className="flex h-full min-h-0 flex-col"
      >
        {content}
      </aside>
    </SidebarPanel>
  )
  return (
    <div className="relative h-full min-h-0 w-full flex-1 overflow-hidden">
      <ResizablePanelGroup
        orientation="horizontal"
        onLayoutChanged={rememberSizes}
      >
        {left && leftInline ? sidebar("left", left) : null}
        {left && leftInline ? <PanelBorder side="left" /> : null}
        <ResizablePanel
          id={`${id}-document`}
          minSize={leftInline || rightInline ? "15rem" : "0%"}
          className="relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
        >
          {children}
        </ResizablePanel>
        {right && rightInline ? <PanelBorder side="right" /> : null}
        {right && rightInline ? sidebar("right", right) : null}
      </ResizablePanelGroup>
      {(["left", "right"] as const).map((side) => {
        const content = side === "left" ? left : right
        const inline = side === "left" ? leftInline : rightInline
        if (!content || inline) return null
        return (
          <ResizablePanelGroup
            key={side}
            orientation="horizontal"
            onLayoutChanged={rememberSizes}
            className={cn(
              "pointer-events-none absolute inset-y-0 z-30",
              side === "left" ? "left-0 min-w-60" : "right-0 min-w-88"
            )}
          >
            {side === "right" ? (
              <ResizablePanel id={`${id}-right-space`} minSize="1rem" />
            ) : null}
            {side === "right" ? <PanelBorder side="right" /> : null}
            {sidebar(side, content)}
            {side === "left" ? <PanelBorder side="left" /> : null}
            {side === "left" ? (
              <ResizablePanel id={`${id}-left-space`} minSize="1rem" />
            ) : null}
          </ResizablePanelGroup>
        )
      })}
    </div>
  )
}
components/extend/pdf-editor-shared.tsx
"use client"

import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import type { DocumentPermissions } from "@embedpdf/core/react"
import type {
  PdfAnnotationObject,
  PdfDocumentObject,
  Rect,
  Size,
} from "@embedpdf/models"
import { PdfAnnotationSubtype } from "@embedpdf/models"
import type { CaptureAreaEvent } from "@embedpdf/plugin-capture/react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import {
  ScrollArea as InlineScrollArea,
  ScrollBar,
} from "@/components/ui/scroll-area"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import { IconPlaceholder } from "@/components/icon-placeholder"

/* -------------------------------------------------------------------------- */
/* Types                                                                      */
/* -------------------------------------------------------------------------- */
export type PdfEditorMode = "view" | "annotate" | "sign" | "forms" | "redact"
export type PdfEditorLeftPanel =
  | "thumbnails"
  | "outline"
  | "attachments"
  | "pages"
export type PdfEditorRightPanel =
  | "properties"
  | "comments"
  | "redactions"
  | "stamps"
  | "signatures"
  | "forms"
export type PdfEditorFeatureFlags = {
  /** Text markup, drawing, shapes, text boxes, notes, links, and image stamps. */
  annotate?: boolean
  /** Mark text or areas for redaction and permanently apply them. */
  redact?: boolean
  /** Fill AcroForm fields and design new form widgets. */
  forms?: boolean
  /** Draw, type, or upload signatures and initials, then place them. */
  sign?: boolean
  /** Rubber stamp libraries and custom stamps. */
  stamps?: boolean
  /** Rotate, reorder, delete, insert, extract, and merge pages. */
  pages?: boolean
  /** Password protection and permission restrictions. */
  security?: boolean
  print?: boolean
  /** Area screenshot capture. */
  capture?: boolean
  /** Bookmark outline sidebar. */
  outline?: boolean
  /** Embedded file attachments sidebar. */
  attachments?: boolean
  /** Comment threads sidebar. */
  comments?: boolean
  fullscreen?: boolean
}
export type PdfEditorResolvedFeatures = Required<PdfEditorFeatureFlags>
export const PDF_EDITOR_DEFAULT_FEATURES: PdfEditorResolvedFeatures = {
  annotate: true,
  redact: true,
  forms: true,
  sign: true,
  stamps: true,
  pages: true,
  security: true,
  print: true,
  capture: true,
  outline: true,
  attachments: true,
  comments: true,
  fullscreen: true,
}
/** A rectangle expressed as percentages (0-100) of the page size. */
export type PdfEditorNormalizedRect = {
  top: number
  left: number
  width: number
  height: number
}
export type PdfEditorSelectionPage = {
  pageIndex: number
  pageNumber: number
  pageSize: Size
  /** Bounding box in PDF points (origin top-left). */
  rect: Rect
  /** Per-line rectangles in PDF points. */
  rects: Rect[]
  normalizedRect: PdfEditorNormalizedRect
  normalizedRects: PdfEditorNormalizedRect[]
}
/** Emitted for custom text-selection actions. Coordinates are page-relative. */
export type PdfEditorSelectionPayload = PdfEditorSelectionPage & {
  documentId: string
  /** The selected text across all pages, joined with newlines. */
  text: string
  /** Every page that the selection spans (usually one). */
  pages: PdfEditorSelectionPage[]
}
export type PdfEditorSelectionAction = {
  id: string
  label: string
  icon?: React.ReactNode
  /** Called with page coordinates and the selected text. */
  onSelect?: (payload: PdfEditorSelectionPayload) => void
  /** Keep the text selected after the action runs. Defaults to false. */
  keepSelection?: boolean
}
export type PdfEditorDialogRequest =
  | {
      type: "print"
    }
  | {
      type: "security"
    }
  | {
      type: "properties"
    }
  | {
      type: "shortcuts"
    }
  | {
      type: "signature"
    }
  | {
      type: "link"
      source: "annotation" | "selection"
    }
  | {
      type: "confirm"
      title: string
      description?: string
      confirmLabel?: string
      destructive?: boolean
      onConfirm: () => void
    }
export type PdfEditorNoticeTone = "info" | "success" | "error"
export type PdfEditorContextValue = {
  documentId: string
  document: PdfDocumentObject | null
  fileName: string
  mode: PdfEditorMode
  setMode: (mode: PdfEditorMode) => void
  leftPanel: PdfEditorLeftPanel | null
  setLeftPanel: (panel: PdfEditorLeftPanel | null) => void
  toggleLeftPanel: (panel: PdfEditorLeftPanel) => void
  rightPanel: PdfEditorRightPanel | null
  setRightPanel: (panel: PdfEditorRightPanel | null) => void
  toggleRightPanel: (panel: PdfEditorRightPanel) => void
  openDialog: (request: PdfEditorDialogRequest) => void
  notify: (message: string, tone?: PdfEditorNoticeTone) => void
  features: PdfEditorResolvedFeatures
  permissions: DocumentPermissions
  annotationAuthor: string
  selectionActions: PdfEditorSelectionAction[]
  formDesignMode: boolean
  setFormDesignMode: React.Dispatch<React.SetStateAction<boolean>>
  /** Replaces the open document with new PDF bytes (page operations, flattening). */
  replaceDocument: (buffer: ArrayBuffer, fileName?: string) => void
  /** Downloads the current document, including pending edits. */
  downloadDocument: (fileName?: string) => Promise<void>
  /** Returns the current document bytes, including pending edits. */
  getDocumentBuffer: () => Promise<ArrayBuffer>
  scrollToPage: (
    pageNumber: number,
    options?: {
      smooth?: boolean
    }
  ) => void
  scrollToAnnotation: (annotation: PdfAnnotationObject) => void
  onCapture?: (event: CaptureAreaEvent) => void
  onSelectionAction?: (
    actionId: string,
    payload: PdfEditorSelectionPayload
  ) => void
  signatureFontsStylesheetUrl: string | null
}
const PdfEditorContext = React.createContext<PdfEditorContextValue | null>(null)
export const PdfEditorContextProvider = PdfEditorContext.Provider
export function usePdfEditor() {
  const context = React.useContext(PdfEditorContext)
  if (!context) {
    throw new Error("usePdfEditor must be used inside a PDFEditor.")
  }
  return context
}
/* -------------------------------------------------------------------------- */
/* Constants                                                                  */
/* -------------------------------------------------------------------------- */
export const PDF_EDITOR_DEFAULT_COLOR_PRESETS = [
  "#111827",
  "#e11d48",
  "#f97316",
  "#facc15",
  "#22c55e",
  "#0ea5e9",
  "#2563eb",
  "#8b5cf6",
  "#ec4899",
  "#64748b",
  "#ffffff",
]
export const PDF_EDITOR_SELECTION_COLOR = "#2563eb"
export const PDF_EDITOR_DEFAULT_STAMP_MANIFEST_URL =
  "https://cdn.jsdelivr.net/npm/@embedpdf/default-stamps/{locale}/manifest.json"
export const PDF_EDITOR_DEFAULT_SIGNATURE_FONTS_URL =
  "https://fonts.googleapis.com/css2?family=Caveat&family=Dancing+Script&family=Great+Vibes&family=Pacifico&display=swap"
export const PDF_EDITOR_SIGNATURE_FONTS: ReadonlyArray<{
  name: string
  family: string
}> = [
  { name: "Dancing Script", family: "'Dancing Script', cursive" },
  { name: "Great Vibes", family: "'Great Vibes', cursive" },
  { name: "Pacifico", family: "'Pacifico', cursive" },
  { name: "Caveat", family: "'Caveat', cursive" },
]
/* -------------------------------------------------------------------------- */
/* Icons                                                                      */
/*                                                                            */
/* Icons that end up in value position (tool catalogs, switch statements) are */
/* wrapped in small `*Glyph` components so the shadcn CLI can still swap the  */
/* placeholder for the consumer's icon library at install time.               */
/* -------------------------------------------------------------------------- */
export type PdfEditorGlyphProps = {
  className?: string
  style?: React.CSSProperties
}
export type PdfEditorGlyph = React.ComponentType<PdfEditorGlyphProps>
export function PanelLeftGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="PanelLeft"
      tabler="IconLayoutSidebar"
      hugeicons="SidebarLeftIcon"
      phosphor="SidebarIcon"
      remixicon="RiLayoutLeftLine"
      className={className}
      style={style}
    />
  )
}
export function PanelRightGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="PanelRight"
      tabler="IconLayoutSidebarRight"
      hugeicons="SidebarRightIcon"
      phosphor="SidebarSimpleIcon"
      remixicon="RiLayoutRightLine"
      className={className}
      style={style}
    />
  )
}
export function UndoGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Undo2"
      tabler="IconArrowBackUp"
      hugeicons="Undo02Icon"
      phosphor="ArrowUUpLeftIcon"
      remixicon="RiArrowGoBackLine"
      className={className}
      style={style}
    />
  )
}
export function RedoGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Redo2"
      tabler="IconArrowForwardUp"
      hugeicons="Redo02Icon"
      phosphor="ArrowUUpRightIcon"
      remixicon="RiArrowGoForwardLine"
      className={className}
      style={style}
    />
  )
}
export function ZoomOutGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="CircleMinus"
      tabler="IconCircleMinus"
      hugeicons="MinusSignCircleIcon"
      phosphor="MinusCircleIcon"
      remixicon="RiIndeterminateCircleLine"
      className={className}
      style={style}
    />
  )
}
export function ZoomInGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="CirclePlus"
      tabler="IconCirclePlus"
      hugeicons="PlusSignCircleIcon"
      phosphor="PlusCircleIcon"
      remixicon="RiAddCircleLine"
      className={className}
      style={style}
    />
  )
}
export function RotateGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="RotateCw"
      tabler="IconRotateClockwise"
      hugeicons="RotateClockwiseIcon"
      phosphor="ArrowClockwiseIcon"
      remixicon="RiClockwiseLine"
      className={className}
      style={style}
    />
  )
}
export function SearchGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Search"
      tabler="IconSearch"
      hugeicons="Search01Icon"
      phosphor="MagnifyingGlassIcon"
      remixicon="RiSearchLine"
      className={className}
      style={style}
    />
  )
}
export function MoreGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Ellipsis"
      tabler="IconDots"
      hugeicons="MoreHorizontalIcon"
      phosphor="DotsThreeIcon"
      remixicon="RiMoreLine"
      className={className}
      style={style}
    />
  )
}
export function DownloadGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Download"
      tabler="IconDownload"
      hugeicons="Download01Icon"
      phosphor="DownloadSimpleIcon"
      remixicon="RiDownload2Line"
      className={className}
      style={style}
    />
  )
}
export function UploadGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Upload"
      tabler="IconUpload"
      hugeicons="Upload01Icon"
      phosphor="UploadSimpleIcon"
      remixicon="RiUpload2Line"
      className={className}
      style={style}
    />
  )
}
export function PrintGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Printer"
      tabler="IconPrinter"
      hugeicons="PrinterIcon"
      phosphor="PrinterIcon"
      remixicon="RiPrinterLine"
      className={className}
      style={style}
    />
  )
}
export function FullscreenGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Maximize"
      tabler="IconMaximize"
      hugeicons="FullScreenIcon"
      phosphor="CornersOutIcon"
      remixicon="RiFullscreenLine"
      className={className}
      style={style}
    />
  )
}
export function ExitFullscreenGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Minimize"
      tabler="IconMinimize"
      hugeicons="MinimizeScreenIcon"
      phosphor="CornersInIcon"
      remixicon="RiFullscreenExitLine"
      className={className}
      style={style}
    />
  )
}
export function HandGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Hand"
      tabler="IconHandStop"
      hugeicons="HandIcon"
      phosphor="HandIcon"
      remixicon="RiHand"
      className={className}
      style={style}
    />
  )
}
export function CursorGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MousePointer2"
      tabler="IconPointer"
      hugeicons="Cursor01Icon"
      phosphor="CursorIcon"
      remixicon="RiCursorLine"
      className={className}
      style={style}
    />
  )
}
export function CameraGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Camera"
      tabler="IconCamera"
      hugeicons="Camera01Icon"
      phosphor="CameraIcon"
      remixicon="RiScreenshotLine"
      className={className}
      style={style}
    />
  )
}
export function MarqueeZoomGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ScanSearch"
      tabler="IconZoomInArea"
      hugeicons="ZoomInAreaIcon"
      phosphor="MagnifyingGlassPlusIcon"
      remixicon="RiZoomInLine"
      className={className}
      style={style}
    />
  )
}
export function BookOpenGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="BookOpen"
      tabler="IconBook"
      hugeicons="Book01Icon"
      phosphor="BookOpenIcon"
      remixicon="RiBookOpenLine"
      className={className}
      style={style}
    />
  )
}
export function FileGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="File"
      tabler="IconFile"
      hugeicons="File01Icon"
      phosphor="FileIcon"
      remixicon="RiFileLine"
      className={className}
      style={style}
    />
  )
}
export function ArrowsHorizontalGlyph({
  className,
  style,
}: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MoveHorizontal"
      tabler="IconArrowsHorizontal"
      hugeicons="ArrowLeftRightIcon"
      phosphor="ArrowsHorizontalIcon"
      remixicon="RiArrowLeftRightLine"
      className={className}
      style={style}
    />
  )
}
export function ArrowsVerticalGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MoveVertical"
      tabler="IconArrowsVertical"
      hugeicons="ArrowUpDownIcon"
      phosphor="ArrowsVerticalIcon"
      remixicon="RiArrowUpDownLine"
      className={className}
      style={style}
    />
  )
}
export function HighlighterGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Highlighter"
      tabler="IconHighlight"
      hugeicons="HighlighterIcon"
      phosphor="HighlighterIcon"
      remixicon="RiMarkPenLine"
      className={className}
      style={style}
    />
  )
}
export function UnderlineGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Underline"
      tabler="IconUnderline"
      hugeicons="TextUnderlineIcon"
      phosphor="TextUnderlineIcon"
      remixicon="RiUnderline"
      className={className}
      style={style}
    />
  )
}
export function StrikethroughGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Strikethrough"
      tabler="IconStrikethrough"
      hugeicons="TextStrikethroughIcon"
      phosphor="TextStrikethroughIcon"
      remixicon="RiStrikethrough"
      className={className}
      style={style}
    />
  )
}
export function SquigglyGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="LineSquiggle"
      tabler="IconWaveSine"
      hugeicons="WaveIcon"
      phosphor="WaveSineIcon"
      remixicon="RiPulseLine"
      className={className}
      style={style}
    />
  )
}
export function PenGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Pen"
      tabler="IconPencil"
      hugeicons="Pen01Icon"
      phosphor="PenIcon"
      remixicon="RiPencilLine"
      className={className}
      style={style}
    />
  )
}
export function BrushGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Brush"
      tabler="IconBrush"
      hugeicons="BrushIcon"
      phosphor="PaintBrushIcon"
      remixicon="RiBrushLine"
      className={className}
      style={style}
    />
  )
}
export function TextGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Type"
      tabler="IconTypography"
      hugeicons="TextIcon"
      phosphor="TextTIcon"
      remixicon="RiText"
      className={className}
      style={style}
    />
  )
}
export function TextFontGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Type"
      tabler="IconTypography"
      hugeicons="TextFontIcon"
      phosphor="TextAaIcon"
      remixicon="RiFontSize"
      className={className}
      style={style}
    />
  )
}
export function CalloutGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MessageSquareText"
      tabler="IconMessage2"
      hugeicons="BubbleChatIcon"
      phosphor="ChatTextIcon"
      remixicon="RiChat3Line"
      className={className}
      style={style}
    />
  )
}
export function StickyNoteGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="StickyNote"
      tabler="IconNote"
      hugeicons="StickyNote01Icon"
      phosphor="NoteIcon"
      remixicon="RiStickyNoteLine"
      className={className}
      style={style}
    />
  )
}
export function SquareGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Square"
      tabler="IconSquare"
      hugeicons="SquareIcon"
      phosphor="SquareIcon"
      remixicon="RiCheckboxBlankLine"
      className={className}
      style={style}
    />
  )
}
export function CircleGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Circle"
      tabler="IconCircle"
      hugeicons="CircleIcon"
      phosphor="CircleIcon"
      remixicon="RiCircleLine"
      className={className}
      style={style}
    />
  )
}
export function LineGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Minus"
      tabler="IconLine"
      hugeicons="MinusSignIcon"
      phosphor="LineSegmentIcon"
      remixicon="RiSubtractLine"
      className={className}
      style={style}
    />
  )
}
export function ArrowGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MoveUpRight"
      tabler="IconArrowUpRight"
      hugeicons="ArrowUpRight01Icon"
      phosphor="ArrowUpRightIcon"
      remixicon="RiArrowRightUpLine"
      className={className}
      style={style}
    />
  )
}
export function PolylineGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Spline"
      tabler="IconVectorSpline"
      hugeicons="SplinePointerIcon"
      phosphor="VectorTwoIcon"
      remixicon="RiRouteLine"
      className={className}
      style={style}
    />
  )
}
export function PolygonGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Pentagon"
      tabler="IconPolygon"
      hugeicons="PolygonIcon"
      phosphor="PolygonIcon"
      remixicon="RiPentagonLine"
      className={className}
      style={style}
    />
  )
}
export function ImageGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Image"
      tabler="IconPhoto"
      hugeicons="Image01Icon"
      phosphor="ImageIcon"
      remixicon="RiImageLine"
      className={className}
      style={style}
    />
  )
}
export function StampGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Stamp"
      tabler="IconRubberStamp"
      hugeicons="StampIcon"
      phosphor="StampIcon"
      remixicon="RiVerifiedBadgeLine"
      className={className}
      style={style}
    />
  )
}
export function LinkGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Link"
      tabler="IconLink"
      hugeicons="Link02Icon"
      phosphor="LinkIcon"
      remixicon="RiLink"
      className={className}
      style={style}
    />
  )
}
export function InsertTextGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="TextCursorInput"
      tabler="IconCursorText"
      hugeicons="CursorTextIcon"
      phosphor="CursorTextIcon"
      remixicon="RiInputCursorMove"
      className={className}
      style={style}
    />
  )
}
export function ReplaceTextGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Replace"
      tabler="IconReplace"
      hugeicons="ReplaceIcon"
      phosphor="ArrowsLeftRightIcon"
      remixicon="RiFindReplaceLine"
      className={className}
      style={style}
    />
  )
}
export function TrashGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Trash2"
      tabler="IconTrash"
      hugeicons="Delete02Icon"
      phosphor="TrashIcon"
      remixicon="RiDeleteBinLine"
      className={className}
      style={style}
    />
  )
}
export function GroupGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Group"
      tabler="IconLayersUnion"
      hugeicons="GroupItemsIcon"
      phosphor="StackIcon"
      remixicon="RiStackLine"
      className={className}
      style={style}
    />
  )
}
export function UngroupGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Ungroup"
      tabler="IconLayersSubtract"
      hugeicons="UngroupItemsIcon"
      phosphor="StackSimpleIcon"
      remixicon="RiStackLine"
      className={className}
      style={style}
    />
  )
}
export function LockGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Lock"
      tabler="IconLock"
      hugeicons="SquareLock02Icon"
      phosphor="LockIcon"
      remixicon="RiLockLine"
      className={className}
      style={style}
    />
  )
}
export function UnlockGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="LockOpen"
      tabler="IconLockOpen"
      hugeicons="SquareUnlock02Icon"
      phosphor="LockOpenIcon"
      remixicon="RiLockUnlockLine"
      className={className}
      style={style}
    />
  )
}
export function CommentGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MessageCircle"
      tabler="IconMessageCircle"
      hugeicons="Comment01Icon"
      phosphor="ChatCircleIcon"
      remixicon="RiChat1Line"
      className={className}
      style={style}
    />
  )
}
export function CommentsGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="MessagesSquare"
      tabler="IconMessages"
      hugeicons="CommentAdd01Icon"
      phosphor="ChatsIcon"
      remixicon="RiChat3Line"
      className={className}
      style={style}
    />
  )
}
export function CheckGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Check"
      tabler="IconCheck"
      hugeicons="Tick02Icon"
      phosphor="CheckIcon"
      remixicon="RiCheckLine"
      className={className}
      style={style}
    />
  )
}
export function CloseGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="X"
      tabler="IconX"
      hugeicons="Cancel01Icon"
      phosphor="XIcon"
      remixicon="RiCloseLine"
      className={className}
      style={style}
    />
  )
}
export function PlusGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Plus"
      tabler="IconPlus"
      hugeicons="Add01Icon"
      phosphor="PlusIcon"
      remixicon="RiAddLine"
      className={className}
      style={style}
    />
  )
}
export function MinusGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Minus"
      tabler="IconMinus"
      hugeicons="MinusSignIcon"
      phosphor="MinusIcon"
      remixicon="RiSubtractLine"
      className={className}
      style={style}
    />
  )
}
export function CopyGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Copy"
      tabler="IconCopy"
      hugeicons="Copy01Icon"
      phosphor="CopyIcon"
      remixicon="RiFileCopyLine"
      className={className}
      style={style}
    />
  )
}
export function EyeOffGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="EyeOff"
      tabler="IconEyeOff"
      hugeicons="ViewOffIcon"
      phosphor="EyeSlashIcon"
      remixicon="RiEyeOffLine"
      className={className}
      style={style}
    />
  )
}
export function EyeGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Eye"
      tabler="IconEye"
      hugeicons="ViewIcon"
      phosphor="EyeIcon"
      remixicon="RiEyeLine"
      className={className}
      style={style}
    />
  )
}
export function RedactAreaGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="SquareDashed"
      tabler="IconSquareDashed"
      hugeicons="CursorRectangleSelection01Icon"
      phosphor="SelectionIcon"
      remixicon="RiCropLine"
      className={className}
      style={style}
    />
  )
}
export function SignatureGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Signature"
      tabler="IconSignature"
      hugeicons="SignatureIcon"
      phosphor="SignatureIcon"
      remixicon="RiQuillPenLine"
      className={className}
      style={style}
    />
  )
}
export function TextFieldGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="RectangleEllipsis"
      tabler="IconForms"
      hugeicons="InputTextIcon"
      phosphor="TextboxIcon"
      remixicon="RiInputField"
      className={className}
      style={style}
    />
  )
}
export function CheckboxGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="SquareCheck"
      tabler="IconSquareCheck"
      hugeicons="CheckmarkSquare01Icon"
      phosphor="CheckSquareIcon"
      remixicon="RiCheckboxLine"
      className={className}
      style={style}
    />
  )
}
export function RadioGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="CircleDot"
      tabler="IconCircleDot"
      hugeicons="RadioButtonIcon"
      phosphor="RadioButtonIcon"
      remixicon="RiRadioButtonLine"
      className={className}
      style={style}
    />
  )
}
export function ComboboxGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ChevronsUpDown"
      tabler="IconSelect"
      hugeicons="UnfoldMoreIcon"
      phosphor="CaretUpDownIcon"
      remixicon="RiArrowUpDownLine"
      className={className}
      style={style}
    />
  )
}
export function ListboxGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="List"
      tabler="IconList"
      hugeicons="LeftToRightListBulletIcon"
      phosphor="ListIcon"
      remixicon="RiListUnordered"
      className={className}
      style={style}
    />
  )
}
export function ListTreeGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ListTree"
      tabler="IconListTree"
      hugeicons="ListTreeIcon"
      phosphor="TreeStructureIcon"
      remixicon="RiNodeTree"
      className={className}
      style={style}
    />
  )
}
export function PaperclipGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Paperclip"
      tabler="IconPaperclip"
      hugeicons="Attachment01Icon"
      phosphor="PaperclipIcon"
      remixicon="RiAttachment2"
      className={className}
      style={style}
    />
  )
}
export function GridGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="LayoutGrid"
      tabler="IconLayoutGrid"
      hugeicons="GridViewIcon"
      phosphor="SquaresFourIcon"
      remixicon="RiLayoutGridLine"
      className={className}
      style={style}
    />
  )
}
export function SlidersGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="SlidersHorizontal"
      tabler="IconAdjustmentsHorizontal"
      hugeicons="SlidersHorizontalIcon"
      phosphor="SlidersHorizontalIcon"
      remixicon="RiEqualizerLine"
      className={className}
      style={style}
    />
  )
}
export function InfoGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Info"
      tabler="IconInfoCircle"
      hugeicons="InformationCircleIcon"
      phosphor="InfoIcon"
      remixicon="RiInformationLine"
      className={className}
      style={style}
    />
  )
}
export function ShieldGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ShieldCheck"
      tabler="IconShieldLock"
      hugeicons="Shield01Icon"
      phosphor="ShieldCheckIcon"
      remixicon="RiShieldKeyholeLine"
      className={className}
      style={style}
    />
  )
}
export function FileTextGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="FileText"
      tabler="IconFileText"
      hugeicons="File01Icon"
      phosphor="FileTextIcon"
      remixicon="RiFileTextLine"
      className={className}
      style={style}
    />
  )
}
export function ChevronDownGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ChevronDown"
      tabler="IconChevronDown"
      hugeicons="ArrowDown01Icon"
      phosphor="CaretDownIcon"
      remixicon="RiArrowDownSLine"
      className={className}
      style={style}
    />
  )
}
export function ChevronUpGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ChevronUp"
      tabler="IconChevronUp"
      hugeicons="ArrowUp01Icon"
      phosphor="CaretUpIcon"
      remixicon="RiArrowUpSLine"
      className={className}
      style={style}
    />
  )
}
export function ChevronLeftGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ChevronLeft"
      tabler="IconChevronLeft"
      hugeicons="ArrowLeft01Icon"
      phosphor="CaretLeftIcon"
      remixicon="RiArrowLeftSLine"
      className={className}
      style={style}
    />
  )
}
export function ChevronRightGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ChevronRight"
      tabler="IconChevronRight"
      hugeicons="ArrowRight01Icon"
      phosphor="CaretRightIcon"
      remixicon="RiArrowRightSLine"
      className={className}
      style={style}
    />
  )
}
export function LayersGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Layers"
      tabler="IconStack2"
      hugeicons="Layers01Icon"
      phosphor="StackIcon"
      remixicon="RiStackLine"
      className={className}
      style={style}
    />
  )
}
export function FilePlusGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="FilePlus"
      tabler="IconFilePlus"
      hugeicons="FileAddIcon"
      phosphor="FilePlusIcon"
      remixicon="RiFileAddLine"
      className={className}
      style={style}
    />
  )
}
export function ScissorsGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Scissors"
      tabler="IconScissors"
      hugeicons="Scissor01Icon"
      phosphor="ScissorsIcon"
      remixicon="RiScissorsLine"
      className={className}
      style={style}
    />
  )
}
export function MergeGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Merge"
      tabler="IconArrowMerge"
      hugeicons="GitMergeIcon"
      phosphor="GitMergeIcon"
      remixicon="RiGitMergeLine"
      className={className}
      style={style}
    />
  )
}
export function RefreshGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="RefreshCw"
      tabler="IconRefresh"
      hugeicons="RefreshIcon"
      phosphor="ArrowsClockwiseIcon"
      remixicon="RiRefreshLine"
      className={className}
      style={style}
    />
  )
}
export function SaveGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Save"
      tabler="IconDeviceFloppy"
      hugeicons="FloppyDiskIcon"
      phosphor="FloppyDiskIcon"
      remixicon="RiSaveLine"
      className={className}
      style={style}
    />
  )
}
export function KeyboardGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Keyboard"
      tabler="IconKeyboard"
      hugeicons="KeyboardIcon"
      phosphor="KeyboardIcon"
      remixicon="RiKeyboardLine"
      className={className}
      style={style}
    />
  )
}
export function PaletteGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Palette"
      tabler="IconPalette"
      hugeicons="PaintBoardIcon"
      phosphor="PaletteIcon"
      remixicon="RiPaletteLine"
      className={className}
      style={style}
    />
  )
}
export function BoldGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Bold"
      tabler="IconBold"
      hugeicons="TextBoldIcon"
      phosphor="TextBIcon"
      remixicon="RiBold"
      className={className}
      style={style}
    />
  )
}
export function ItalicGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Italic"
      tabler="IconItalic"
      hugeicons="TextItalicIcon"
      phosphor="TextItalicIcon"
      remixicon="RiItalic"
      className={className}
      style={style}
    />
  )
}
export function AlignLeftGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="TextAlignStart"
      tabler="IconAlignLeft"
      hugeicons="TextAlignLeft01Icon"
      phosphor="TextAlignLeftIcon"
      remixicon="RiAlignLeft"
      className={className}
      style={style}
    />
  )
}
export function AlignCenterGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="TextAlignCenter"
      tabler="IconAlignCenter"
      hugeicons="TextAlignCenterIcon"
      phosphor="TextAlignCenterIcon"
      remixicon="RiAlignCenter"
      className={className}
      style={style}
    />
  )
}
export function AlignRightGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="TextAlignEnd"
      tabler="IconAlignRight"
      hugeicons="TextAlignRight01Icon"
      phosphor="TextAlignRightIcon"
      remixicon="RiAlignRight"
      className={className}
      style={style}
    />
  )
}
export function AlignTopGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ArrowUpToLine"
      tabler="IconAlignBoxTopCenter"
      hugeicons="AlignBoxTopCenterIcon"
      phosphor="AlignTopIcon"
      remixicon="RiAlignTop"
      className={className}
      style={style}
    />
  )
}
export function AlignMiddleGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="AlignVerticalJustifyCenter"
      tabler="IconAlignBoxCenterMiddle"
      hugeicons="AlignBoxMiddleCenterIcon"
      phosphor="AlignCenterVerticalIcon"
      remixicon="RiAlignVertically"
      className={className}
      style={style}
    />
  )
}
export function AlignBottomGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ArrowDownToLine"
      tabler="IconAlignBoxBottomCenter"
      hugeicons="AlignBoxBottomCenterIcon"
      phosphor="AlignBottomIcon"
      remixicon="RiAlignBottom"
      className={className}
      style={style}
    />
  )
}
export function FlagGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Flag"
      tabler="IconFlag"
      hugeicons="Flag01Icon"
      phosphor="FlagIcon"
      remixicon="RiFlagLine"
      className={className}
      style={style}
    />
  )
}
export function ClipboardGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Clipboard"
      tabler="IconClipboard"
      hugeicons="ClipboardIcon"
      phosphor="ClipboardIcon"
      remixicon="RiClipboardLine"
      className={className}
      style={style}
    />
  )
}
export function KeyGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Key"
      tabler="IconKey"
      hugeicons="Key01Icon"
      phosphor="KeyIcon"
      remixicon="RiKey2Line"
      className={className}
      style={style}
    />
  )
}
export function ExternalLinkGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ExternalLink"
      tabler="IconExternalLink"
      hugeicons="ExternalLinkIcon"
      phosphor="ArrowSquareOutIcon"
      remixicon="RiExternalLinkLine"
      className={className}
      style={style}
    />
  )
}
export function MoveGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Move"
      tabler="IconArrowsMove"
      hugeicons="MoveIcon"
      phosphor="ArrowsOutCardinalIcon"
      remixicon="RiDragMove2Line"
      className={className}
      style={style}
    />
  )
}
export function AlertGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="TriangleAlert"
      tabler="IconAlertTriangle"
      hugeicons="Alert01Icon"
      phosphor="WarningIcon"
      remixicon="RiAlertLine"
      className={className}
      style={style}
    />
  )
}
export function ArrowUpGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ArrowUp"
      tabler="IconArrowUp"
      hugeicons="ArrowUp02Icon"
      phosphor="ArrowUpIcon"
      remixicon="RiArrowUpLine"
      className={className}
      style={style}
    />
  )
}
export function ArrowDownGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="ArrowDown"
      tabler="IconArrowDown"
      hugeicons="ArrowDown02Icon"
      phosphor="ArrowDownIcon"
      remixicon="RiArrowDownLine"
      className={className}
      style={style}
    />
  )
}
export function EditGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Pencil"
      tabler="IconEdit"
      hugeicons="PencilEdit01Icon"
      phosphor="PencilSimpleIcon"
      remixicon="RiEditLine"
      className={className}
      style={style}
    />
  )
}
export function SparklesGlyph({ className, style }: PdfEditorGlyphProps) {
  return (
    <IconPlaceholder
      lucide="Sparkles"
      tabler="IconSparkles"
      hugeicons="SparklesIcon"
      phosphor="SparkleIcon"
      remixicon="RiSparklingLine"
      className={className}
      style={style}
    />
  )
}
/* -------------------------------------------------------------------------- */
/* Tool catalogs                                                              */
/* -------------------------------------------------------------------------- */
export type PdfEditorToolDefinition = {
  /** EmbedPDF annotation tool id. */
  id: string
  label: string
  glyph: PdfEditorGlyph
  /** Short hint shown in the tooltip. */
  hint?: string
}
export type PdfEditorToolGroup = {
  id: string
  label: string
  tools: PdfEditorToolDefinition[]
}
export const PDF_EDITOR_ANNOTATION_TOOL_GROUPS: PdfEditorToolGroup[] = [
  {
    id: "markup",
    label: "Text markup",
    tools: [
      {
        id: "highlight",
        label: "Highlight",
        glyph: HighlighterGlyph,
        hint: "Select text to highlight",
      },
      {
        id: "underline",
        label: "Underline",
        glyph: UnderlineGlyph,
        hint: "Select text to underline",
      },
      {
        id: "strikeout",
        label: "Strikethrough",
        glyph: StrikethroughGlyph,
        hint: "Select text to strike through",
      },
      {
        id: "squiggly",
        label: "Squiggly",
        glyph: SquigglyGlyph,
        hint: "Select text for a squiggly underline",
      },
    ],
  },
  {
    id: "draw",
    label: "Draw",
    tools: [
      {
        id: "ink",
        label: "Pen",
        glyph: PenGlyph,
        hint: "Draw freehand",
      },
      {
        id: "inkHighlighter",
        label: "Highlighter pen",
        glyph: BrushGlyph,
        hint: "Draw a translucent highlighter stroke",
      },
    ],
  },
  {
    id: "text",
    label: "Text",
    tools: [
      {
        id: "freeText",
        label: "Text box",
        glyph: TextGlyph,
        hint: "Click or drag to add text",
      },
      {
        id: "freeTextCallout",
        label: "Callout",
        glyph: CalloutGlyph,
        hint: "Drag to add a callout with a leader line",
      },
      {
        id: "textComment",
        label: "Sticky note",
        glyph: StickyNoteGlyph,
        hint: "Click to add a note",
      },
    ],
  },
  {
    id: "shapes",
    label: "Shapes",
    tools: [
      { id: "square", label: "Rectangle", glyph: SquareGlyph },
      { id: "circle", label: "Ellipse", glyph: CircleGlyph },
      { id: "line", label: "Line", glyph: LineGlyph },
      { id: "lineArrow", label: "Arrow", glyph: ArrowGlyph },
      {
        id: "polyline",
        label: "Polyline",
        glyph: PolylineGlyph,
        hint: "Click to add points, double-click to finish",
      },
      {
        id: "polygon",
        label: "Polygon",
        glyph: PolygonGlyph,
        hint: "Click to add points, double-click to finish",
      },
    ],
  },
  {
    id: "insert",
    label: "Insert",
    tools: [
      {
        id: "stamp",
        label: "Image",
        glyph: ImageGlyph,
        hint: "Click the page to place an image",
      },
      {
        id: "link",
        label: "Link area",
        glyph: LinkGlyph,
        hint: "Drag to add a link rectangle",
      },
      {
        id: "insertText",
        label: "Insert text mark",
        glyph: InsertTextGlyph,
        hint: "Select text to mark an insertion point",
      },
      {
        id: "replaceText",
        label: "Replace text mark",
        glyph: ReplaceTextGlyph,
        hint: "Select text to mark it for replacement",
      },
    ],
  },
]
export const PDF_EDITOR_ANNOTATION_TOOLS: PdfEditorToolDefinition[] =
  PDF_EDITOR_ANNOTATION_TOOL_GROUPS.flatMap((group) => group.tools)
export const PDF_EDITOR_FORM_TOOLS: PdfEditorToolDefinition[] = [
  {
    id: "formTextField",
    label: "Text field",
    glyph: TextFieldGlyph,
    hint: "Click or drag to add a text field",
  },
  {
    id: "formCheckbox",
    label: "Checkbox",
    glyph: CheckboxGlyph,
    hint: "Click to add a checkbox",
  },
  {
    id: "formRadioButton",
    label: "Radio button",
    glyph: RadioGlyph,
    hint: "Click to add a radio button",
  },
  {
    id: "formCombobox",
    label: "Dropdown",
    glyph: ComboboxGlyph,
    hint: "Click or drag to add a dropdown",
  },
  {
    id: "formListbox",
    label: "List box",
    glyph: ListboxGlyph,
    hint: "Click or drag to add a list box",
  },
]
export const PDF_EDITOR_MODE_OPTIONS: Array<{
  id: PdfEditorMode
  label: string
  glyph: PdfEditorGlyph
  feature: keyof PdfEditorFeatureFlags | null
}> = [
  { id: "view", label: "View", glyph: EyeGlyph, feature: null },
  { id: "annotate", label: "Annotate", glyph: PenGlyph, feature: "annotate" },
  { id: "sign", label: "Sign", glyph: SignatureGlyph, feature: "sign" },
  { id: "forms", label: "Forms", glyph: TextFieldGlyph, feature: "forms" },
  { id: "redact", label: "Redact", glyph: EyeOffGlyph, feature: "redact" },
]
export type PdfEditorAnnotationMeta = {
  label: string
  glyph: PdfEditorGlyph
  /** The most representative color of the annotation, if any. */
  color?: string
}
export function getAnnotationColor(
  annotation: PdfAnnotationObject
): string | undefined {
  const record = annotation as unknown as Record<string, unknown>
  const candidates = [record.strokeColor, record.fontColor, record.color]
  for (const candidate of candidates) {
    if (typeof candidate === "string" && candidate.trim()) return candidate
  }
  return undefined
}
export function getAnnotationMeta(
  annotation: PdfAnnotationObject
): PdfEditorAnnotationMeta {
  const color = getAnnotationColor(annotation)
  const intent = annotation.intent ?? ""
  switch (annotation.type) {
    case PdfAnnotationSubtype.HIGHLIGHT:
      return { label: "Highlight", glyph: HighlighterGlyph, color }
    case PdfAnnotationSubtype.UNDERLINE:
      return { label: "Underline", glyph: UnderlineGlyph, color }
    case PdfAnnotationSubtype.STRIKEOUT:
      return intent === "StrikeOutTextReplace"
        ? { label: "Replace text", glyph: ReplaceTextGlyph, color }
        : { label: "Strikethrough", glyph: StrikethroughGlyph, color }
    case PdfAnnotationSubtype.SQUIGGLY:
      return { label: "Squiggly", glyph: SquigglyGlyph, color }
    case PdfAnnotationSubtype.INK:
      return intent === "InkHighlight"
        ? { label: "Highlighter pen", glyph: BrushGlyph, color }
        : { label: "Ink", glyph: PenGlyph, color }
    case PdfAnnotationSubtype.SQUARE:
      return { label: "Rectangle", glyph: SquareGlyph, color }
    case PdfAnnotationSubtype.CIRCLE:
      return { label: "Ellipse", glyph: CircleGlyph, color }
    case PdfAnnotationSubtype.LINE:
      return intent === "LineArrow"
        ? { label: "Arrow", glyph: ArrowGlyph, color }
        : { label: "Line", glyph: LineGlyph, color }
    case PdfAnnotationSubtype.POLYLINE:
      return { label: "Polyline", glyph: PolylineGlyph, color }
    case PdfAnnotationSubtype.POLYGON:
      return { label: "Polygon", glyph: PolygonGlyph, color }
    case PdfAnnotationSubtype.FREETEXT:
      return intent === "FreeTextCallout"
        ? { label: "Callout", glyph: CalloutGlyph, color }
        : { label: "Text box", glyph: TextGlyph, color }
    case PdfAnnotationSubtype.TEXT:
      return { label: "Note", glyph: StickyNoteGlyph, color }
    case PdfAnnotationSubtype.STAMP:
      return { label: "Stamp", glyph: StampGlyph, color }
    case PdfAnnotationSubtype.LINK:
      return { label: "Link", glyph: LinkGlyph, color }
    case PdfAnnotationSubtype.CARET:
      return { label: "Insert text", glyph: InsertTextGlyph, color }
    case PdfAnnotationSubtype.REDACT:
      return { label: "Redaction", glyph: EyeOffGlyph, color }
    case PdfAnnotationSubtype.WIDGET:
      return { label: "Form field", glyph: TextFieldGlyph, color }
    case PdfAnnotationSubtype.FILEATTACHMENT:
      return { label: "Attachment", glyph: PaperclipGlyph, color }
    default:
      return { label: "Annotation", glyph: FlagGlyph, color }
  }
}
/* -------------------------------------------------------------------------- */
/* Helpers                                                                    */
/* -------------------------------------------------------------------------- */
export function clamp(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value))
}
export function ensurePdfExtension(fileName: string) {
  return fileName.toLowerCase().endsWith(".pdf") ? fileName : `${fileName}.pdf`
}
export function getPdfFileName(fileName: string | undefined, src?: string) {
  if (fileName?.trim()) return ensurePdfExtension(fileName.trim())
  if (src && !src.startsWith("blob:") && !src.startsWith("data:")) {
    const pathname = src.split(/[?#]/)[0] ?? ""
    const rawName = pathname.split("/").pop() || ""
    if (rawName) {
      try {
        return ensurePdfExtension(decodeURIComponent(rawName))
      } catch {
        return ensurePdfExtension(rawName)
      }
    }
  }
  return "document.pdf"
}
export function withFileNameSuffix(fileName: string, suffix: string) {
  return ensurePdfExtension(fileName.replace(/\.pdf$/i, "") + suffix)
}
export function downloadBlob(blob: Blob, fileName: string) {
  const url = URL.createObjectURL(blob)
  const anchor = document.createElement("a")
  anchor.href = url
  anchor.download = fileName
  anchor.rel = "noopener"
  document.body.append(anchor)
  anchor.click()
  anchor.remove()
  window.setTimeout(() => URL.revokeObjectURL(url), 0)
}
export function downloadArrayBuffer(
  buffer: ArrayBuffer | Uint8Array,
  fileName: string,
  type = "application/pdf"
) {
  const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
  const copy = new ArrayBuffer(bytes.byteLength)
  new Uint8Array(copy).set(bytes)
  downloadBlob(new Blob([copy], { type }), fileName)
}
export function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
  const copy = new ArrayBuffer(bytes.byteLength)
  new Uint8Array(copy).set(bytes)
  return copy
}
export function readFileAsArrayBuffer(file: Blob) {
  return file.arrayBuffer()
}
export function formatBytes(bytes: number | undefined) {
  if (bytes === undefined || Number.isNaN(bytes)) return "—"
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
export function formatDateTime(date: Date | undefined | null) {
  if (!date) return "—"
  const value = date instanceof Date ? date : new Date(date)
  if (Number.isNaN(value.getTime())) return "—"
  return value.toLocaleString(undefined, {
    dateStyle: "medium",
    timeStyle: "short",
  })
}
export function formatRelativeDate(date: Date | undefined | null) {
  if (!date) return ""
  const value = date instanceof Date ? date : new Date(date)
  if (Number.isNaN(value.getTime())) return ""
  const diff = Date.now() - value.getTime()
  const minutes = Math.round(diff / 60000)
  if (minutes < 1) return "just now"
  if (minutes < 60) return `${minutes}m ago`
  const hours = Math.round(minutes / 60)
  if (hours < 24) return `${hours}h ago`
  return value.toLocaleDateString(undefined, {
    month: "short",
    day: "numeric",
  })
}
export function toNormalizedRect(
  rect: Rect,
  pageSize: Size
): PdfEditorNormalizedRect {
  const width = pageSize.width || 1
  const height = pageSize.height || 1
  return {
    top: (rect.origin.y / height) * 100,
    left: (rect.origin.x / width) * 100,
    width: (rect.size.width / width) * 100,
    height: (rect.size.height / height) * 100,
  }
}
export function isTypingTarget(target: EventTarget | null) {
  if (!(target instanceof HTMLElement)) return false
  if (target.isContentEditable) return true
  return Boolean(
    target.closest(
      "input, textarea, select, [contenteditable='true'], [contenteditable='']"
    )
  )
}
export function isMacPlatform() {
  if (typeof navigator === "undefined") return false
  return /mac|iphone|ipad|ipod/i.test(navigator.platform)
}
export function formatShortcut(shortcut: string) {
  const mac = isMacPlatform()
  return shortcut
    .replace(/mod/gi, mac ? "⌘" : "Ctrl")
    .replace(/shift/gi, mac ? "⇧" : "Shift")
    .replace(/alt/gi, mac ? "⌥" : "Alt")
}
export function uniqueName(base: string, existing: Iterable<string>) {
  const taken = new Set(existing)
  if (!taken.has(base)) return base
  let index = 2
  while (taken.has(`${base} ${index}`)) index += 1
  return `${base} ${index}`
}
export function noop() {}
/* -------------------------------------------------------------------------- */
/* Small UI primitives shared by the editor files                             */
/* -------------------------------------------------------------------------- */
export function PdfEditorTooltip({
  label,
  shortcut,
  children,
}: {
  label: React.ReactNode
  shortcut?: string
  children: React.ReactNode
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={<span className="inline-flex">{children}</span>}
      ></TooltipTrigger>
      <TooltipContent side="bottom">
        <span className="inline-flex items-center gap-2">
          {label}
          {shortcut ? (
            <Kbd className="h-4 min-w-4 px-1 text-[10px]">
              {formatShortcut(shortcut)}
            </Kbd>
          ) : null}
        </span>
      </TooltipContent>
    </Tooltip>
  )
}
export function PdfEditorToolButton({
  label,
  shortcut,
  active = false,
  className,
  children,
  size = "icon-sm",
  variant = "ghost",
  ...props
}: Omit<ButtonProps, "variant" | "size" | "children"> & {
  label: string
  shortcut?: string
  active?: boolean
  size?: ButtonProps["size"]
  variant?: ButtonProps["variant"]
  children: React.ReactNode
}) {
  return (
    <PdfEditorTooltip label={label} shortcut={shortcut}>
      <Button
        type="button"
        variant={variant}
        size={size}
        aria-label={label}
        aria-pressed={active}
        data-active={active ? "" : undefined}
        className={cn(
          active &&
            "bg-accent text-accent-foreground ring-1 ring-ring/40 ring-inset",
          className
        )}
        {...props}
      >
        {children}
      </Button>
    </PdfEditorTooltip>
  )
}
export function PdfEditorToolbarSeparator({
  className,
}: {
  className?: string
}) {
  return (
    <div
      aria-hidden="true"
      className={cn("mx-1 h-4 w-px shrink-0 self-center bg-border", className)}
    />
  )
}
export function PdfEditorPanelHeader({
  title,
  description,
  actions,
  onClose,
}: {
  title: React.ReactNode
  description?: React.ReactNode
  actions?: React.ReactNode
  onClose?: () => void
}) {
  return (
    <div className="flex shrink-0 items-start justify-between gap-2 border-b px-3 py-2.5">
      <div className="min-w-0">
        <div className="truncate text-sm font-medium text-foreground">
          {title}
        </div>
        {description ? (
          <div className="mt-0.5 text-xs text-muted-foreground">
            {description}
          </div>
        ) : null}
      </div>
      <div className="flex shrink-0 items-center gap-1">
        {actions}
        {onClose ? (
          <PdfEditorToolButton
            label="Close panel"
            size="icon-xs"
            onClick={onClose}
          >
            <CloseGlyph className="size-3.5" />
          </PdfEditorToolButton>
        ) : null}
      </div>
    </div>
  )
}
export function PdfEditorEmptyState({
  glyph: Glyph,
  title,
  description,
  action,
  className,
}: {
  glyph?: PdfEditorGlyph
  title: React.ReactNode
  description?: React.ReactNode
  action?: React.ReactNode
  className?: string
}) {
  return (
    <div
      className={cn(
        "flex flex-col items-center gap-2 px-4 py-10 text-center",
        className
      )}
    >
      {Glyph ? (
        <div className="grid size-10 place-items-center rounded-full bg-muted text-muted-foreground">
          <Glyph className="size-5" />
        </div>
      ) : null}
      <div className="text-sm font-medium text-foreground">{title}</div>
      {description ? (
        <div className="max-w-56 text-xs text-muted-foreground">
          {description}
        </div>
      ) : null}
      {action ? <div className="mt-2">{action}</div> : null}
    </div>
  )
}
export function PdfEditorSection({
  title,
  actions,
  children,
  className,
}: {
  title?: React.ReactNode
  actions?: React.ReactNode
  children: React.ReactNode
  className?: string
}) {
  return (
    <section className={cn("space-y-2", className)}>
      {title || actions ? (
        <div className="flex items-center justify-between gap-2">
          {title ? (
            <div className="text-xs font-medium text-muted-foreground">
              {title}
            </div>
          ) : (
            <span />
          )}
          {actions}
        </div>
      ) : null}
      {children}
    </section>
  )
}
export function PdfEditorFieldLabel({
  children,
  htmlFor,
  className,
}: {
  children: React.ReactNode
  htmlFor?: string
  className?: string
}) {
  return (
    <label
      htmlFor={htmlFor}
      className={cn("block text-xs font-medium text-foreground", className)}
    >
      {children}
    </label>
  )
}
export function PdfEditorCheckbox({
  checked,
  disabled,
  label,
  description,
  onCheckedChange,
  className,
}: {
  checked: boolean
  disabled?: boolean
  label: React.ReactNode
  description?: React.ReactNode
  onCheckedChange: (checked: boolean) => void
  className?: string
}) {
  return (
    <button
      type="button"
      role="checkbox"
      aria-checked={checked}
      disabled={disabled}
      onClick={() => onCheckedChange(!checked)}
      className={cn(
        "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",
        className
      )}
    >
      <span
        aria-hidden="true"
        className={cn(
          "mt-0.5 grid size-4 shrink-0 place-items-center rounded border transition-colors",
          checked
            ? "border-primary bg-primary text-primary-foreground"
            : "border-input bg-background"
        )}
      >
        {checked ? <CheckGlyph className="size-3" /> : null}
      </span>
      <span className="min-w-0">
        <span className="block leading-4">{label}</span>
        {description ? (
          <span className="block text-xs text-muted-foreground">
            {description}
          </span>
        ) : null}
      </span>
    </button>
  )
}
export function PdfEditorSwatch({
  color,
  active,
  label,
  onClick,
  size = "md",
}: {
  color: string
  active?: boolean
  label?: string
  onClick?: () => void
  size?: "sm" | "md"
}) {
  const isTransparent = color === "transparent"
  const accessibleLabel = label ?? (isTransparent ? "No fill" : color)
  const swatch = (
    <button
      type="button"
      aria-label={accessibleLabel}
      aria-pressed={active}
      onClick={onClick}
      className={cn(
        "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",
        size === "sm" ? "size-4" : "size-5",
        active && "ring-2 ring-ring ring-offset-2 ring-offset-background"
      )}
      style={
        isTransparent
          ? {
              backgroundImage:
                "linear-gradient(45deg, transparent 45%, #ef4444 45%, #ef4444 55%, transparent 55%)",
              backgroundColor: "#fff",
            }
          : { backgroundColor: color }
      }
    />
  )
  return label ? (
    <PdfEditorTooltip label={label}>{swatch}</PdfEditorTooltip>
  ) : (
    swatch
  )
}
/* -------------------------------------------------------------------------- */
/* Scroll area with a resolvable viewport (mirrors the PDF viewer)            */
/* -------------------------------------------------------------------------- */
export type PdfEditorScrollAreaViewportResolver = (
  container: HTMLDivElement
) => HTMLDivElement | null
const DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR =
  '[data-slot="scroll-area-viewport"]'
export function resolveDefaultScrollAreaViewport(container: HTMLDivElement) {
  return container.querySelector<HTMLDivElement>(
    DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR
  )
}
export const PdfEditorScrollAreaResolverContext =
  React.createContext<PdfEditorScrollAreaViewportResolver>(
    resolveDefaultScrollAreaViewport
  )
function setRefValue<T>(ref: React.Ref<T> | undefined, value: T | null) {
  if (!ref) return
  if (typeof ref === "function") {
    ref(value)
  } else {
    ref.current = value
  }
}
export function PdfEditorScrollArea({
  children,
  className,
  orientation = "vertical",
  scrollbarGutter = true,
  viewportClassName,
  viewportProps,
  viewportRef,
}: {
  children: React.ReactNode
  className?: string
  orientation?: "vertical" | "horizontal" | "both"
  scrollbarGutter?: boolean
  viewportClassName?: string
  viewportProps?: React.HTMLAttributes<HTMLDivElement>
  viewportRef?: React.Ref<HTMLDivElement>
}) {
  const resolveViewport = React.useContext(PdfEditorScrollAreaResolverContext)
  const containerRef = React.useRef<HTMLDivElement | null>(null)
  const { className: viewportPropsClassName, ...resolvedViewportProps } =
    viewportProps ?? {}
  const setViewportRef = React.useCallback(
    (viewport: HTMLDivElement | null) => {
      setRefValue(viewportRef, viewport)
    },
    [viewportRef]
  )
  React.useLayoutEffect(() => {
    const container = containerRef.current
    if (!container) return
    const viewport = resolveViewport(container)
    if (!viewport) {
      console.error(
        `PDFEditor could not resolve the scroll viewport. Add ${DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR} to your ScrollArea viewport or pass resolveScrollAreaViewport.`
      )
      return
    }
    setViewportRef(viewport)
    return () => setViewportRef(null)
  }, [resolveViewport, setViewportRef])
  return (
    <div
      ref={containerRef}
      data-slot="pdf-editor-scroll-area"
      className={cn("size-full min-h-0", className)}
    >
      <InlineScrollArea2
        className="size-full min-h-0"
        orientation={orientation}
        scrollbarGutter={scrollbarGutter}
      >
        <div
          {...resolvedViewportProps}
          data-slot="pdf-editor-scroll-content"
          className={cn(
            "min-h-full",
            viewportPropsClassName,
            viewportClassName
          )}
        >
          {children}
        </div>
      </InlineScrollArea2>
    </div>
  )
}

type ButtonProps = React.ComponentProps<typeof Button>
function InlineScrollArea2({
  className,
  children,
  orientation = "both",
  scrollFade = false,
  scrollbarGutter = false,
  scrollbarOverflowOnly = false,
  viewportClassName,
  viewportProps,
  viewportRef,
  ...props
}: InlineScrollAreaProps) {
  const {
    className: viewportPropsClassName,
    ref: viewportPropsRef,
    ...resolvedViewportProps
  } = viewportProps ?? {}
  const composedViewportRef = React.useMemo(
    () => InlineComposeRefs(viewportPropsRef, viewportRef),
    [viewportPropsRef, viewportRef]
  )

  if (
    !viewportProps &&
    !viewportRef &&
    !viewportClassName &&
    !scrollFade &&
    !scrollbarGutter &&
    !scrollbarOverflowOnly
  ) {
    return (
      <InlineScrollArea
        {...props}
        className={cn(
          "size-full min-h-0",
          orientation === "horizontal" &&
            "[&>[data-orientation=vertical]]:hidden",
          className
        )}
      >
        {children}
        {orientation !== "vertical" ? (
          <ScrollBar orientation="horizontal" />
        ) : null}
      </InlineScrollArea>
    )
  }

  return (
    <ScrollAreaPrimitive.Root
      className={cn(
        "size-full min-h-0",
        scrollbarOverflowOnly &&
          "[&: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",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.Viewport
        {...resolvedViewportProps}
        ref={composedViewportRef}
        className={cn(
          "h-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring",
          scrollFade &&
            "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]",
          scrollbarGutter && orientation !== "vertical" && "pb-3.5",
          scrollbarGutter && orientation !== "horizontal" && "pe-3.5",
          viewportPropsClassName,
          viewportClassName
        )}
        data-slot="scroll-area-viewport"
      >
        {children}
      </ScrollAreaPrimitive.Viewport>
      {orientation !== "horizontal" ? (
        <ScrollBar orientation="vertical" />
      ) : null}
      {orientation !== "vertical" ? (
        <ScrollBar orientation="horizontal" />
      ) : null}
      {orientation === "both" ? <ScrollAreaPrimitive.Corner /> : null}
    </ScrollAreaPrimitive.Root>
  )
}
type InlineScrollAreaProps = ScrollAreaPrimitive.Root.Props & {
  orientation?: "vertical" | "horizontal" | "both"
  scrollFade?: boolean
  scrollbarGutter?: boolean
  scrollbarOverflowOnly?: boolean
  viewportClassName?: string
  viewportProps?: ScrollAreaPrimitive.Viewport.Props
  viewportRef?: React.Ref<HTMLDivElement>
}
function InlineComposeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (!ref) continue
      if (typeof ref === "function") ref(node)
      else ref.current = node
    }
  }
}
components/extend/pdf-editor-dialogs.tsx
"use client"

import * as React from "react"
import type { DocumentState } from "@embedpdf/core"
import {
  useDocumentPermissions,
  useDocumentState,
  useRegistry,
} from "@embedpdf/core/react"
import {
  ignore,
  PdfActionType,
  PdfAnnotationBorderStyle,
  PdfAnnotationReplyType,
  PdfAnnotationSubtype,
  PdfBlendMode,
  PdfErrorCode,
  PdfPermissionFlag,
  PdfZoomMode,
  uuidV4,
  type PdfLinkAnnoObject,
  type PdfLinkTarget,
  type PdfMetadataObject,
  type PdfPrintOptions,
} from "@embedpdf/models"
import { useAnnotationCapability } from "@embedpdf/plugin-annotation/react"
import { useCapture } from "@embedpdf/plugin-capture/react"
import { useDocumentManagerCapability } from "@embedpdf/plugin-document-manager/react"
import { usePrint } from "@embedpdf/plugin-print/react"
import { useScroll } from "@embedpdf/plugin-scroll/react"
import { useSelectionCapability } from "@embedpdf/plugin-selection/react"
import {
  SignatureDrawPad,
  SignatureMode,
  SignatureTypePad,
  useSignatureCapability,
  useSignatureUpload,
  type SignatureDrawPadHandle,
  type SignatureFieldDefinition,
  type SignatureTypePadHandle,
} from "@embedpdf/plugin-signature/react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Kbd } from "@/components/ui/kbd"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { ColorPicker } from "@/components/extend/document-color-picker"
import {
  CopyGlyph,
  downloadBlob,
  DownloadGlyph,
  formatBytes,
  formatDateTime,
  formatShortcut,
  KeyGlyph,
  LockGlyph,
  PDF_EDITOR_SIGNATURE_FONTS,
  PdfEditorCheckbox,
  PdfEditorFieldLabel,
  PdfEditorSection,
  PdfEditorSwatch,
  ShieldGlyph,
  UnlockGlyph,
  usePdfEditor,
} from "@/components/extend/pdf-editor-shared"
import { IconPlaceholder } from "@/components/icon-placeholder"

/* -------------------------------------------------------------------------- */
/* Confirm                                                                    */
/* -------------------------------------------------------------------------- */
export function PdfEditorConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = "Confirm",
  destructive = false,
  onConfirm,
}: {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: string
  description?: string
  confirmLabel?: string
  destructive?: boolean
  onConfirm: () => void
}) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          {description ? (
            <DialogDescription>{description}</DialogDescription>
          ) : null}
        </DialogHeader>
        <DialogFooter>
          <Button
            type="button"
            variant="outline"
            onClick={() => onOpenChange(false)}
          >
            Cancel
          </Button>
          <Button
            type="button"
            variant={destructive ? "destructive" : "default"}
            onClick={() => {
              onConfirm()
              onOpenChange(false)
            }}
          >
            {confirmLabel}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Print                                                                      */
/* -------------------------------------------------------------------------- */
type PrintPageSelection = "all" | "current" | "custom"
export function PdfEditorPrintDialog({
  documentId,
  open,
  onOpenChange,
}: {
  documentId: string
  open: boolean
  onOpenChange: (open: boolean) => void
}) {
  const { provides: print } = usePrint(documentId)
  const { state: scrollState } = useScroll(documentId)
  const { notify } = usePdfEditor()
  const [selection, setSelection] = React.useState<PrintPageSelection>("all")
  const [customPages, setCustomPages] = React.useState("")
  const [includeAnnotations, setIncludeAnnotations] = React.useState(true)
  const [isPrinting, setIsPrinting] = React.useState(false)
  const [error, setError] = React.useState<string | null>(null)
  const [previousOpen, setPreviousOpen] = React.useState(open)
  if (previousOpen !== open) {
    setPreviousOpen(open)
    if (!open) {
      setSelection("all")
      setCustomPages("")
      setIncludeAnnotations(true)
      setIsPrinting(false)
      setError(null)
    }
  }
  const canSubmit =
    !isPrinting && (selection !== "custom" || customPages.trim().length > 0)
  const handlePrint = () => {
    if (!print || !canSubmit) return
    const options: PdfPrintOptions = {
      includeAnnotations,
      pageRange:
        selection === "current"
          ? String(scrollState.currentPage)
          : selection === "custom"
            ? customPages.trim()
            : undefined,
    }
    setIsPrinting(true)
    setError(null)
    print.print(options).wait(
      () => {
        setIsPrinting(false)
        onOpenChange(false)
      },
      (failure) => {
        setIsPrinting(false)
        const message =
          failure?.reason?.message ?? "The document could not be printed."
        setError(message)
        notify(message, "error")
      }
    )
  }
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>Print</DialogTitle>
          <DialogDescription>
            Prepares a print-ready copy and opens the browser print dialog.
          </DialogDescription>
        </DialogHeader>
        <InlineDialogPanel className="space-y-5">
          <PdfEditorSection title="Pages">
            <div className="space-y-1">
              <PdfEditorCheckbox
                checked={selection === "all"}
                onCheckedChange={() => setSelection("all")}
                label="All pages"
              />
              <PdfEditorCheckbox
                checked={selection === "current"}
                onCheckedChange={() => setSelection("current")}
                label={`Current page (${scrollState.currentPage || 1})`}
              />
              <PdfEditorCheckbox
                checked={selection === "custom"}
                onCheckedChange={() => setSelection("custom")}
                label="Page range"
              />
            </div>
            <Input
              placeholder="e.g. 1-3, 5, 8-10"
              value={customPages}
              disabled={selection !== "custom"}
              onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
                setCustomPages(event.target.value)
              }
              onFocus={() => setSelection("custom")}
            />
            <div className="text-xs text-muted-foreground">
              {scrollState.totalPages} pages in this document.
            </div>
          </PdfEditorSection>
          <PdfEditorSection title="Options">
            <PdfEditorCheckbox
              checked={includeAnnotations}
              onCheckedChange={setIncludeAnnotations}
              label="Include annotations and form values"
            />
          </PdfEditorSection>
          {error ? (
            <div className="text-xs text-destructive">{error}</div>
          ) : null}
        </InlineDialogPanel>
        <DialogFooter>
          <Button
            type="button"
            variant="outline"
            disabled={isPrinting}
            onClick={() => onOpenChange(false)}
          >
            Cancel
          </Button>
          <InlineLoadingButton
            type="button"
            loading={isPrinting}
            disabled={!canSubmit || !print}
            onClick={handlePrint}
          >
            Print
          </InlineLoadingButton>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Capture                                                                    */
/* -------------------------------------------------------------------------- */
type CaptureResult = {
  pageIndex: number
  blob: Blob
  imageType: string
  url: string
}
export function PdfEditorCaptureDialog({ documentId }: { documentId: string }) {
  const { provides: capture } = useCapture(documentId)
  const { fileName, notify, onCapture } = usePdfEditor()
  const [result, setResult] = React.useState<CaptureResult | null>(null)
  const onCaptureRef = React.useRef(onCapture)
  React.useEffect(() => {
    onCaptureRef.current = onCapture
  })
  React.useEffect(() => {
    if (!capture) return
    return capture.onCaptureArea((event) => {
      onCaptureRef.current?.(event)
      setResult((previous) => {
        if (previous) URL.revokeObjectURL(previous.url)
        return {
          pageIndex: event.pageIndex,
          blob: event.blob,
          imageType: event.imageType,
          url: URL.createObjectURL(event.blob),
        }
      })
    })
  }, [capture])
  React.useEffect(
    () => () => {
      if (result) URL.revokeObjectURL(result.url)
    },
    [result]
  )
  const close = () => setResult(null)
  const extension = result?.imageType.split("/")[1] ?? "png"
  const captureFileName = `${fileName.replace(/\.pdf$/i, "")}-page-${(result?.pageIndex ?? 0) + 1}.${extension}`
  const canCopy =
    typeof navigator !== "undefined" &&
    typeof window !== "undefined" &&
    "ClipboardItem" in window &&
    Boolean(navigator.clipboard?.write)
  return (
    <Dialog
      open={result !== null}
      onOpenChange={(open) => {
        if (!open) close()
      }}
    >
      <DialogContent className="max-w-2xl">
        <DialogHeader>
          <DialogTitle>Captured area</DialogTitle>
          <DialogDescription>
            Page {(result?.pageIndex ?? 0) + 1} ·{" "}
            {formatBytes(result?.blob.size)}
          </DialogDescription>
        </DialogHeader>
        <InlineDialogPanel>
          <div className="grid place-items-center rounded-lg border bg-muted/40 p-3">
            {result ? (
              <img
                src={result.url}
                alt={`Captured area from page ${result.pageIndex + 1}`}
                className="max-h-[52vh] max-w-full rounded-md shadow-sm"
              />
            ) : null}
          </div>
        </InlineDialogPanel>
        <DialogFooter>
          {canCopy ? (
            <Button
              type="button"
              variant="outline"
              onClick={async () => {
                if (!result) return
                try {
                  await navigator.clipboard.write([
                    new ClipboardItem({ [result.imageType]: result.blob }),
                  ])
                  notify("Image copied to the clipboard.", "success")
                } catch {
                  notify("The image could not be copied.", "error")
                }
              }}
            >
              <CopyGlyph className="size-4" />
              Copy image
            </Button>
          ) : null}
          <Button
            type="button"
            onClick={() => {
              if (!result) return
              downloadBlob(result.blob, captureFileName)
              close()
            }}
          >
            <DownloadGlyph className="size-4" />
            Download
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Security                                                                   */
/* -------------------------------------------------------------------------- */
const PERMISSION_OPTIONS: Array<{
  flag: PdfPermissionFlag
  label: string
  description: string
}> = [
  {
    flag: PdfPermissionFlag.Print,
    label: "Print",
    description: "Allow printing, possibly at reduced quality.",
  },
  {
    flag: PdfPermissionFlag.PrintHighQuality,
    label: "Print in high quality",
    description: "Allow full-resolution printing.",
  },
  {
    flag: PdfPermissionFlag.ModifyContents,
    label: "Modify contents",
    description: "Allow editing the page content.",
  },
  {
    flag: PdfPermissionFlag.CopyContents,
    label: "Copy text and images",
    description: "Allow copying and extracting content.",
  },
  {
    flag: PdfPermissionFlag.ModifyAnnotations,
    label: "Annotate",
    description: "Allow adding and changing annotations.",
  },
  {
    flag: PdfPermissionFlag.FillForms,
    label: "Fill forms",
    description: "Allow filling in existing form fields.",
  },
  {
    flag: PdfPermissionFlag.ExtractForAccessibility,
    label: "Extract for accessibility",
    description: "Allow assistive technologies to read the content.",
  },
  {
    flag: PdfPermissionFlag.AssembleDocument,
    label: "Assemble pages",
    description: "Allow inserting, rotating, and deleting pages.",
  },
]
function PasswordInput({
  id,
  value,
  onChange,
  placeholder,
  autoFocus,
  onEnter,
}: {
  id?: string
  value: string
  onChange: (value: string) => void
  placeholder?: string
  autoFocus?: boolean
  onEnter?: () => void
}) {
  return (
    <Input
      id={id}
      type="password"
      autoComplete="off"
      value={value}
      placeholder={placeholder}
      autoFocus={autoFocus}
      onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
        onChange(event.target.value)
      }
      onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
        if (event.key === "Enter") onEnter?.()
      }}
    />
  )
}
export function PdfEditorSecurityDialog({
  documentId,
  open,
  onOpenChange,
}: {
  documentId: string
  open: boolean
  onOpenChange: (open: boolean) => void
}) {
  const { provides: documentManager } = useDocumentManagerCapability()
  const documentState = useDocumentState(documentId)
  const permissions = useDocumentPermissions(documentId)
  const { notify } = usePdfEditor()
  const document = documentState?.document ?? null
  const isEncrypted = document?.isEncrypted ?? false
  const isOwnerUnlocked = document?.isOwnerUnlocked ?? false
  const needsOwnerUnlock = isEncrypted && !isOwnerUnlocked
  const [unlockPassword, setUnlockPassword] = React.useState("")
  const [isUnlocking, setIsUnlocking] = React.useState(false)
  const [requireOpenPassword, setRequireOpenPassword] = React.useState(false)
  const [openPassword, setOpenPassword] = React.useState("")
  const [confirmOpenPassword, setConfirmOpenPassword] = React.useState("")
  const [restrictPermissions, setRestrictPermissions] = React.useState(false)
  const [ownerPassword, setOwnerPassword] = React.useState("")
  const [confirmOwnerPassword, setConfirmOwnerPassword] = React.useState("")
  const [allowedFlags, setAllowedFlags] = React.useState<
    Set<PdfPermissionFlag>
  >(() => new Set(PERMISSION_OPTIONS.map((option) => option.flag)))
  const [isApplying, setIsApplying] = React.useState(false)
  const [error, setError] = React.useState<string | null>(null)
  const [previousOpen, setPreviousOpen] = React.useState(open)
  if (previousOpen !== open) {
    setPreviousOpen(open)
    if (open) {
      setUnlockPassword("")
      setIsUnlocking(false)
      setRequireOpenPassword(false)
      setOpenPassword("")
      setConfirmOpenPassword("")
      setRestrictPermissions(false)
      setOwnerPassword("")
      setConfirmOwnerPassword("")
      setAllowedFlags(new Set(PERMISSION_OPTIONS.map((option) => option.flag)))
      setIsApplying(false)
      setError(null)
    }
  }
  const openPasswordValid =
    !requireOpenPassword ||
    (openPassword.length > 0 && openPassword === confirmOpenPassword)
  const ownerPasswordValid =
    !restrictPermissions ||
    (ownerPassword.length > 0 && ownerPassword === confirmOwnerPassword)
  const canApply =
    !isApplying &&
    (requireOpenPassword || restrictPermissions) &&
    openPasswordValid &&
    ownerPasswordValid
  const handleUnlock = () => {
    if (!documentManager || !unlockPassword.trim()) return
    setIsUnlocking(true)
    setError(null)
    documentManager.unlockOwnerPermissions(documentId, unlockPassword).wait(
      (success) => {
        setIsUnlocking(false)
        if (success) {
          setUnlockPassword("")
          notify("Owner permissions unlocked.", "success")
        } else {
          setError("The owner password is incorrect.")
        }
      },
      (failure) => {
        setIsUnlocking(false)
        setError(
          failure?.reason?.message ?? "The document could not be unlocked."
        )
      }
    )
  }
  const handleApply = () => {
    if (!documentManager || !canApply) return
    let flags = PdfPermissionFlag.AllowAll as number
    if (restrictPermissions) {
      flags = 0
      for (const flag of allowedFlags) flags |= flag
    }
    const userPassword = requireOpenPassword ? openPassword : ""
    const resolvedOwnerPassword = restrictPermissions
      ? ownerPassword
      : requireOpenPassword
        ? openPassword
        : ""
    setIsApplying(true)
    setError(null)
    documentManager
      .setDocumentEncryption(documentId, {
        userPassword,
        ownerPassword: resolvedOwnerPassword,
        allowedFlags: flags,
      })
      .wait(
        (success) => {
          setIsApplying(false)
          if (success) {
            notify(
              "Protection is applied the next time you download the document.",
              "success"
            )
            onOpenChange(false)
          } else {
            setError("The protection settings could not be applied.")
          }
        },
        (failure) => {
          setIsApplying(false)
          setError(
            failure?.reason?.message ??
              "The protection settings could not be applied."
          )
        }
      )
  }
  const handleRemove = () => {
    if (!documentManager) return
    setIsApplying(true)
    setError(null)
    documentManager.removeEncryption(documentId).wait(
      (success) => {
        setIsApplying(false)
        if (success) {
          notify(
            "Protection is removed the next time you download the document.",
            "success"
          )
          onOpenChange(false)
        } else {
          setError("The protection could not be removed.")
        }
      },
      (failure) => {
        setIsApplying(false)
        setError(
          failure?.reason?.message ?? "The protection could not be removed."
        )
      }
    )
  }
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-lg">
        <DialogHeader>
          <DialogTitle>Security</DialogTitle>
          <DialogDescription>
            Passwords and permissions are written into the PDF when you download
            it.
          </DialogDescription>
        </DialogHeader>
        <InlineDialogPanel className="space-y-5">
          <PdfEditorSection title="Current status">
            <div className="flex flex-wrap items-center gap-2 text-sm">
              {isEncrypted ? (
                <Badge variant="secondary">
                  <LockGlyph className="size-3" />
                  Encrypted
                </Badge>
              ) : (
                <Badge variant="outline">
                  <UnlockGlyph className="size-3" />
                  Not encrypted
                </Badge>
              )}
              {isEncrypted ? (
                <Badge variant={isOwnerUnlocked ? "secondary" : "outline"}>
                  <KeyGlyph className="size-3" />
                  {isOwnerUnlocked ? "Owner unlocked" : "Owner locked"}
                </Badge>
              ) : null}
            </div>
            <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground">
              {PERMISSION_OPTIONS.map((option) => {
                const allowed = permissions.hasPermission(option.flag)
                return (
                  <div key={option.flag} className="flex items-center gap-1.5">
                    <span
                      aria-hidden="true"
                      className={cn(
                        "size-1.5 rounded-full",
                        allowed ? "bg-emerald-500" : "bg-destructive"
                      )}
                    />
                    <span className={cn(!allowed && "line-through")}>
                      {option.label}
                    </span>
                  </div>
                )
              })}
            </div>
          </PdfEditorSection>

          {needsOwnerUnlock ? (
            <PdfEditorSection title="Unlock owner permissions">
              <div className="text-xs text-muted-foreground">
                Enter the owner password to change protection settings.
              </div>
              <div className="flex gap-2">
                <PasswordInput
                  value={unlockPassword}
                  placeholder="Owner password"
                  onChange={setUnlockPassword}
                  onEnter={handleUnlock}
                />
                <InlineLoadingButton
                  type="button"
                  variant="outline"
                  loading={isUnlocking}
                  disabled={!unlockPassword.trim()}
                  onClick={handleUnlock}
                >
                  Unlock
                </InlineLoadingButton>
              </div>
            </PdfEditorSection>
          ) : null}

          <PdfEditorSection title="Open password">
            <PdfEditorCheckbox
              checked={requireOpenPassword}
              disabled={needsOwnerUnlock}
              onCheckedChange={setRequireOpenPassword}
              label="Require a password to open the document"
            />
            {requireOpenPassword ? (
              <div className="grid gap-2 sm:grid-cols-2">
                <PasswordInput
                  value={openPassword}
                  placeholder="Password"
                  onChange={setOpenPassword}
                />
                <PasswordInput
                  value={confirmOpenPassword}
                  placeholder="Confirm password"
                  onChange={setConfirmOpenPassword}
                />
                {!openPasswordValid && confirmOpenPassword ? (
                  <div className="text-xs text-destructive sm:col-span-2">
                    The passwords do not match.
                  </div>
                ) : null}
              </div>
            ) : null}
          </PdfEditorSection>

          <PdfEditorSection title="Permissions">
            <PdfEditorCheckbox
              checked={restrictPermissions}
              disabled={needsOwnerUnlock}
              onCheckedChange={setRestrictPermissions}
              label="Restrict what readers can do"
              description="Requires an owner password to lift the restrictions later."
            />
            {restrictPermissions ? (
              <div className="space-y-3">
                <div className="grid gap-2 sm:grid-cols-2">
                  <PasswordInput
                    value={ownerPassword}
                    placeholder="Owner password"
                    onChange={setOwnerPassword}
                  />
                  <PasswordInput
                    value={confirmOwnerPassword}
                    placeholder="Confirm owner password"
                    onChange={setConfirmOwnerPassword}
                  />
                  {!ownerPasswordValid && confirmOwnerPassword ? (
                    <div className="text-xs text-destructive sm:col-span-2">
                      The passwords do not match.
                    </div>
                  ) : null}
                </div>
                <div className="grid gap-1 sm:grid-cols-2">
                  {PERMISSION_OPTIONS.map((option) => (
                    <PdfEditorCheckbox
                      key={option.flag}
                      checked={allowedFlags.has(option.flag)}
                      onCheckedChange={(checked) =>
                        setAllowedFlags((previous) => {
                          const next = new Set(previous)
                          if (checked) {
                            next.add(option.flag)
                          } else {
                            next.delete(option.flag)
                          }
                          return next
                        })
                      }
                      label={option.label}
                      description={option.description}
                    />
                  ))}
                </div>
              </div>
            ) : null}
          </PdfEditorSection>

          {error ? (
            <div className="text-xs text-destructive">{error}</div>
          ) : null}
        </InlineDialogPanel>
        <DialogFooter>
          {isEncrypted && !needsOwnerUnlock ? (
            <Button
              type="button"
              disabled={isApplying}
              onClick={handleRemove}
              variant={"outline"}
              className={cn(
                "border-destructive/32 text-destructive hover:bg-destructive/10",
                "sm:mr-auto"
              )}
            >
              Remove protection
            </Button>
          ) : null}
          <Button
            type="button"
            variant="outline"
            disabled={isApplying}
            onClick={() => onOpenChange(false)}
          >
            Cancel
          </Button>
          <InlineLoadingButton
            type="button"
            loading={isApplying}
            disabled={!canApply || needsOwnerUnlock}
            onClick={handleApply}
          >
            <ShieldGlyph className="size-4" />
            Apply protection
          </InlineLoadingButton>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Document properties (metadata)                                             */
/* -------------------------------------------------------------------------- */
type EditableMetadata = {
  title: string
  author: string
  subject: string
  keywords: string
  creator: string
  producer: string
}
const EMPTY_METADATA: EditableMetadata = {
  title: "",
  author: "",
  subject: "",
  keywords: "",
  creator: "",
  producer: "",
}
export function PdfEditorPropertiesDialog({
  documentId,
  open,
  onOpenChange,
}: {
  documentId: string
  open: boolean
  onOpenChange: (open: boolean) => void
}) {
  const { registry } = useRegistry()
  const documentState = useDocumentState(documentId)
  const permissions = useDocumentPermissions(documentId)
  const { fileName, notify } = usePdfEditor()
  const document = documentState?.document ?? null
  const [metadata, setMetadata] =
    React.useState<EditableMetadata>(EMPTY_METADATA)
  const [dates, setDates] = React.useState<{
    creationDate: Date | null
    modificationDate: Date | null
  }>({ creationDate: null, modificationDate: null })
  const [isLoading, setIsLoading] = React.useState(
    Boolean(open && registry && document)
  )
  const [isSaving, setIsSaving] = React.useState(false)
  const [error, setError] = React.useState<string | null>(null)
  const [metadataRequest, setMetadataRequest] = React.useState({
    document,
    open,
    registry,
  })
  if (
    metadataRequest.document !== document ||
    metadataRequest.open !== open ||
    metadataRequest.registry !== registry
  ) {
    setMetadataRequest({ document, open, registry })
    setIsLoading(Boolean(open && registry && document))
    setError(null)
  }
  React.useEffect(() => {
    if (!open || !registry || !document) return
    let cancelled = false
    registry
      .getEngine()
      .getMetadata(document)
      .wait(
        (result: PdfMetadataObject) => {
          if (cancelled) return
          setMetadata({
            title: result.title ?? "",
            author: result.author ?? "",
            subject: result.subject ?? "",
            keywords: result.keywords ?? "",
            creator: result.creator ?? "",
            producer: result.producer ?? "",
          })
          setDates({
            creationDate: result.creationDate ?? null,
            modificationDate: result.modificationDate ?? null,
          })
          setIsLoading(false)
        },
        (failure) => {
          if (cancelled) return
          setError(
            failure?.reason?.message ??
              "The document metadata could not be read."
          )
          setIsLoading(false)
        }
      )
    return () => {
      cancelled = true
    }
  }, [document, open, registry])
  const updateField = (key: keyof EditableMetadata, value: string) =>
    setMetadata((previous) => ({ ...previous, [key]: value }))
  const handleSave = () => {
    if (!registry || !document) return
    setIsSaving(true)
    setError(null)
    registry
      .getEngine()
      .setMetadata(document, {
        title: metadata.title,
        author: metadata.author,
        subject: metadata.subject,
        keywords: metadata.keywords,
        creator: metadata.creator,
        producer: metadata.producer,
        modificationDate: new Date(),
      })
      .wait(
        () => {
          setIsSaving(false)
          notify("Document properties saved.", "success")
          onOpenChange(false)
        },
        (failure) => {
          setIsSaving(false)
          setError(
            failure?.reason?.message ??
              "The document metadata could not be saved."
          )
        }
      )
  }
  const firstPage = document?.pages[0]
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-lg">
        <DialogHeader>
          <DialogTitle>Document properties</DialogTitle>
          <DialogDescription>{fileName}</DialogDescription>
        </DialogHeader>
        <InlineDialogPanel className="space-y-5">
          {isLoading ? (
            <div className="grid place-items-center py-8">
              <InlineSpinner className="size-4" />
            </div>
          ) : (
            <>
              <PdfEditorSection title="Description">
                <div className="grid gap-3">
                  {(
                    [
                      ["title", "Title"],
                      ["author", "Author"],
                      ["subject", "Subject"],
                      ["keywords", "Keywords"],
                      ["creator", "Creator"],
                      ["producer", "Producer"],
                    ] as Array<[keyof EditableMetadata, string]>
                  ).map(([key, label]) => (
                    <div key={key} className="grid gap-1">
                      <PdfEditorFieldLabel htmlFor={`pdf-editor-meta-${key}`}>
                        {label}
                      </PdfEditorFieldLabel>
                      <Input
                        id={`pdf-editor-meta-${key}`}
                        value={metadata[key]}
                        disabled={!permissions.canModifyContents}
                        onChange={(
                          event: React.ChangeEvent<HTMLInputElement>
                        ) => updateField(key, event.target.value)}
                        className={cn("h-8 px-2.5")}
                      />
                    </div>
                  ))}
                </div>
              </PdfEditorSection>
              <PdfEditorSection title="Details">
                <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs">
                  <dt className="text-muted-foreground">Pages</dt>
                  <dd>{document?.pageCount ?? "—"}</dd>
                  <dt className="text-muted-foreground">Page size</dt>
                  <dd>
                    {firstPage
                      ? `${Math.round(firstPage.size.width)} × ${Math.round(firstPage.size.height)} pt`
                      : "—"}
                  </dd>
                  <dt className="text-muted-foreground">Created</dt>
                  <dd>{formatDateTime(dates.creationDate)}</dd>
                  <dt className="text-muted-foreground">Modified</dt>
                  <dd>{formatDateTime(dates.modificationDate)}</dd>
                  <dt className="text-muted-foreground">Security</dt>
                  <dd>
                    {document?.isEncrypted
                      ? "Password protected"
                      : "No protection"}
                  </dd>
                </dl>
              </PdfEditorSection>
              {error ? (
                <div className="text-xs text-destructive">{error}</div>
              ) : null}
            </>
          )}
        </InlineDialogPanel>
        <DialogFooter>
          <Button
            type="button"
            variant="outline"
            disabled={isSaving}
            onClick={() => onOpenChange(false)}
          >
            Cancel
          </Button>
          <InlineLoadingButton
            type="button"
            loading={isSaving}
            disabled={isLoading || !permissions.canModifyContents}
            onClick={handleSave}
          >
            Save
          </InlineLoadingButton>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Signature creation                                                         */
/* -------------------------------------------------------------------------- */
type SignatureCreationTab = "draw" | "type" | "upload"
type SignatureTarget = "signature" | "initials"
const SIGNATURE_COLORS = [
  { name: "Black", value: "#111827" },
  { name: "Blue", value: "#1d4ed8" },
  { name: "Red", value: "#dc2626" },
]
function useSignatureFontsStylesheet(url: string | null, enabled: boolean) {
  React.useEffect(() => {
    if (!enabled || !url || typeof document === "undefined") return
    const existing = document.querySelector<HTMLLinkElement>(
      `link[data-pdf-editor-signature-fonts="${CSS.escape(url)}"]`
    )
    if (existing) return
    const link = document.createElement("link")
    link.rel = "stylesheet"
    link.href = url
    link.dataset.pdfEditorSignatureFonts = url
    document.head.append(link)
  }, [enabled, url])
}
type SignatureUploadZoneProps = {
  inputRef: ReturnType<typeof useSignatureUpload>["inputRef"]
  accept: string
  isDragging: boolean
  previewUrl: string | null
  placeholder: string
  onOpen: () => void
  onFileInputChange: (event: Event) => void
  onDrop: (event: DragEvent) => void
  onDragOver: (event: DragEvent) => void
  onDragLeave: () => void
}
function SignatureUploadZone({
  inputRef,
  accept,
  isDragging,
  previewUrl,
  placeholder,
  onOpen,
  onFileInputChange,
  onDrop,
  onDragOver,
  onDragLeave,
}: SignatureUploadZoneProps) {
  return (
    <>
      <input
        ref={inputRef}
        type="file"
        accept={accept}
        className="sr-only"
        tabIndex={-1}
        onChange={(event) => onFileInputChange(event.nativeEvent)}
      />
      <button
        type="button"
        onClick={onOpen}
        onDrop={(event) => {
          event.preventDefault()
          onDrop(event.nativeEvent)
        }}
        onDragOver={(event) => {
          event.preventDefault()
          onDragOver(event.nativeEvent)
        }}
        onDragLeave={() => onDragLeave()}
        className={cn(
          "flex h-full w-full items-center justify-center rounded-md border-2 border-dashed transition-colors",
          isDragging
            ? "border-primary bg-primary/5"
            : "border-input hover:border-muted-foreground/60"
        )}
      >
        {previewUrl ? (
          <img
            src={previewUrl}
            alt="Uploaded signature"
            className="max-h-[90%] max-w-[90%] object-contain"
          />
        ) : (
          <span className="px-4 text-center text-xs text-muted-foreground">
            {placeholder}
          </span>
        )}
      </button>
    </>
  )
}
function SignaturePad({
  tab,
  color,
  fontFamily,
  placeholder,
  uploadPlaceholder,
  drawRef,
  typeRef,
  upload,
  onResult,
}: {
  tab: SignatureCreationTab
  color: string
  fontFamily: string
  placeholder: string
  uploadPlaceholder: string
  drawRef: React.MutableRefObject<SignatureDrawPadHandle | null>
  typeRef: React.MutableRefObject<SignatureTypePadHandle | null>
  upload: ReturnType<typeof useSignatureUpload>
  onResult: (result: SignatureFieldDefinition | null) => void
}) {
  return (
    <div className="h-full w-full">
      {tab === "draw" ? (
        <SignatureDrawPad
          onResult={onResult}
          padRef={(handle) => {
            drawRef.current = handle
          }}
          strokeColor={color}
          strokeWidth={3}
          className="h-full w-full rounded-md border border-input bg-white"
        />
      ) : null}
      {tab === "type" ? (
        <SignatureTypePad
          onResult={onResult}
          padRef={(handle) => {
            typeRef.current = handle
          }}
          fontFamily={fontFamily}
          fontSize={40}
          color={color}
          placeholder={placeholder}
          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"
        />
      ) : null}
      {tab === "upload" ? (
        <SignatureUploadZone
          inputRef={upload.inputRef}
          accept={upload.accept}
          isDragging={upload.isDragging}
          previewUrl={upload.previewUrl}
          placeholder={uploadPlaceholder}
          onOpen={upload.openFilePicker}
          onFileInputChange={upload.handleFileInputChange}
          onDrop={upload.handleDrop}
          onDragOver={upload.handleDragOver}
          onDragLeave={upload.handleDragLeave}
        />
      ) : null}
    </div>
  )
}
export function PdfEditorSignatureDialog({
  open,
  onOpenChange,
}: {
  open: boolean
  onOpenChange: (open: boolean) => void
}) {
  const { provides: signatureCapability } = useSignatureCapability()
  const { notify, signatureFontsStylesheetUrl } = usePdfEditor()
  const supportsInitials =
    (signatureCapability?.mode ?? SignatureMode.SignatureOnly) ===
    SignatureMode.SignatureAndInitials
  const [tab, setTab] = React.useState<SignatureCreationTab>("draw")
  const [color, setColor] = React.useState(SIGNATURE_COLORS[0].value)
  const [fontFamily, setFontFamily] = React.useState(
    PDF_EDITOR_SIGNATURE_FONTS[0].family
  )
  const [signature, setSignature] =
    React.useState<SignatureFieldDefinition | null>(null)
  const [initials, setInitials] =
    React.useState<SignatureFieldDefinition | null>(null)
  const [activeTarget, setActiveTarget] =
    React.useState<SignatureTarget>("signature")
  const signatureDrawRef = React.useRef<SignatureDrawPadHandle | null>(null)
  const initialsDrawRef = React.useRef<SignatureDrawPadHandle | null>(null)
  const signatureTypeRef = React.useRef<SignatureTypePadHandle | null>(null)
  const initialsTypeRef = React.useRef<SignatureTypePadHandle | null>(null)
  useSignatureFontsStylesheet(signatureFontsStylesheetUrl, open)
  const signatureUpload = useSignatureUpload({ onResult: setSignature })
  const initialsUpload = useSignatureUpload({ onResult: setInitials })
  const clearSignatureUpload = signatureUpload.clear
  const clearInitialsUpload = initialsUpload.clear
  const clearAll = React.useCallback(() => {
    signatureDrawRef.current?.clear()
    initialsDrawRef.current?.clear()
    signatureTypeRef.current?.clear()
    initialsTypeRef.current?.clear()
    clearSignatureUpload()
    clearInitialsUpload()
    setSignature(null)
    setInitials(null)
  }, [clearInitialsUpload, clearSignatureUpload])
  const [previousOpen, setPreviousOpen] = React.useState(open)
  if (previousOpen !== open) {
    setPreviousOpen(open)
    if (!open) {
      setTab("draw")
      setColor(SIGNATURE_COLORS[0].value)
      setFontFamily(PDF_EDITOR_SIGNATURE_FONTS[0].family)
      setActiveTarget("signature")
      setSignature(null)
      setInitials(null)
    }
  }
  const handleTabChange = (nextTab: SignatureCreationTab) => {
    clearAll()
    setTab(nextTab)
  }
  const canSave = Boolean(signature || initials)
  const handleSave = () => {
    const entrySignature = signature ?? initials
    if (!entrySignature || !signatureCapability) return
    signatureCapability.addEntry({
      signature: entrySignature,
      ...(initials ? { initials } : {}),
    })
    notify(signature ? "Signature saved." : "Initials saved.", "success")
    onOpenChange(false)
  }
  const tabs: Array<{
    id: SignatureCreationTab
    label: string
  }> = [
    { id: "draw", label: "Draw" },
    { id: "type", label: "Type" },
    { id: "upload", label: "Upload" },
  ]
  const fontOptions = PDF_EDITOR_SIGNATURE_FONTS.map((font) => ({
    label: font.name,
    value: font.family,
  }))
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-2xl">
        <DialogHeader>
          <DialogTitle>Create signature</DialogTitle>
          <DialogDescription>
            Saved signatures can be placed anywhere in the document.
          </DialogDescription>
        </DialogHeader>
        <InlineDialogPanel className="space-y-4">
          <div className="flex items-center gap-1 rounded-lg bg-muted p-0.5">
            {tabs.map((item) => (
              <button
                key={item.id}
                type="button"
                onClick={() => handleTabChange(item.id)}
                className={cn(
                  "flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
                  tab === item.id
                    ? "bg-background text-foreground shadow-sm"
                    : "text-muted-foreground hover:text-foreground"
                )}
              >
                {item.label}
              </button>
            ))}
          </div>

          <div className="space-y-2">
            <div className="flex items-center justify-between gap-3">
              {supportsInitials ? (
                <div className="flex items-center gap-1 rounded-lg bg-muted p-0.5">
                  {(["signature", "initials"] as const).map((target) => (
                    <button
                      key={target}
                      type="button"
                      aria-pressed={activeTarget === target}
                      onClick={() => setActiveTarget(target)}
                      className={cn(
                        "rounded-md px-3 py-1 text-xs font-medium transition-colors",
                        activeTarget === target
                          ? "bg-background text-foreground shadow-sm"
                          : "text-muted-foreground hover:text-foreground"
                      )}
                    >
                      {target === "signature" ? "Signature" : "Initials"}
                    </button>
                  ))}
                </div>
              ) : (
                <PdfEditorFieldLabel>Signature</PdfEditorFieldLabel>
              )}
              <Button
                type="button"
                variant="ghost"
                size="xs"
                onClick={() => {
                  if (activeTarget === "signature") {
                    if (tab === "draw") signatureDrawRef.current?.clear()
                    else if (tab === "type") signatureTypeRef.current?.clear()
                    else clearSignatureUpload()
                    setSignature(null)
                  } else {
                    if (tab === "draw") initialsDrawRef.current?.clear()
                    else if (tab === "type") initialsTypeRef.current?.clear()
                    else clearInitialsUpload()
                    setInitials(null)
                  }
                }}
              >
                Clear
              </Button>
            </div>
            <div
              className={cn(
                "h-36 w-full",
                activeTarget !== "signature" && "hidden"
              )}
            >
              <SignaturePad
                tab={tab}
                color={color}
                fontFamily={fontFamily}
                placeholder="Type your name"
                uploadPlaceholder="Click or drop an image of your signature"
                drawRef={signatureDrawRef}
                typeRef={signatureTypeRef}
                upload={signatureUpload}
                onResult={setSignature}
              />
            </div>
            {supportsInitials ? (
              <div
                className={cn(
                  "w-full",
                  activeTarget !== "initials" && "hidden"
                )}
              >
                <div className="aspect-[20/7] h-36 max-w-full">
                  <SignaturePad
                    tab={tab}
                    color={color}
                    fontFamily={fontFamily}
                    placeholder="Initials"
                    uploadPlaceholder="Click or drop an image"
                    drawRef={initialsDrawRef}
                    typeRef={initialsTypeRef}
                    upload={initialsUpload}
                    onResult={setInitials}
                  />
                </div>
              </div>
            ) : null}
          </div>

          <div className="flex flex-wrap items-center gap-4">
            {tab !== "upload" ? (
              <div className="flex items-center gap-2">
                <span className="text-xs text-muted-foreground">Color</span>
                <div className="flex items-center gap-1.5">
                  {SIGNATURE_COLORS.map((option) => (
                    <PdfEditorSwatch
                      key={option.value}
                      color={option.value}
                      label={option.name}
                      active={color === option.value}
                      onClick={() => setColor(option.value)}
                    />
                  ))}
                  <ColorPicker
                    label="Custom signature color"
                    rainbowTrigger
                    color={color}
                    onChange={setColor}
                  />
                </div>
              </div>
            ) : null}
            {tab === "type" ? (
              <div className="flex items-center gap-2">
                <span className="text-xs text-muted-foreground">Font</span>
                <Select
                  items={fontOptions}
                  value={fontFamily}
                  onValueChange={(value) => setFontFamily(String(value))}
                >
                  <SelectTrigger size="sm" className="w-40">
                    <SelectValue placeholder="Font" />
                  </SelectTrigger>
                  <SelectContent alignItemWithTrigger={false}>
                    {fontOptions.map((option) => (
                      <SelectItem key={option.value} value={option.value}>
                        <span style={{ fontFamily: option.value }}>
                          {option.label}
                        </span>
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
            ) : null}
          </div>
        </InlineDialogPanel>
        <DialogFooter>
          <Button
            type="button"
            variant="outline"
            onClick={() => onOpenChange(false)}
          >
            Cancel
          </Button>
          <Button type="button" disabled={!canSave} onClick={handleSave}>
            Save
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Links                                                                      */
/* -------------------------------------------------------------------------- */
type LinkTab = "url" | "page"
function readLinkTarget(target: PdfLinkTarget | undefined): {
  tab: LinkTab
  url: string
  pageNumber: number | null
} {
  if (!target) return { tab: "url", url: "", pageNumber: null }
  if (target.type === "action") {
    if (target.action.type === PdfActionType.URI) {
      return { tab: "url", url: target.action.uri, pageNumber: null }
    }
    if (
      target.action.type === PdfActionType.Goto ||
      target.action.type === PdfActionType.RemoteGoto
    ) {
      return {
        tab: "page",
        url: "",
        pageNumber: target.action.destination.pageIndex + 1,
      }
    }
    return { tab: "url", url: "", pageNumber: null }
  }
  return {
    tab: "page",
    url: "",
    pageNumber: target.destination.pageIndex + 1,
  }
}
export function PdfEditorLinkDialog({
  documentId,
  open,
  onOpenChange,
  source,
}: {
  documentId: string
  open: boolean
  onOpenChange: (open: boolean) => void
  source: "annotation" | "selection"
}) {
  const { provides: annotationCapability } = useAnnotationCapability()
  const { provides: selectionCapability } = useSelectionCapability()
  const { state: scrollState } = useScroll(documentId)
  const { notify } = usePdfEditor()
  const [tab, setTab] = React.useState<LinkTab>("url")
  const [url, setUrl] = React.useState("")
  const [pageNumber, setPageNumber] = React.useState(1)
  const totalPages = Math.max(scrollState.totalPages, 1)
  const annotationScope = React.useMemo(
    () => annotationCapability?.forDocument(documentId) ?? null,
    [annotationCapability, documentId]
  )
  const selectionScope = React.useMemo(
    () => selectionCapability?.forDocument(documentId) ?? null,
    [documentId, selectionCapability]
  )
  const [linkRequest, setLinkRequest] = React.useState({
    annotationScope,
    open: false,
    source,
  })
  if (
    linkRequest.annotationScope !== annotationScope ||
    linkRequest.open !== open ||
    linkRequest.source !== source
  ) {
    setLinkRequest({ annotationScope, open, source })
    if (open) {
      const selected =
        source === "annotation"
          ? annotationScope?.getSelectedAnnotation()
          : null
      const existing =
        selected?.object.type === PdfAnnotationSubtype.LINK
          ? readLinkTarget((selected.object as PdfLinkAnnoObject).target)
          : { tab: "url" as const, url: "", pageNumber: 1 }
      setTab(existing.tab)
      setUrl(existing.url)
      setPageNumber(existing.pageNumber ?? 1)
    }
  }
  const buildTarget = (): PdfLinkTarget | null => {
    if (tab === "url") {
      const trimmed = url.trim()
      if (!trimmed) return null
      const normalized = /^[a-z][a-z0-9+.-]*:/i.test(trimmed)
        ? trimmed
        : `https://${trimmed}`
      return {
        type: "action",
        action: { type: PdfActionType.URI, uri: normalized },
      }
    }
    return {
      type: "destination",
      destination: {
        pageIndex: Math.min(Math.max(pageNumber, 1), totalPages) - 1,
        zoom: { mode: PdfZoomMode.FitPage },
        view: [],
      },
    }
  }
  const handleSubmit = () => {
    const target = buildTarget()
    if (!target || !annotationScope) return
    const selected =
      source === "annotation" ? annotationScope.getSelectedAnnotation() : null
    if (selected && selected.object.type === PdfAnnotationSubtype.LINK) {
      annotationScope.updateAnnotation(
        selected.object.pageIndex,
        selected.object.id,
        {
          target,
        } as Partial<PdfLinkAnnoObject>
      )
      notify("Link updated.", "success")
      onOpenChange(false)
      return
    }
    if (selected) {
      const rects =
        "segmentRects" in selected.object &&
        Array.isArray(selected.object.segmentRects) &&
        selected.object.segmentRects.length > 0
          ? selected.object.segmentRects
          : [selected.object.rect]
      for (const rect of rects) {
        annotationScope.createAnnotation<PdfLinkAnnoObject>(
          selected.object.pageIndex,
          {
            id: uuidV4(),
            type: PdfAnnotationSubtype.LINK,
            pageIndex: selected.object.pageIndex,
            rect,
            inReplyToId: selected.object.id,
            replyType: PdfAnnotationReplyType.Group,
            target,
            strokeStyle: PdfAnnotationBorderStyle.UNDERLINE,
            strokeColor: "#2563eb",
            strokeWidth: 2,
            created: new Date(),
          }
        )
      }
      notify("Link added.", "success")
      onOpenChange(false)
      return
    }
    const formattedSelection = selectionScope?.getFormattedSelection() ?? []
    if (formattedSelection.length === 0) {
      notify("Select text or an annotation first.", "error")
      return
    }
    const applyLinks = (text: string[]) => {
      for (const page of formattedSelection) {
        const highlightId = uuidV4()
        annotationScope.createAnnotation(page.pageIndex, {
          id: highlightId,
          created: new Date(),
          flags: ["print"],
          type: PdfAnnotationSubtype.HIGHLIGHT,
          blendMode: PdfBlendMode.Multiply,
          pageIndex: page.pageIndex,
          rect: page.rect,
          segmentRects: page.segmentRects,
          strokeColor: "#ffffff",
          opacity: 0,
          custom: { text: text.join("\n") },
        })
        for (const rect of page.segmentRects) {
          annotationScope.createAnnotation<PdfLinkAnnoObject>(page.pageIndex, {
            id: uuidV4(),
            type: PdfAnnotationSubtype.LINK,
            pageIndex: page.pageIndex,
            rect,
            inReplyToId: highlightId,
            replyType: PdfAnnotationReplyType.Group,
            target,
            strokeStyle: PdfAnnotationBorderStyle.UNDERLINE,
            strokeColor: "#2563eb",
            strokeWidth: 2,
            created: new Date(),
          })
        }
      }
      selectionScope?.clear()
      notify("Link added.", "success")
      onOpenChange(false)
    }
    const textTask = selectionScope?.getSelectedText()
    if (textTask) {
      textTask.wait(applyLinks, () => applyLinks([]))
    } else {
      applyLinks([])
    }
  }
  const canSubmit = tab === "url" ? url.trim().length > 0 : pageNumber >= 1
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>Add link</DialogTitle>
          <DialogDescription>
            {source === "selection"
              ? "The selected text becomes a clickable link."
              : "The selected annotation becomes a clickable link."}
          </DialogDescription>
        </DialogHeader>
        <InlineDialogPanel className="space-y-4">
          <div className="flex items-center gap-1 rounded-lg bg-muted p-0.5">
            {(
              [
                ["url", "Web address"],
                ["page", "Page in document"],
              ] as Array<[LinkTab, string]>
            ).map(([id, label]) => (
              <button
                key={id}
                type="button"
                onClick={() => setTab(id)}
                className={cn(
                  "flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
                  tab === id
                    ? "bg-background text-foreground shadow-sm"
                    : "text-muted-foreground hover:text-foreground"
                )}
              >
                {label}
              </button>
            ))}
          </div>
          {tab === "url" ? (
            <div className="grid gap-1">
              <PdfEditorFieldLabel htmlFor="pdf-editor-link-url">
                URL
              </PdfEditorFieldLabel>
              <Input
                id="pdf-editor-link-url"
                autoFocus
                placeholder="https://example.com"
                value={url}
                onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
                  setUrl(event.target.value)
                }
                onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
                  if (event.key === "Enter" && canSubmit) handleSubmit()
                }}
              />
            </div>
          ) : (
            <div className="grid gap-1">
              <PdfEditorFieldLabel htmlFor="pdf-editor-link-page">
                Page number (1–{totalPages})
              </PdfEditorFieldLabel>
              <Input
                id="pdf-editor-link-page"
                type="number"
                min={1}
                max={totalPages}
                value={pageNumber}
                onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
                  setPageNumber(Number(event.target.value) || 1)
                }
              />
            </div>
          )}
        </InlineDialogPanel>
        <DialogFooter>
          <Button
            type="button"
            variant="outline"
            onClick={() => onOpenChange(false)}
          >
            Cancel
          </Button>
          <Button type="button" disabled={!canSubmit} onClick={handleSubmit}>
            Save link
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Keyboard shortcuts                                                         */
/* -------------------------------------------------------------------------- */
export const PDF_EDITOR_SHORTCUTS: Array<{
  group: string
  items: Array<{
    keys: string[]
    label: string
  }>
}> = [
  {
    group: "Document",
    items: [
      { keys: ["mod+S"], label: "Download" },
      { keys: ["mod+P"], label: "Print" },
      { keys: ["mod+F"], label: "Search" },
      { keys: ["mod+Z"], label: "Undo" },
      { keys: ["mod+shift+Z"], label: "Redo" },
    ],
  },
  {
    group: "View",
    items: [
      { keys: ["mod+="], label: "Zoom in" },
      { keys: ["mod+-"], label: "Zoom out" },
      { keys: ["mod+0"], label: "Fit page" },
      { keys: ["H"], label: "Hand tool" },
      { keys: ["V"], label: "Select tool" },
    ],
  },
  {
    group: "Editing",
    items: [
      { keys: ["Esc"], label: "Cancel tool / clear selection" },
      { keys: ["Delete"], label: "Delete selected annotations" },
      { keys: ["mod+A"], label: "Select all annotations on the page" },
    ],
  },
]
export function PdfEditorShortcutsDialog({
  open,
  onOpenChange,
}: {
  open: boolean
  onOpenChange: (open: boolean) => void
}) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>Keyboard shortcuts</DialogTitle>
        </DialogHeader>
        <InlineDialogPanel className="space-y-4">
          {PDF_EDITOR_SHORTCUTS.map((group) => (
            <PdfEditorSection key={group.group} title={group.group}>
              <div className="divide-y rounded-lg border">
                {group.items.map((item) => (
                  <div
                    key={item.label}
                    className="flex items-center justify-between gap-3 px-3 py-1.5 text-sm"
                  >
                    <span>{item.label}</span>
                    <span className="flex items-center gap-1">
                      {item.keys.map((key) => (
                        <Kbd key={key}>{formatShortcut(key)}</Kbd>
                      ))}
                    </span>
                  </div>
                ))}
              </div>
            </PdfEditorSection>
          ))}
        </InlineDialogPanel>
        <DialogFooter>
          <Button type="button" onClick={() => onOpenChange(false)}>
            Done
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
/* -------------------------------------------------------------------------- */
/* Password prompt (inline)                                                   */
/* -------------------------------------------------------------------------- */
export function PdfEditorPasswordPrompt({
  documentState,
}: {
  documentState: DocumentState
}) {
  const { provides: documentManager } = useDocumentManagerCapability()
  const [password, setPassword] = React.useState("")
  const [isRetrying, setIsRetrying] = React.useState(false)
  const isPasswordError = documentState.errorCode === PdfErrorCode.Password
  const isIncorrect = isPasswordError && documentState.passwordProvided
  const handleRetry = () => {
    if (!documentManager || !password.trim()) return
    setIsRetrying(true)
    documentManager.retryDocument(documentState.id, { password }).wait(
      () => {
        setPassword("")
        setIsRetrying(false)
      },
      () => setIsRetrying(false)
    )
  }
  if (!isPasswordError) {
    return (
      <div className="grid h-full place-items-center p-6">
        <div className="max-w-sm space-y-2 text-center">
          <div className="text-sm font-medium text-foreground">
            The document could not be opened
          </div>
          <div className="text-sm text-muted-foreground">
            {documentState.error ?? "An unknown error occurred."}
          </div>
        </div>
      </div>
    )
  }
  return (
    <div className="grid h-full place-items-center p-6">
      <div className="w-full max-w-sm space-y-4 rounded-xl border bg-background p-5 shadow-sm">
        <div className="flex items-start gap-3">
          <div className="grid size-9 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground">
            <LockGlyph className="size-4" />
          </div>
          <div className="space-y-1">
            <div className="text-sm font-medium text-foreground">
              Password required
            </div>
            <div className="text-xs text-muted-foreground">
              {isIncorrect
                ? "The password was incorrect. Try again."
                : "This document is protected. Enter the password to open it."}
            </div>
          </div>
        </div>
        <PasswordInput
          value={password}
          autoFocus
          placeholder="Document password"
          onChange={setPassword}
          onEnter={handleRetry}
        />
        <div className="flex justify-end">
          <InlineLoadingButton
            type="button"
            loading={isRetrying}
            disabled={!password.trim()}
            onClick={handleRetry}
          >
            Open document
          </InlineLoadingButton>
        </div>
      </div>
    </div>
  )
}
export { ignore as ignorePdfTask }
function InlineDialogPanel({
  className,
  children,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <ScrollArea className="min-h-0">
      <div
        data-slot="dialog-panel"
        className={cn("min-h-0", className)}
        {...props}
      >
        {children}
      </div>
    </ScrollArea>
  )
}
function InlineLoadingButton({
  loading,
  disabled,
  children,
  className,
  ...props
}: React.ComponentProps<typeof Button> & {
  loading?: boolean
}) {
  return (
    <Button
      {...props}
      className={cn("relative", className)}
      disabled={disabled || loading}
      aria-busy={loading || undefined}
    >
      <span className={cn("contents", loading && "invisible")}>{children}</span>
      {loading ? (
        <InlineSpinner className="pointer-events-none absolute" />
      ) : null}
    </Button>
  )
}
function InlineSpinner({ className, ...props }: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="LoaderCircle"
      tabler="IconLoader2"
      hugeicons="Loading03Icon"
      phosphor="CircleNotchIcon"
      remixicon="RiLoader4Line"
      role="status"
      aria-label="Loading"
      className={cn("size-4 animate-spin", className)}
      {...props}
    />
  )
}
type InlineRegistryIconProps = Omit<
  React.ComponentProps<"svg">,
  "children" | "strokeWidth"
> & { strokeWidth?: number }
components/extend/pdf-editor-properties.tsx
"use client"

import * as React from "react"
import {
  blendModeLabel,
  blendModeValues,
  makeStandardFont,
  PDF_FORM_FIELD_FLAG,
  PDF_FORM_FIELD_TYPE,
  PdfAnnotationBorderStyle,
  PdfAnnotationLineEnding,
  PdfAnnotationSubtype,
  PdfStandardFont,
  PdfStandardFontFamily,
  PdfTextAlignment,
  PdfVerticalAlignment,
  STANDARD_FONT_FAMILIES,
  standardFontFamily,
  standardFontIsBold,
  standardFontIsItalic,
  type LineEndings,
  type PdfAnnotationFlagName,
  type PdfAnnotationObject,
  type PdfTextWidgetAnnoField,
  type PdfWidgetAnnoField,
  type PdfWidgetAnnoObject,
  type PdfWidgetAnnoOption,
} from "@embedpdf/models"
import {
  getSelectedAnnotations,
  useAnnotation,
  useAnnotationCapability,
  type AnnotationTool,
  type TrackedAnnotation,
} from "@embedpdf/plugin-annotation/react"
import { useFormCapability } from "@embedpdf/plugin-form/react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { ColorPicker } from "@/components/extend/document-color-picker"
import {
  AlignBottomGlyph,
  AlignCenterGlyph,
  AlignLeftGlyph,
  AlignMiddleGlyph,
  AlignRightGlyph,
  AlignTopGlyph,
  BoldGlyph,
  formatDateTime,
  getAnnotationMeta,
  ItalicGlyph,
  PdfEditorEmptyState,
  PdfEditorFieldLabel,
  PdfEditorSection,
  PdfEditorSwatch,
  SlidersGlyph,
  TrashGlyph,
  usePdfEditor,
} from "@/components/extend/pdf-editor-shared"

/* -------------------------------------------------------------------------- */
/* Property schema                                                            */
/* -------------------------------------------------------------------------- */
type PropertyKind =
  | "color"
  | "colorWithTransparent"
  | "opacity"
  | "strokeWidth"
  | "strokeStyle"
  | "strokeStyleWithCloudy"
  | "linkStrokeStyle"
  | "lineEnding"
  | "lineEndings"
  | "fontFamily"
  | "fontSize"
  | "fontColor"
  | "textAlign"
  | "verticalAlign"
  | "blendMode"
  | "overlayText"
  | "rotation"
type PropertyConfig = {
  key: string
  kind: PropertyKind
  label: string
  /** Only shown while editing an existing annotation, never for tool defaults. */
  editOnly?: boolean
}
const PROPERTY_CONFIGS: Record<string, PropertyConfig> = {
  color: { key: "color", kind: "colorWithTransparent", label: "Fill" },
  strokeColor: {
    key: "strokeColor",
    kind: "colorWithTransparent",
    label: "Stroke",
  },
  markupColor: { key: "strokeColor", kind: "color", label: "Color" },
  opacity: { key: "opacity", kind: "opacity", label: "Opacity" },
  strokeWidth: {
    key: "strokeWidth",
    kind: "strokeWidth",
    label: "Stroke width",
  },
  strokeStyle: {
    key: "strokeStyle",
    kind: "strokeStyle",
    label: "Border style",
  },
  strokeStyleWithCloudy: {
    key: "strokeStyle",
    kind: "strokeStyleWithCloudy",
    label: "Border style",
  },
  linkStrokeStyle: {
    key: "strokeStyle",
    kind: "linkStrokeStyle",
    label: "Border style",
  },
  lineEnding: {
    key: "lineEnding",
    kind: "lineEnding",
    label: "Line ending",
  },
  lineEndings: {
    key: "lineEndings",
    kind: "lineEndings",
    label: "Line endings",
  },
  fontFamily: { key: "fontFamily", kind: "fontFamily", label: "Font" },
  fontSize: { key: "fontSize", kind: "fontSize", label: "Font size" },
  fontColor: { key: "fontColor", kind: "fontColor", label: "Text color" },
  textAlign: { key: "textAlign", kind: "textAlign", label: "Alignment" },
  verticalAlign: {
    key: "verticalAlign",
    kind: "verticalAlign",
    label: "Vertical alignment",
  },
  blendMode: { key: "blendMode", kind: "blendMode", label: "Blend mode" },
  overlayText: {
    key: "overlayText",
    kind: "overlayText",
    label: "Overlay text",
  },
  rotation: {
    key: "rotation",
    kind: "rotation",
    label: "Rotation",
    editOnly: true,
  },
}
/** Ordered property lists keyed by EmbedPDF tool id. */
const TOOL_PROPERTIES: Record<string, string[]> = {
  highlight: ["markupColor", "opacity", "blendMode"],
  underline: ["markupColor", "opacity", "blendMode"],
  strikeout: ["markupColor", "opacity", "blendMode"],
  squiggly: ["markupColor", "opacity", "blendMode"],
  ink: ["markupColor", "opacity", "strokeWidth", "rotation"],
  inkHighlighter: [
    "markupColor",
    "opacity",
    "strokeWidth",
    "blendMode",
    "rotation",
  ],
  signatureInk: ["markupColor", "opacity", "strokeWidth", "rotation"],
  circle: [
    "color",
    "opacity",
    "strokeColor",
    "strokeStyleWithCloudy",
    "strokeWidth",
    "rotation",
  ],
  square: [
    "color",
    "opacity",
    "strokeColor",
    "strokeStyleWithCloudy",
    "strokeWidth",
    "rotation",
  ],
  polygon: [
    "strokeColor",
    "opacity",
    "strokeStyleWithCloudy",
    "strokeWidth",
    "color",
    "rotation",
  ],
  line: [
    "strokeColor",
    "opacity",
    "strokeStyle",
    "strokeWidth",
    "lineEndings",
    "color",
    "rotation",
  ],
  lineArrow: [
    "strokeColor",
    "opacity",
    "strokeStyle",
    "strokeWidth",
    "lineEndings",
    "color",
    "rotation",
  ],
  polyline: [
    "strokeColor",
    "opacity",
    "strokeStyle",
    "strokeWidth",
    "lineEndings",
    "color",
    "rotation",
  ],
  textComment: ["markupColor", "opacity"],
  insertText: ["markupColor", "opacity"],
  replaceText: ["markupColor", "opacity"],
  freeText: [
    "fontFamily",
    "fontSize",
    "fontColor",
    "textAlign",
    "verticalAlign",
    "opacity",
    "color",
    "rotation",
  ],
  freeTextCallout: [
    "fontFamily",
    "fontSize",
    "fontColor",
    "textAlign",
    "verticalAlign",
    "opacity",
    "color",
    "markupColor",
    "strokeWidth",
    "lineEnding",
  ],
  stamp: ["rotation"],
  rubberStamp: ["rotation"],
  signatureStamp: ["rotation"],
  redact: ["strokeColor", "color", "opacity", "overlayText"],
  link: ["strokeColor", "strokeWidth", "linkStrokeStyle"],
  formTextField: [
    "fontSize",
    "fontColor",
    "strokeColor",
    "strokeWidth",
    "color",
  ],
  formCheckbox: ["strokeColor", "color", "strokeWidth"],
  formCombobox: [
    "fontSize",
    "fontColor",
    "strokeColor",
    "strokeWidth",
    "color",
  ],
  formListbox: [
    "fontFamily",
    "fontSize",
    "fontColor",
    "strokeColor",
    "strokeWidth",
    "color",
  ],
  formRadioButton: ["strokeColor", "color", "strokeWidth"],
}
function getSharedProperties(toolIds: string[]) {
  const unique = [...new Set(toolIds)]
  if (unique.length === 0) return []
  const first = new Set(TOOL_PROPERTIES[unique[0]] ?? [])
  for (const id of unique.slice(1)) {
    const own = new Set(TOOL_PROPERTIES[id] ?? [])
    for (const property of first) {
      if (!own.has(property)) first.delete(property)
    }
  }
  return (TOOL_PROPERTIES[unique[0]] ?? []).filter((property) =>
    first.has(property)
  )
}
const FONT_SIZE_PRESETS = [
  8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72,
]
const LINE_ENDING_OPTIONS: Array<{
  value: PdfAnnotationLineEnding
  label: string
}> = [
  { value: PdfAnnotationLineEnding.None, label: "None" },
  { value: PdfAnnotationLineEnding.OpenArrow, label: "Open arrow" },
  { value: PdfAnnotationLineEnding.ClosedArrow, label: "Closed arrow" },
  { value: PdfAnnotationLineEnding.ROpenArrow, label: "Reverse open arrow" },
  {
    value: PdfAnnotationLineEnding.RClosedArrow,
    label: "Reverse closed arrow",
  },
  { value: PdfAnnotationLineEnding.Square, label: "Square" },
  { value: PdfAnnotationLineEnding.Circle, label: "Circle" },
  { value: PdfAnnotationLineEnding.Diamond, label: "Diamond" },
  { value: PdfAnnotationLineEnding.Butt, label: "Butt" },
  { value: PdfAnnotationLineEnding.Slash, label: "Slash" },
]
const FLAG_OPTIONS: Array<{
  flag: PdfAnnotationFlagName
  label: string
  description: string
}> = [
  {
    flag: "locked",
    label: "Lock position",
    description: "Prevents moving, resizing, and deleting.",
  },
  {
    flag: "lockedContents",
    label: "Lock contents",
    description: "Prevents editing the text.",
  },
  {
    flag: "print",
    label: "Print",
    description: "Include when printing.",
  },
]
/* -------------------------------------------------------------------------- */
/* Controls                                                                   */
/* -------------------------------------------------------------------------- */
function useDebouncedCommit<T>(
  value: T,
  onCommit: (value: T) => void,
  delay = 150
) {
  const [draft, setDraft] = React.useState(value)
  const timeoutRef = React.useRef<number | null>(null)
  const onCommitRef = React.useRef(onCommit)
  React.useEffect(() => {
    onCommitRef.current = onCommit
  })
  const [previousValue, setPreviousValue] = React.useState(value)
  if (!Object.is(previousValue, value)) {
    setPreviousValue(value)
    setDraft(value)
  }
  React.useEffect(
    () => () => {
      if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current)
    },
    []
  )
  const update = React.useCallback(
    (next: T) => {
      setDraft(next)
      if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current)
      timeoutRef.current = window.setTimeout(() => {
        timeoutRef.current = null
        onCommitRef.current(next)
      }, delay)
    },
    [delay]
  )
  return [draft, update] as const
}
function PropertySwitch({
  checked,
  disabled,
  label,
  description,
  onCheckedChange,
}: {
  checked: boolean
  disabled?: boolean
  label: React.ReactNode
  description?: React.ReactNode
  onCheckedChange: (checked: boolean) => void
}) {
  const id = React.useId()
  return (
    <label
      htmlFor={id}
      data-slot="label"
      className={cn(
        "flex items-center justify-between gap-4 rounded-md px-1 py-1",
        disabled ? "cursor-not-allowed opacity-64" : "cursor-pointer"
      )}
    >
      <span className="min-w-0">
        <span className="block text-sm leading-4">{label}</span>
        {description ? (
          <span className="block text-xs text-muted-foreground">
            {description}
          </span>
        ) : null}
      </span>
      <Switch
        id={id}
        checked={checked}
        disabled={disabled}
        onCheckedChange={onCheckedChange}
      />
    </label>
  )
}
function ColorControl({
  label,
  value,
  presets,
  allowTransparent,
  onChange,
}: {
  label: string
  value: string | undefined
  presets: string[]
  allowTransparent: boolean
  onChange: (value: string) => void
}) {
  const current = (value ?? "").toLowerCase()
  const isTransparent = !value || current === "transparent"
  const pickerSource = isTransparent ? "#111827" : (value ?? "#111827")
  return (
    <div className="space-y-1.5">
      <div className="flex items-center justify-between">
        <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>
        <span className="text-[11px] text-muted-foreground tabular-nums">
          {isTransparent ? "None" : current}
        </span>
      </div>
      <div className="flex flex-wrap items-center gap-1.5">
        {presets.map((preset) => (
          <PdfEditorSwatch
            key={preset}
            color={preset}
            active={!isTransparent && current === preset.toLowerCase()}
            onClick={() => onChange(preset)}
          />
        ))}
        {allowTransparent ? (
          <PdfEditorSwatch
            color="transparent"
            label="No fill"
            active={isTransparent}
            onClick={() => onChange("transparent")}
          />
        ) : null}
        <ColorPicker
          label={`Custom ${label.toLowerCase()}`}
          rainbowTrigger
          color={pickerSource}
          onChange={onChange}
        />
      </div>
    </div>
  )
}
function SliderControl({
  label,
  value,
  min,
  max,
  step,
  format,
  onChange,
}: {
  label: string
  value: number
  min: number
  max: number
  step: number
  format: (value: number) => string
  onChange: (value: number) => void
}) {
  const [draft, update] = useDebouncedCommit(value, onChange)
  return (
    <div className="space-y-1.5">
      <div className="flex items-center justify-between">
        <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>
        <span className="text-[11px] text-muted-foreground tabular-nums">
          {format(draft)}
        </span>
      </div>
      <input
        type="range"
        min={min}
        max={max}
        step={step}
        value={draft}
        aria-label={label}
        onChange={(event) => update(Number(event.target.value))}
        className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary"
      />
    </div>
  )
}
type StrokeStyleValue =
  | "solid"
  | "dotted"
  | "short-dashed"
  | "dashed"
  | "long-dashed"
  | "dash-dot"
  | "long-dash-dot"
  | "dash-dot-dot"
  | "cloudy-1"
  | "cloudy-2"
  | "underline"
  | "custom"
const STROKE_STYLE_PRESETS: Array<{
  value: Exclude<
    StrokeStyleValue,
    "cloudy-1" | "cloudy-2" | "underline" | "custom"
  >
  label: string
  dashArray?: number[]
}> = [
  { value: "solid", label: "Solid" },
  { value: "dotted", label: "Dotted", dashArray: [1, 2] },
  { value: "short-dashed", label: "Short dashed", dashArray: [2, 2] },
  { value: "dashed", label: "Dashed", dashArray: [4, 3] },
  { value: "long-dashed", label: "Long dashed", dashArray: [8, 4] },
  { value: "dash-dot", label: "Dash dot", dashArray: [6, 3, 1, 3] },
  {
    value: "long-dash-dot",
    label: "Long dash dot",
    dashArray: [10, 4, 1, 4],
  },
  {
    value: "dash-dot-dot",
    label: "Dash dot dot",
    dashArray: [6, 3, 1, 3, 1, 3],
  },
]
function getStrokeDashArray(value: StrokeStyleValue) {
  return STROKE_STYLE_PRESETS.find((preset) => preset.value === value)
    ?.dashArray
}
function StrokeStylePreview({
  value,
  dashArray,
}: {
  value: StrokeStyleValue
  dashArray?: number[]
}) {
  const path =
    value === "cloudy-2"
      ? "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"
      : "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"
  const resolvedDashArray = dashArray ?? getStrokeDashArray(value)
  return (
    <svg
      aria-hidden="true"
      viewBox="0 0 56 12"
      className="h-6! w-24! shrink-0 text-foreground"
    >
      <path
        d={value.startsWith("cloudy") ? path : "M1 6H55"}
        fill="none"
        stroke="currentColor"
        strokeDasharray={resolvedDashArray?.join(" ")}
        strokeLinecap="round"
        strokeWidth="2.25"
      />
    </svg>
  )
}
function SelectControl({
  label,
  value,
  options,
  onChange,
}: {
  label: string
  value: string
  options: Array<{
    value: string
    label: string
    preview?: React.ReactNode
  }>
  onChange: (value: string) => void
}) {
  const selectedOption =
    options.find((option) => option.value === value) ?? null
  return (
    <div className="space-y-1.5">
      <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>
      <Select
        items={options}
        value={value}
        onValueChange={(next) => {
          if (next) onChange(String(next))
        }}
      >
        <SelectTrigger size="sm" className="w-full">
          <SelectValue placeholder={label}>
            {selectedOption ? (
              <span className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
                <span className="truncate">{selectedOption.label}</span>
                {selectedOption.preview}
              </span>
            ) : (
              label
            )}
          </SelectValue>
        </SelectTrigger>
        <SelectContent alignItemWithTrigger={false}>
          {options.map((option) => (
            <SelectItem
              key={option.value}
              value={option.value}
              className={cn(option.preview && "py-0.5 pe-2")}
            >
              <span className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
                <span className="truncate">{option.label}</span>
                {option.preview}
              </span>
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  )
}
function getStrokeStyleValue(
  strokeStyle: PdfAnnotationBorderStyle | undefined,
  cloudyBorderIntensity: number | undefined,
  strokeDashArray: number[] | undefined
): StrokeStyleValue {
  if (
    strokeStyle === PdfAnnotationBorderStyle.CLOUDY ||
    (cloudyBorderIntensity ?? 0) > 0
  ) {
    return (cloudyBorderIntensity ?? 1) >= 2 ? "cloudy-2" : "cloudy-1"
  }
  if (strokeStyle === PdfAnnotationBorderStyle.UNDERLINE) return "underline"
  if (strokeStyle === PdfAnnotationBorderStyle.DASHED) {
    const preset = STROKE_STYLE_PRESETS.find((item) => {
      if (!item.dashArray || !strokeDashArray) return false
      if (item.dashArray.length !== strokeDashArray.length) return false
      return item.dashArray.every(
        (part, index) => part === strokeDashArray[index]
      )
    })
    return preset?.value ?? "custom"
  }
  return "solid"
}
function getStrokeStylePatch(
  value: StrokeStyleValue,
  customDashArray?: number[]
) {
  const dashArray = getStrokeDashArray(value)
  if (dashArray || value === "custom") {
    return {
      strokeStyle: PdfAnnotationBorderStyle.DASHED,
      strokeDashArray: dashArray ?? customDashArray ?? [4, 3],
      cloudyBorderIntensity: 0,
    }
  }
  switch (value) {
    case "underline":
      return {
        strokeStyle: PdfAnnotationBorderStyle.UNDERLINE,
        strokeDashArray: undefined,
        cloudyBorderIntensity: 0,
      }
    case "cloudy-1":
      return {
        strokeStyle: PdfAnnotationBorderStyle.CLOUDY,
        strokeDashArray: undefined,
        cloudyBorderIntensity: 1,
      }
    case "cloudy-2":
      return {
        strokeStyle: PdfAnnotationBorderStyle.CLOUDY,
        strokeDashArray: undefined,
        cloudyBorderIntensity: 2,
      }
    default:
      return {
        strokeStyle: PdfAnnotationBorderStyle.SOLID,
        strokeDashArray: undefined,
        cloudyBorderIntensity: 0,
      }
  }
}
function FontControl({
  value,
  onChange,
}: {
  value: PdfStandardFont | undefined
  onChange: (value: PdfStandardFont) => void
}) {
  const font = value ?? PdfStandardFont.Helvetica
  const family = standardFontFamily(font)
  const bold = standardFontIsBold(font)
  const italic = standardFontIsItalic(font)
  const supportsStyles =
    family !== PdfStandardFontFamily.Symbol &&
    family !== PdfStandardFontFamily.ZapfDingbats
  const familyOptions = STANDARD_FONT_FAMILIES.filter(
    (item) => item !== PdfStandardFontFamily.Unknown
  ).map((item) => ({ label: item, value: item }))
  const apply = (
    nextFamily: PdfStandardFontFamily,
    nextBold: boolean,
    nextItalic: boolean
  ) => {
    const next =
      makeStandardFont(nextFamily, { bold: nextBold, italic: nextItalic }) ??
      makeStandardFont(nextFamily, { bold: false, italic: false })
    if (next !== undefined && next !== PdfStandardFont.Unknown) onChange(next)
  }
  return (
    <div className="space-y-1.5">
      <PdfEditorFieldLabel>Font</PdfEditorFieldLabel>
      <div className="flex items-center gap-1.5">
        <Select
          items={familyOptions}
          value={family}
          onValueChange={(next) =>
            apply(String(next) as PdfStandardFontFamily, bold, italic)
          }
        >
          <SelectTrigger size="sm" className="min-w-0 flex-1">
            <SelectValue placeholder="Font" />
          </SelectTrigger>
          <SelectContent alignItemWithTrigger={false}>
            {familyOptions.map((option) => (
              <SelectItem key={option.value} value={option.value}>
                {option.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <ToggleGroup
          multiple
          value={[bold ? "bold" : "", italic ? "italic" : ""].filter(Boolean)}
          onValueChange={(next: string[]) =>
            apply(family, next.includes("bold"), next.includes("italic"))
          }
          className="shrink-0"
          spacing={0}
        >
          <ToggleGroupItem
            value="bold"
            size="sm"
            variant="outline"
            aria-label="Bold"
            disabled={!supportsStyles}
          >
            <BoldGlyph className="size-3.5" />
          </ToggleGroupItem>
          <ToggleGroupItem
            value="italic"
            size="sm"
            variant="outline"
            aria-label="Italic"
            disabled={!supportsStyles}
          >
            <ItalicGlyph className="size-3.5" />
          </ToggleGroupItem>
        </ToggleGroup>
      </div>
    </div>
  )
}
function FontSizeControl({
  value,
  onChange,
}: {
  value: number | undefined
  onChange: (value: number) => void
}) {
  const [draft, update] = useDebouncedCommit(value ?? 12, onChange, 250)
  const sizeOptions = (
    FONT_SIZE_PRESETS.includes(draft)
      ? FONT_SIZE_PRESETS
      : [...FONT_SIZE_PRESETS, draft].sort((a, b) => a - b)
  ).map((size) => ({ label: `${size} pt`, value: String(size) }))
  return (
    <div className="space-y-1.5">
      <PdfEditorFieldLabel>Font size</PdfEditorFieldLabel>
      <div className="flex items-center gap-1.5">
        <Input
          type="number"
          min={4}
          max={200}
          value={draft}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            const next = Number(event.target.value)
            if (Number.isFinite(next) && next > 0) update(next)
          }}
          className={cn("h-8 px-2.5", "w-20")}
        />
        <Select
          items={sizeOptions}
          value={String(draft)}
          onValueChange={(next) => onChange(Number(next))}
        >
          <SelectTrigger size="sm" className="min-w-0 flex-1">
            <SelectValue placeholder="Size" />
          </SelectTrigger>
          <SelectContent alignItemWithTrigger={false}>
            {sizeOptions.map((option) => (
              <SelectItem key={option.value} value={option.value}>
                {option.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>
    </div>
  )
}
function AlignControl({
  label,
  value,
  onChange,
  options,
}: {
  label: string
  value: string
  onChange: (value: string) => void
  options: Array<{
    value: string
    label: string
    glyph: React.ReactNode
  }>
}) {
  return (
    <div className="space-y-1.5">
      <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>
      <ToggleGroup
        value={[value]}
        onValueChange={(next: string[]) => {
          const selected = next[0]
          if (selected !== undefined) onChange(selected)
        }}
        spacing={0}
      >
        {options.map((option) => (
          <ToggleGroupItem
            key={option.value}
            value={option.value}
            size="sm"
            variant="outline"
            aria-label={option.label}
          >
            {option.glyph}
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
    </div>
  )
}
function LineEndingPreview({ value }: { value: PdfAnnotationLineEnding }) {
  const isOpenArrow =
    value === PdfAnnotationLineEnding.OpenArrow ||
    value === PdfAnnotationLineEnding.ROpenArrow
  const isClosedArrow =
    value === PdfAnnotationLineEnding.ClosedArrow ||
    value === PdfAnnotationLineEnding.RClosedArrow
  const isReverse =
    value === PdfAnnotationLineEnding.ROpenArrow ||
    value === PdfAnnotationLineEnding.RClosedArrow
  const lineEndX = isReverse
    ? isClosedArrow
      ? 58
      : 56
    : isClosedArrow
      ? 68
      : 66
  return (
    <svg
      aria-hidden="true"
      viewBox="0 0 72 16"
      className="h-6! w-24! shrink-0 text-foreground"
    >
      <path
        d={`M2 8H${lineEndX}`}
        fill="none"
        stroke="currentColor"
        strokeLinecap="round"
        strokeWidth="2.5"
      />
      {isOpenArrow ? (
        <path
          d={isReverse ? "M56 8 66 2M56 8l10 6" : "M66 8 56 2M66 8l-10 6"}
          fill="none"
          stroke="currentColor"
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth="2.5"
        />
      ) : null}
      {isClosedArrow ? (
        <path
          d={isReverse ? "m58 8 10-6v12Z" : "m68 8-10-6v12Z"}
          fill="currentColor"
        />
      ) : null}
      {value === PdfAnnotationLineEnding.Square ? (
        <rect x="60" y="2" width="12" height="12" fill="currentColor" />
      ) : null}
      {value === PdfAnnotationLineEnding.Circle ? (
        <circle cx="66" cy="8" r="6" fill="currentColor" />
      ) : null}
      {value === PdfAnnotationLineEnding.Diamond ? (
        <path d="m66 2 6 6-6 6-6-6Z" fill="currentColor" />
      ) : null}
      {value === PdfAnnotationLineEnding.Butt ? (
        <path d="M66 2v12" stroke="currentColor" strokeWidth="2.5" />
      ) : null}
      {value === PdfAnnotationLineEnding.Slash ? (
        <path d="m60 14 12-12" stroke="currentColor" strokeWidth="2.5" />
      ) : null}
    </svg>
  )
}
function LineEndingsControl({
  value,
  onChange,
}: {
  value: LineEndings | undefined
  onChange: (value: LineEndings) => void
}) {
  const start = value?.start ?? PdfAnnotationLineEnding.None
  const end = value?.end ?? PdfAnnotationLineEnding.None
  const options = LINE_ENDING_OPTIONS.map((option) => ({
    value: String(option.value),
    label: option.label,
    preview: <LineEndingPreview value={option.value} />,
  }))
  return (
    <div className="space-y-2">
      <SelectControl
        label="Line start"
        value={String(start)}
        options={options}
        onChange={(next) =>
          onChange({ start: Number(next) as PdfAnnotationLineEnding, end })
        }
      />
      <SelectControl
        label="Line end"
        value={String(end)}
        options={options}
        onChange={(next) =>
          onChange({ start, end: Number(next) as PdfAnnotationLineEnding })
        }
      />
    </div>
  )
}
function LineEndingControl({
  label,
  value,
  onChange,
}: {
  label: string
  value: PdfAnnotationLineEnding | undefined
  onChange: (value: PdfAnnotationLineEnding) => void
}) {
  const options = LINE_ENDING_OPTIONS.map((option) => ({
    value: String(option.value),
    label: option.label,
    preview: <LineEndingPreview value={option.value} />,
  }))
  return (
    <SelectControl
      label={label}
      value={String(value ?? PdfAnnotationLineEnding.None)}
      options={options}
      onChange={(next) => onChange(Number(next) as PdfAnnotationLineEnding)}
    />
  )
}
function TextControl({
  label,
  value,
  placeholder,
  onChange,
}: {
  label: string
  value: string
  placeholder?: string
  onChange: (value: string) => void
}) {
  const [draft, update] = useDebouncedCommit(value, onChange, 400)
  return (
    <div className="space-y-1.5">
      <PdfEditorFieldLabel>{label}</PdfEditorFieldLabel>
      <Input
        value={draft}
        placeholder={placeholder}
        onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
          update(event.target.value)
        }
        className={cn("h-8 px-2.5")}
      />
    </div>
  )
}
function RotationControl({
  value,
  onChange,
}: {
  value: number
  onChange: (value: number) => void
}) {
  const [draft, update] = useDebouncedCommit(value, onChange, 250)
  return (
    <div className="space-y-1.5">
      <PdfEditorFieldLabel>Rotation</PdfEditorFieldLabel>
      <div className="flex items-center gap-1.5">
        <Input
          type="number"
          min={0}
          max={359}
          value={Math.round(draft)}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
            update(((Number(event.target.value) % 360) + 360) % 360)
          }
          className={cn("h-8 px-2.5", "w-20")}
        />
        <span className="text-xs text-muted-foreground">degrees</span>
        {[0, 90, 180, 270].map((preset) => (
          <Button
            key={preset}
            type="button"
            size="xs"
            variant={draft === preset ? "secondary" : "ghost"}
            onClick={() => {
              update(preset)
            }}
          >
            {preset}°
          </Button>
        ))}
      </div>
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Property list                                                              */
/* -------------------------------------------------------------------------- */
function PropertyControl({
  config,
  values,
  colorPresets,
  onPatch,
  onRotate,
}: {
  config: PropertyConfig
  values: Record<string, unknown>
  colorPresets: string[]
  onPatch: (patch: Record<string, unknown>) => void
  onRotate?: (rotation: number) => void
}) {
  const value = values[config.key]
  switch (config.kind) {
    case "color":
    case "colorWithTransparent":
    case "fontColor":
      return (
        <ColorControl
          label={config.label}
          value={typeof value === "string" ? value : undefined}
          presets={colorPresets}
          allowTransparent={config.kind === "colorWithTransparent"}
          onChange={(next) => onPatch({ [config.key]: next })}
        />
      )
    case "opacity":
      return (
        <SliderControl
          label={config.label}
          value={typeof value === "number" ? value : 1}
          min={0}
          max={1}
          step={0.05}
          format={(item) => `${Math.round(item * 100)}%`}
          onChange={(next) => onPatch({ [config.key]: next })}
        />
      )
    case "strokeWidth":
      return (
        <SliderControl
          label={config.label}
          value={typeof value === "number" ? value : 1}
          min={1}
          max={30}
          step={1}
          format={(item) => `${item} pt`}
          onChange={(next) => onPatch({ [config.key]: next })}
        />
      )
    case "strokeStyle":
    case "strokeStyleWithCloudy":
    case "linkStrokeStyle": {
      const strokeDashArray = values.strokeDashArray as number[] | undefined
      const selectedValue = getStrokeStyleValue(
        value as PdfAnnotationBorderStyle | undefined,
        values.cloudyBorderIntensity as number | undefined,
        strokeDashArray
      )
      const options: Array<{
        value: string
        label: string
        preview: React.ReactNode
      }> = STROKE_STYLE_PRESETS.map((preset) => ({
        value: preset.value,
        label: preset.label,
        preview: (
          <StrokeStylePreview
            value={preset.value}
            dashArray={preset.dashArray}
          />
        ),
      }))
      if (config.kind === "linkStrokeStyle") {
        options.splice(1, 0, {
          value: "underline",
          label: "Underline",
          preview: <StrokeStylePreview value="underline" />,
        })
      }
      if (config.kind === "strokeStyleWithCloudy") {
        options.push(
          {
            value: "cloudy-1",
            label: "Cloudy",
            preview: <StrokeStylePreview value="cloudy-1" />,
          },
          {
            value: "cloudy-2",
            label: "Cloudy (dense)",
            preview: <StrokeStylePreview value="cloudy-2" />,
          }
        )
      }
      if (selectedValue === "custom") {
        options.push({
          value: "custom",
          label: "Custom dashed",
          preview: (
            <StrokeStylePreview value="custom" dashArray={strokeDashArray} />
          ),
        })
      }
      return (
        <SelectControl
          label={config.label}
          value={selectedValue}
          options={options}
          onChange={(next) =>
            onPatch(
              getStrokeStylePatch(next as StrokeStyleValue, strokeDashArray)
            )
          }
        />
      )
    }
    case "lineEnding":
      return (
        <LineEndingControl
          label={config.label}
          value={value as PdfAnnotationLineEnding | undefined}
          onChange={(next) => onPatch({ [config.key]: next })}
        />
      )
    case "lineEndings":
      return (
        <LineEndingsControl
          value={value as LineEndings | undefined}
          onChange={(next) => onPatch({ lineEndings: next })}
        />
      )
    case "fontFamily":
      return (
        <FontControl
          value={value as PdfStandardFont | undefined}
          onChange={(next) => onPatch({ fontFamily: next })}
        />
      )
    case "fontSize":
      return (
        <FontSizeControl
          value={typeof value === "number" ? value : undefined}
          onChange={(next) => onPatch({ fontSize: next })}
        />
      )
    case "textAlign":
      return (
        <AlignControl
          label={config.label}
          value={String(value ?? PdfTextAlignment.Left)}
          onChange={(next) => onPatch({ textAlign: Number(next) })}
          options={[
            {
              value: String(PdfTextAlignment.Left),
              label: "Align left",
              glyph: <AlignLeftGlyph className="size-3.5" />,
            },
            {
              value: String(PdfTextAlignment.Center),
              label: "Align center",
              glyph: <AlignCenterGlyph className="size-3.5" />,
            },
            {
              value: String(PdfTextAlignment.Right),
              label: "Align right",
              glyph: <AlignRightGlyph className="size-3.5" />,
            },
          ]}
        />
      )
    case "verticalAlign":
      return (
        <AlignControl
          label={config.label}
          value={String(value ?? PdfVerticalAlignment.Top)}
          onChange={(next) => onPatch({ verticalAlign: Number(next) })}
          options={[
            {
              value: String(PdfVerticalAlignment.Top),
              label: "Align top",
              glyph: <AlignTopGlyph className="size-3.5" />,
            },
            {
              value: String(PdfVerticalAlignment.Middle),
              label: "Align middle",
              glyph: <AlignMiddleGlyph className="size-3.5" />,
            },
            {
              value: String(PdfVerticalAlignment.Bottom),
              label: "Align bottom",
              glyph: <AlignBottomGlyph className="size-3.5" />,
            },
          ]}
        />
      )
    case "blendMode":
      return (
        <SelectControl
          label={config.label}
          value={String(value ?? 0)}
          options={blendModeValues.map((mode) => ({
            value: String(mode),
            label: blendModeLabel(mode),
          }))}
          onChange={(next) => onPatch({ blendMode: Number(next) })}
        />
      )
    case "overlayText":
      return (
        <TextControl
          label={config.label}
          value={typeof value === "string" ? value : ""}
          placeholder="e.g. REDACTED"
          onChange={(next) => onPatch({ overlayText: next })}
        />
      )
    case "rotation":
      return (
        <RotationControl
          value={typeof value === "number" ? value : 0}
          onChange={(next) =>
            onRotate ? onRotate(next) : onPatch({ rotation: next })
          }
        />
      )
    default:
      return null
  }
}
/* -------------------------------------------------------------------------- */
/* Widget (form field) properties                                             */
/* -------------------------------------------------------------------------- */
const FIELD_TYPE_LABELS: Partial<Record<PDF_FORM_FIELD_TYPE, string>> = {
  [PDF_FORM_FIELD_TYPE.TEXTFIELD]: "Text field",
  [PDF_FORM_FIELD_TYPE.CHECKBOX]: "Checkbox",
  [PDF_FORM_FIELD_TYPE.RADIOBUTTON]: "Radio button",
  [PDF_FORM_FIELD_TYPE.COMBOBOX]: "Dropdown",
  [PDF_FORM_FIELD_TYPE.LISTBOX]: "List box",
  [PDF_FORM_FIELD_TYPE.PUSHBUTTON]: "Button",
  [PDF_FORM_FIELD_TYPE.SIGNATURE]: "Signature field",
}
export function getFormFieldTypeLabel(type: PDF_FORM_FIELD_TYPE) {
  return FIELD_TYPE_LABELS[type] ?? "Form field"
}
function WidgetProperties({
  documentId,
  widget,
}: {
  documentId: string
  widget: PdfWidgetAnnoObject
}) {
  const { provides: annotation } = useAnnotation(documentId)
  const { provides: form } = useFormCapability()
  const { openDialog, notify } = usePdfEditor()
  const [nameDraft, setNameDraft] = React.useState(widget.field.name)
  const [nameError, setNameError] = React.useState<string | null>(null)
  const field = widget.field
  const fieldLabel = getFormFieldTypeLabel(field.type)
  const [previousField, setPreviousField] = React.useState({
    id: widget.id,
    name: widget.field.name,
  })
  if (
    previousField.id !== widget.id ||
    previousField.name !== widget.field.name
  ) {
    setPreviousField({ id: widget.id, name: widget.field.name })
    setNameDraft(widget.field.name)
    setNameError(null)
  }
  const updateField = React.useCallback(
    (patch: Partial<PdfWidgetAnnoField>) => {
      annotation?.updateAnnotations([
        {
          pageIndex: widget.pageIndex,
          id: widget.id,
          patch: { field: { ...widget.field, ...patch } as PdfWidgetAnnoField },
        },
      ])
    },
    [annotation, widget.field, widget.id, widget.pageIndex]
  )
  const toggleFlag = (flag: PDF_FORM_FIELD_FLAG, enabled: boolean) => {
    const current = field.flag ?? PDF_FORM_FIELD_FLAG.NONE
    updateField({ flag: enabled ? current | flag : current & ~flag })
  }
  const commitName = () => {
    const nextName = nameDraft.trim()
    if (!form || !nextName || nextName === field.name) {
      setNameDraft(field.name)
      return
    }
    form.renameField(widget.id, nextName, documentId).wait(
      (result) => {
        setNameError(null)
        if (result.outcome === "conflict") {
          openDialog({
            type: "confirm",
            title: "Share this field?",
            description: `A ${fieldLabel.toLowerCase()} named "${result.fieldName}" already exists. Sharing makes both widgets show the same value.`,
            confirmLabel: "Share field",
            onConfirm: () => {
              form
                .shareField(widget.id, result.targetAnnotationId, documentId)
                .wait(
                  () => notify("Field shared.", "success"),
                  (failure) => {
                    setNameError(failure.reason.message)
                    setNameDraft(field.name)
                  }
                )
            },
          })
        }
      },
      (failure) => {
        setNameError(failure.reason.message)
        setNameDraft(field.name)
      }
    )
  }
  const options: PdfWidgetAnnoOption[] =
    field.type === PDF_FORM_FIELD_TYPE.COMBOBOX ||
    field.type === PDF_FORM_FIELD_TYPE.LISTBOX
      ? ((
          field as {
            options?: PdfWidgetAnnoOption[]
          }
        ).options ?? [])
      : []
  const setOptions = (next: PdfWidgetAnnoOption[]) =>
    updateField({ options: next } as Partial<PdfWidgetAnnoField>)
  const isTextField = field.type === PDF_FORM_FIELD_TYPE.TEXTFIELD
  const textField = field as PdfTextWidgetAnnoField
  const maxLength = isTextField ? (textField.maxLen ?? 0) : 0
  const isComb = Boolean(field.flag & PDF_FORM_FIELD_FLAG.TEXT_COMB)
  return (
    <PdfEditorSection title={`${fieldLabel} settings`}>
      <div className="space-y-3">
        <div className="space-y-1.5">
          <PdfEditorFieldLabel htmlFor={`pdf-editor-field-name-${widget.id}`}>
            Field name
          </PdfEditorFieldLabel>
          <Input
            id={`pdf-editor-field-name-${widget.id}`}
            value={nameDraft}
            onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
              setNameDraft(event.target.value)
            }
            onBlur={commitName}
            onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
              if (event.key === "Enter") event.currentTarget.blur()
            }}
            className={cn("h-8 px-2.5")}
          />
          {nameError ? (
            <div className="text-xs text-destructive">{nameError}</div>
          ) : null}
        </div>

        {isTextField ? (
          <>
            <TextControl
              label="Default value"
              value={field.value ?? ""}
              onChange={(next) => updateField({ value: next })}
            />
            <div className="space-y-1.5">
              <PdfEditorFieldLabel>Max length</PdfEditorFieldLabel>
              <Input
                type="number"
                min={0}
                placeholder="Unlimited"
                value={maxLength || ""}
                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
                  const parsed = Number.parseInt(event.target.value, 10)
                  const next =
                    Number.isNaN(parsed) || parsed <= 0 ? undefined : parsed
                  updateField({ maxLen: next } as Partial<PdfWidgetAnnoField>)
                  if (!next && isComb)
                    toggleFlag(PDF_FORM_FIELD_FLAG.TEXT_COMB, false)
                }}
                className={cn("h-8 px-2.5")}
              />
            </div>
          </>
        ) : null}

        {options.length > 0 ||
        field.type === PDF_FORM_FIELD_TYPE.COMBOBOX ||
        field.type === PDF_FORM_FIELD_TYPE.LISTBOX ? (
          <div className="space-y-1.5">
            <PdfEditorFieldLabel>Options</PdfEditorFieldLabel>
            <div className="space-y-1.5">
              {options.map((option, index) => (
                <div key={index} className="flex items-center gap-1.5">
                  <Input
                    defaultValue={option.label}
                    onBlur={(event: React.FocusEvent<HTMLInputElement>) => {
                      const nextLabel = event.target.value
                      if (nextLabel === option.label) return
                      setOptions(
                        options.map((item, itemIndex) =>
                          itemIndex === index
                            ? { ...item, label: nextLabel }
                            : item
                        )
                      )
                    }}
                    onKeyDown={(
                      event: React.KeyboardEvent<HTMLInputElement>
                    ) => {
                      if (event.key === "Enter") event.currentTarget.blur()
                    }}
                    className={cn("h-8 px-2.5")}
                  />
                  <Button
                    type="button"
                    variant="ghost"
                    size="icon-xs"
                    aria-label="Remove option"
                    onClick={() =>
                      setOptions(
                        options.filter((_, itemIndex) => itemIndex !== index)
                      )
                    }
                  >
                    <TrashGlyph className="size-3.5" />
                  </Button>
                </div>
              ))}
              <Button
                type="button"
                variant="outline"
                size="xs"
                onClick={() =>
                  setOptions([
                    ...options,
                    {
                      label: `Option ${options.length + 1}`,
                      isSelected: false,
                    },
                  ])
                }
              >
                Add option
              </Button>
            </div>
          </div>
        ) : null}

        <div className="space-y-0.5">
          <PropertySwitch
            checked={Boolean(field.flag & PDF_FORM_FIELD_FLAG.READONLY)}
            onCheckedChange={(checked) =>
              toggleFlag(PDF_FORM_FIELD_FLAG.READONLY, checked)
            }
            label="Read only"
          />
          <PropertySwitch
            checked={Boolean(field.flag & PDF_FORM_FIELD_FLAG.REQUIRED)}
            onCheckedChange={(checked) =>
              toggleFlag(PDF_FORM_FIELD_FLAG.REQUIRED, checked)
            }
            label="Required"
          />
          {isTextField ? (
            <>
              <PropertySwitch
                checked={Boolean(
                  field.flag & PDF_FORM_FIELD_FLAG.TEXT_MULTIPLINE
                )}
                onCheckedChange={(checked) =>
                  toggleFlag(PDF_FORM_FIELD_FLAG.TEXT_MULTIPLINE, checked)
                }
                label="Multiline"
              />
              <PropertySwitch
                checked={isComb}
                disabled={maxLength <= 0}
                onCheckedChange={(checked) =>
                  toggleFlag(PDF_FORM_FIELD_FLAG.TEXT_COMB, checked)
                }
                label="Comb of characters"
                description="Requires a max length."
              />
            </>
          ) : null}
          {field.type === PDF_FORM_FIELD_TYPE.LISTBOX ? (
            <PropertySwitch
              checked={Boolean(
                field.flag & PDF_FORM_FIELD_FLAG.CHOICE_MULTL_SELECT
              )}
              onCheckedChange={(checked) =>
                toggleFlag(PDF_FORM_FIELD_FLAG.CHOICE_MULTL_SELECT, checked)
              }
              label="Allow multiple selections"
            />
          ) : null}
        </div>
      </div>
    </PdfEditorSection>
  )
}
/* -------------------------------------------------------------------------- */
/* Panel                                                                      */
/* -------------------------------------------------------------------------- */
export function PdfEditorPropertiesPanel({
  documentId,
}: {
  documentId: string
}) {
  const { provides: annotationCapability } = useAnnotationCapability()
  const { provides: annotation, state } = useAnnotation(documentId)
  const { permissions } = usePdfEditor()
  const [toolSnapshot, setToolSnapshot] = React.useState<{
    source: typeof annotationCapability
    tools: AnnotationTool[]
  } | null>(null)
  React.useEffect(() => {
    if (!annotationCapability) return
    return annotationCapability.onToolsChange(({ tools }) =>
      setToolSnapshot({ source: annotationCapability, tools })
    )
  }, [annotationCapability])
  const selected = React.useMemo(() => getSelectedAnnotations(state), [state])
  const tools =
    toolSnapshot?.source === annotationCapability
      ? toolSnapshot.tools
      : (annotationCapability?.getTools() ?? [])
  const activeTool: AnnotationTool | null =
    state.activeToolId && annotationCapability
      ? (tools.find((tool) => tool.id === state.activeToolId) ?? null)
      : null
  const colorPresets = annotationCapability?.getColorPresets() ?? []
  const selectedToolIds = React.useMemo(
    () =>
      selected
        .map(
          (item) => annotationCapability?.findToolForAnnotation(item.object)?.id
        )
        .filter((id): id is string => Boolean(id)),
    [annotationCapability, selected]
  )
  const isEditing = selected.length > 0
  const propertyKeys = isEditing
    ? getSharedProperties(selectedToolIds)
    : activeTool
      ? (TOOL_PROPERTIES[activeTool.id] ?? [])
      : []
  const configs = propertyKeys
    .map((key) => PROPERTY_CONFIGS[key])
    .filter((config): config is PropertyConfig => Boolean(config))
    .filter((config) => isEditing || !config.editOnly)
  const values = React.useMemo<Record<string, unknown>>(() => {
    if (isEditing)
      return selected[0].object as unknown as Record<string, unknown>
    if (activeTool)
      return activeTool.defaults as unknown as Record<string, unknown>
    return {}
  }, [activeTool, isEditing, selected])
  const canEdit = permissions.canModifyAnnotations
  const structurallyLocked = selected.some((item) =>
    annotation ? annotation.isAnnotationStructurallyLocked(item.object) : false
  )
  const applyPatch = React.useCallback(
    (patch: Record<string, unknown>) => {
      if (isEditing) {
        annotation?.updateAnnotations(
          selected.map((item) => ({
            pageIndex: item.object.pageIndex,
            id: item.object.id,
            patch: {
              ...patch,
              modified: new Date(),
            } as Partial<PdfAnnotationObject>,
          }))
        )
        return
      }
      if (activeTool && annotationCapability) {
        annotationCapability.setToolDefaults(
          activeTool.id,
          patch as Partial<PdfAnnotationObject> & Record<string, unknown>
        )
      }
    },
    [activeTool, annotation, annotationCapability, isEditing, selected]
  )
  const applyRotation = React.useCallback(
    (rotation: number) => {
      if (!annotation || !annotationCapability) return
      for (const item of selected) {
        const current = item.object.rotation ?? 0
        let patch: Partial<PdfAnnotationObject>
        try {
          patch = annotationCapability.transformAnnotation(item.object, {
            type: "rotate",
            changes: { rotation },
            metadata: {
              rotationAngle: rotation,
              rotationDelta: rotation - current,
            },
          })
        } catch {
          patch = { rotation }
        }
        annotation.updateAnnotation(item.object.pageIndex, item.object.id, {
          ...patch,
          modified: new Date(),
        })
      }
    },
    [annotation, annotationCapability, selected]
  )
  const single: TrackedAnnotation | null =
    selected.length === 1 ? selected[0] : null
  const singleMeta = single ? getAnnotationMeta(single.object) : null
  const isWidget = single?.object.type === PdfAnnotationSubtype.WIDGET
  const title = isEditing
    ? selected.length > 1
      ? `${selected.length} annotations`
      : (singleMeta?.label ?? "Annotation")
    : activeTool
      ? `${activeTool.name} defaults`
      : "Properties"
  if (!isEditing && !activeTool) {
    return (
      <PdfEditorEmptyState
        glyph={SlidersGlyph}
        title="Nothing selected"
        description="Select an annotation on the page, or pick a tool to edit its default style."
      />
    )
  }
  const flags = single?.object.flags ?? []
  const contents =
    single && !isWidget && typeof single.object.contents === "string"
      ? single.object.contents
      : ""
  const verticalAlignmentConfig = configs.find(
    (config) => config.kind === "verticalAlign"
  )
  return (
    <div className="space-y-5 p-3">
      <div>
        <div className="text-sm font-medium text-foreground">{title}</div>
        <div className="text-xs text-muted-foreground">
          {isEditing
            ? structurallyLocked
              ? "This annotation is locked. Unlock it to make changes."
              : canEdit
                ? "Changes apply immediately."
                : "This document does not allow annotation changes."
            : "New annotations use these settings."}
        </div>
      </div>

      {configs.length > 0 ? (
        <PdfEditorSection title="Appearance">
          <fieldset
            disabled={isEditing && (!canEdit || structurallyLocked)}
            className="space-y-4 disabled:opacity-60"
          >
            {configs.map((config) => {
              if (
                config.kind === "verticalAlign" &&
                configs.some((item) => item.kind === "textAlign")
              ) {
                return null
              }
              if (config.kind === "textAlign" && verticalAlignmentConfig) {
                return (
                  <div
                    key="alignment-controls"
                    className="grid grid-cols-2 items-start gap-3"
                  >
                    <PropertyControl
                      config={config}
                      values={values}
                      colorPresets={colorPresets}
                      onPatch={applyPatch}
                      onRotate={isEditing ? applyRotation : undefined}
                    />
                    <PropertyControl
                      config={verticalAlignmentConfig}
                      values={values}
                      colorPresets={colorPresets}
                      onPatch={applyPatch}
                      onRotate={isEditing ? applyRotation : undefined}
                    />
                  </div>
                )
              }
              return (
                <PropertyControl
                  key={config.key + config.kind}
                  config={config}
                  values={values}
                  colorPresets={colorPresets}
                  onPatch={applyPatch}
                  onRotate={isEditing ? applyRotation : undefined}
                />
              )
            })}
          </fieldset>
        </PdfEditorSection>
      ) : null}

      {single && isWidget ? (
        <WidgetProperties
          documentId={documentId}
          widget={single.object as PdfWidgetAnnoObject}
        />
      ) : null}

      {single && !isWidget ? (
        <PdfEditorSection title="Note">
          <NoteEditor
            key={single.object.id}
            value={contents}
            disabled={!canEdit}
            onChange={(next) =>
              annotation?.updateAnnotation(
                single.object.pageIndex,
                single.object.id,
                {
                  contents: next,
                  modified: new Date(),
                }
              )
            }
          />
        </PdfEditorSection>
      ) : null}

      {isEditing ? (
        <PdfEditorSection title="Behavior">
          <div className="space-y-0.5">
            {FLAG_OPTIONS.map((option) => {
              const checked = selected.every((item) =>
                (item.object.flags ?? []).includes(option.flag)
              )
              return (
                <PropertySwitch
                  key={option.flag}
                  checked={checked}
                  disabled={!canEdit}
                  onCheckedChange={(next) => {
                    annotation?.updateAnnotations(
                      selected.map((item) => {
                        const current = item.object.flags ?? []
                        const nextFlags = next
                          ? current.includes(option.flag)
                            ? current
                            : [...current, option.flag]
                          : current.filter((flag) => flag !== option.flag)
                        return {
                          pageIndex: item.object.pageIndex,
                          id: item.object.id,
                          patch: { flags: nextFlags },
                        }
                      })
                    )
                  }}
                  label={option.label}
                  description={option.description}
                />
              )
            })}
          </div>
        </PdfEditorSection>
      ) : null}

      {single ? (
        <PdfEditorSection title="Details">
          <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
            <dt className="text-muted-foreground">Type</dt>
            <dd>{singleMeta?.label}</dd>
            <dt className="text-muted-foreground">Page</dt>
            <dd>{single.object.pageIndex + 1}</dd>
            <dt className="text-muted-foreground">Author</dt>
            <dd className="truncate">{single.object.author || "—"}</dd>
            <dt className="text-muted-foreground">Created</dt>
            <dd>{formatDateTime(single.object.created)}</dd>
            <dt className="text-muted-foreground">Modified</dt>
            <dd>{formatDateTime(single.object.modified)}</dd>
            {flags.length > 0 ? (
              <>
                <dt className="text-muted-foreground">Flags</dt>
                <dd className="truncate">{flags.join(", ")}</dd>
              </>
            ) : null}
          </dl>
        </PdfEditorSection>
      ) : null}
    </div>
  )
}
function NoteEditor({
  value,
  disabled,
  onChange,
}: {
  value: string
  disabled?: boolean
  onChange: (value: string) => void
}) {
  const [draft, setDraft] = React.useState(value)
  const [previousValue, setPreviousValue] = React.useState(value)
  if (!Object.is(previousValue, value)) {
    setPreviousValue(value)
    setDraft(value)
  }
  return (
    <textarea
      value={draft}
      disabled={disabled}
      rows={3}
      placeholder="Add a note…"
      onChange={(event) => setDraft(event.target.value)}
      onBlur={() => {
        if (draft !== value) onChange(draft)
      }}
      className={cn(
        "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"
      )}
    />
  )
}
components/extend/pdf-editor-panels.tsx
"use client"

import * as React from "react"
import { useDocumentState, useRegistry } from "@embedpdf/core/react"
import {
  PDF_FORM_FIELD_TYPE,
  PdfActionType,
  PdfAnnotationName,
  PdfAnnotationSubtype,
  PdfZoomMode,
  uuidV4,
  type PdfAttachmentObject,
  type PdfBookmarkObject,
  type PdfDestinationObject,
  type PdfWidgetAnnoObject,
} from "@embedpdf/models"
import {
  getGroupLeaderId,
  getSelectedAnnotations,
  getSidebarAnnotationsWithRepliesGroupedByPage,
  useAnnotation,
  type SidebarAnnotationEntry,
  type TrackedAnnotation,
} from "@embedpdf/plugin-annotation/react"
import { useAttachmentCapability } from "@embedpdf/plugin-attachment/react"
import { useBookmarkCapability } from "@embedpdf/plugin-bookmark/react"
import {
  useFormCapability,
  type FormFieldInfo,
} from "@embedpdf/plugin-form/react"
import {
  useRedaction,
  type RedactionItem,
} from "@embedpdf/plugin-redaction/react"
import { useScrollCapability } from "@embedpdf/plugin-scroll/react"
import {
  SignatureFieldKind,
  useActivePlacement,
  useSignatureEntries,
} from "@embedpdf/plugin-signature/react"
import {
  StampImg,
  useActiveStamp,
  useStampCapability,
  useStampLibraries,
  useStampsByLibrary,
  type StampDefinition,
} from "@embedpdf/plugin-stamp/react"
import {
  ThumbImg,
  useThumbnailPlugin,
  type ThumbMeta,
  type WindowState,
} from "@embedpdf/plugin-thumbnail/react"
import type { degrees, ParseSpeeds, PDFDocument } from "pdf-lib"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { getFormFieldTypeLabel } from "@/components/extend/pdf-editor-properties"
import {
  ArrowDownGlyph,
  ArrowUpGlyph,
  CheckGlyph,
  ChevronDownGlyph,
  ChevronRightGlyph,
  CommentsGlyph,
  downloadArrayBuffer,
  downloadBlob,
  DownloadGlyph,
  EditGlyph,
  EyeOffGlyph,
  FilePlusGlyph,
  formatBytes,
  formatRelativeDate,
  getAnnotationMeta,
  GridGlyph,
  ListTreeGlyph,
  MergeGlyph,
  PaperclipGlyph,
  PdfEditorEmptyState,
  PdfEditorScrollArea,
  PdfEditorToolbarSeparator,
  PdfEditorToolButton,
  PlusGlyph,
  RedactAreaGlyph,
  RotateGlyph,
  ScissorsGlyph,
  SignatureGlyph,
  StampGlyph,
  TextFieldGlyph,
  toArrayBuffer,
  TrashGlyph,
  UploadGlyph,
  usePdfEditor,
  withFileNameSuffix,
} from "@/components/extend/pdf-editor-shared"
import { IconPlaceholder } from "@/components/icon-placeholder"

/* -------------------------------------------------------------------------- */
/* Thumbnails                                                                 */
/* -------------------------------------------------------------------------- */
const THUMBNAIL_FOCUS_RING_CLASS =
  "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"
function createFormFieldStore(
  scope: {
    getFormFields: () => FormFieldInfo[]
    onFormReady: (listener: (fields: FormFieldInfo[]) => void) => () => void
    onFieldValueChange: (listener: () => void) => () => void
  } | null
) {
  let fields: FormFieldInfo[] = scope?.getFormFields() ?? []
  return {
    getSnapshot: () => fields,
    subscribe: (listener: () => void) => {
      if (!scope) return () => {}
      const update = (next: FormFieldInfo[]) => {
        fields = next
        listener()
      }
      const unsubscribeReady = scope.onFormReady(update)
      const unsubscribeChange = scope.onFieldValueChange(() =>
        update(scope.getFormFields())
      )
      update(scope.getFormFields())
      return () => {
        unsubscribeReady()
        unsubscribeChange()
      }
    },
  }
}
export function PdfEditorThumbnailsPanel({
  documentId,
  activePage,
  pageCount,
  onSelectPage,
}: {
  documentId: string
  activePage: number
  pageCount: number
  onSelectPage: (pageNumber: number) => void
}) {
  const { plugin: thumbnailPlugin } = useThumbnailPlugin()
  const listboxId = React.useId()
  const viewportRef = React.useRef<HTMLDivElement | null>(null)
  const scope = React.useMemo(
    () => thumbnailPlugin?.provides().forDocument(documentId) ?? null,
    [documentId, thumbnailPlugin]
  )
  const windowState = React.useSyncExternalStore<WindowState | null>(
    React.useCallback(
      (onStoreChange) => {
        if (!scope) return () => undefined
        return scope.onWindow(() => onStoreChange())
      },
      [scope]
    ),
    React.useCallback(() => scope?.getWindow() ?? null, [scope]),
    () => null
  )
  const paddingY = thumbnailPlugin?.cfg.paddingY ?? 0
  React.useEffect(() => {
    const viewport = viewportRef.current
    if (!viewport || !scope) return
    const updateWindow = () => {
      scope.updateWindow(viewport.scrollTop, viewport.clientHeight)
    }
    updateWindow()
    viewport.addEventListener("scroll", updateWindow, { passive: true })
    const resizeObserver = new ResizeObserver(updateWindow)
    resizeObserver.observe(viewport)
    return () => {
      viewport.removeEventListener("scroll", updateWindow)
      resizeObserver.disconnect()
    }
  }, [scope])
  React.useEffect(() => {
    const viewport = viewportRef.current
    if (!viewport || !scope) return
    return scope.onScrollTo(({ top, behavior }) => {
      viewport.scrollTo({ top, behavior })
    })
  }, [scope])
  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (pageCount < 1) return
      const currentPage = activePage > 0 ? activePage : 1
      let nextPage: number | null = null
      if (event.key === "ArrowDown")
        nextPage = Math.min(pageCount, currentPage + 1)
      else if (event.key === "ArrowUp") nextPage = Math.max(1, currentPage - 1)
      else if (event.key === "Home") nextPage = 1
      else if (event.key === "End") nextPage = pageCount
      if (nextPage === null) return
      event.preventDefault()
      onSelectPage(nextPage)
    },
    [activePage, onSelectPage, pageCount]
  )
  return (
    <PdfEditorScrollArea
      className="h-full w-full"
      viewportClassName="group/pdf-editor-thumbnails px-4 focus-visible:ring-0 focus-visible:ring-offset-0"
      viewportProps={{
        "aria-activedescendant":
          activePage > 0 ? `${listboxId}-page-${activePage}` : undefined,
        "aria-label": "Pages",
        onKeyDown: handleKeyDown,
        onMouseDown: (event) => {
          event.currentTarget.focus({ preventScroll: true })
        },
        role: "listbox",
        style: { paddingBottom: paddingY, paddingTop: paddingY },
        tabIndex: 0,
      }}
      viewportRef={viewportRef}
    >
      <div
        className="relative"
        style={{ height: windowState?.totalHeight ?? 0 }}
      >
        {windowState?.items.map((meta: ThumbMeta) => {
          const pageNumber = meta.pageIndex + 1
          const isActive = pageNumber === activePage
          const imagePadding = meta.padding ?? 0
          return (
            <div
              key={meta.pageIndex}
              className={cn(
                "absolute right-0 left-0 flex justify-center",
                isActive && "z-10"
              )}
              style={{ top: meta.top, height: meta.wrapperHeight }}
            >
              <div
                id={`${listboxId}-page-${pageNumber}`}
                role="option"
                aria-selected={isActive}
                aria-current={isActive ? "page" : undefined}
                aria-label={`Page ${pageNumber}`}
                style={{ maxWidth: meta.width + imagePadding * 2 + 16 }}
                className={cn(
                  "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",
                  isActive
                    ? cn(
                        "bg-sidebar-accent text-foreground",
                        THUMBNAIL_FOCUS_RING_CLASS
                      )
                    : "text-muted-foreground"
                )}
                onClick={() => onSelectPage(pageNumber)}
              >
                <span
                  className="flex items-center justify-center overflow-hidden rounded-md"
                  style={{
                    width: meta.width + imagePadding * 2,
                    height: meta.height + imagePadding * 2,
                    padding: imagePadding,
                  }}
                >
                  <ThumbImg
                    documentId={documentId}
                    meta={meta}
                    className="block rounded-sm object-contain shadow-xs"
                    style={{ width: meta.width, height: meta.height }}
                  />
                </span>
                <span
                  className="flex items-center justify-center tabular-nums"
                  style={{ height: meta.labelHeight }}
                >
                  {pageNumber}
                </span>
              </div>
            </div>
          )
        })}
      </div>
    </PdfEditorScrollArea>
  )
}
/* -------------------------------------------------------------------------- */
/* Outline                                                                    */
/* -------------------------------------------------------------------------- */
function resolveBookmarkDestination(bookmark: PdfBookmarkObject): {
  destination: PdfDestinationObject | null
  uri: string | null
} {
  const target = bookmark.target
  if (!target) return { destination: null, uri: null }
  if (target.type === "destination") {
    return { destination: target.destination, uri: null }
  }
  const action = target.action
  if (
    action.type === PdfActionType.Goto ||
    action.type === PdfActionType.RemoteGoto
  ) {
    return { destination: action.destination, uri: null }
  }
  if (action.type === PdfActionType.URI) {
    return { destination: null, uri: action.uri }
  }
  return { destination: null, uri: null }
}
export function PdfEditorOutlinePanel({ documentId }: { documentId: string }) {
  const { provides: bookmark } = useBookmarkCapability()
  const { provides: scroll } = useScrollCapability()
  const documentState = useDocumentState(documentId)
  const document = documentState?.document ?? null
  const [bookmarks, setBookmarks] = React.useState<PdfBookmarkObject[] | null>(
    null
  )
  const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set())
  React.useEffect(() => {
    if (!bookmark || !document) return
    let cancelled = false
    const task = bookmark.forDocument(documentId).getBookmarks()
    task.wait(
      (result) => {
        if (cancelled) return
        setBookmarks(result.bookmarks)
        setExpanded(
          new Set(result.bookmarks.map((_, index) => `bookmark-${index}`))
        )
      },
      () => {
        if (!cancelled) setBookmarks([])
      }
    )
    return () => {
      cancelled = true
    }
  }, [bookmark, document, documentId])
  const navigate = React.useCallback(
    (item: PdfBookmarkObject) => {
      const { destination, uri } = resolveBookmarkDestination(item)
      if (uri) {
        window.open(uri, "_blank", "noopener")
        return
      }
      if (!destination || !scroll) return
      const scope = scroll.forDocument(documentId)
      const page = document?.pages[destination.pageIndex]
      if (destination.zoom.mode === PdfZoomMode.XYZ && page) {
        scope.scrollToPage({
          pageNumber: destination.pageIndex + 1,
          pageCoordinates: {
            x: destination.zoom.params.x,
            y: page.size.height - destination.zoom.params.y,
          },
          behavior: "smooth",
        })
        return
      }
      scope.scrollToPage({
        pageNumber: destination.pageIndex + 1,
        behavior: "smooth",
      })
    },
    [document, documentId, scroll]
  )
  if (bookmarks === null) {
    return (
      <div className="grid h-full place-items-center">
        <InlineSpinner className="size-4" />
      </div>
    )
  }
  if (bookmarks.length === 0) {
    return (
      <PdfEditorEmptyState
        glyph={ListTreeGlyph}
        title="No outline"
        description="This document does not include bookmarks."
      />
    )
  }
  const renderBookmark = (
    item: PdfBookmarkObject,
    id: string,
    level: number
  ): React.ReactNode => {
    const hasChildren = Boolean(item.children && item.children.length > 0)
    const isExpanded = expanded.has(id)
    return (
      <div key={id}>
        <div
          className="group flex cursor-default items-center gap-1 rounded-md py-1 pr-2 text-sm hover:bg-sidebar-accent"
          style={{ paddingLeft: level * 12 + 4 }}
        >
          {hasChildren ? (
            <button
              type="button"
              aria-label={isExpanded ? "Collapse" : "Expand"}
              className="grid size-5 shrink-0 place-items-center rounded text-muted-foreground hover:text-foreground"
              onClick={(event) => {
                event.stopPropagation()
                setExpanded((previous) => {
                  const next = new Set(previous)
                  if (next.has(id)) next.delete(id)
                  else next.add(id)
                  return next
                })
              }}
            >
              {isExpanded ? (
                <ChevronDownGlyph className="size-3.5" />
              ) : (
                <ChevronRightGlyph className="size-3.5" />
              )}
            </button>
          ) : (
            <span className="size-5 shrink-0" />
          )}
          <button
            type="button"
            className="min-w-0 flex-1 truncate text-left text-foreground/90 outline-none"
            onClick={() => navigate(item)}
          >
            {item.title || "Untitled"}
          </button>
        </div>
        {hasChildren && isExpanded
          ? item.children?.map((child, index) =>
              renderBookmark(child, `${id}-${index}`, level + 1)
            )
          : null}
      </div>
    )
  }
  return (
    <PdfEditorScrollArea className="h-full w-full" viewportClassName="p-2">
      {bookmarks.map((item, index) =>
        renderBookmark(item, `bookmark-${index}`, 0)
      )}
    </PdfEditorScrollArea>
  )
}
/* -------------------------------------------------------------------------- */
/* Attachments                                                                */
/* -------------------------------------------------------------------------- */
export function PdfEditorAttachmentsPanel({
  documentId,
}: {
  documentId: string
}) {
  const { provides: attachment } = useAttachmentCapability()
  const { registry } = useRegistry()
  const documentState = useDocumentState(documentId)
  const { notify, permissions } = usePdfEditor()
  const document = documentState?.document ?? null
  const [attachments, setAttachments] = React.useState<
    PdfAttachmentObject[] | null
  >(null)
  const [version, setVersion] = React.useState(0)
  const [busy, setBusy] = React.useState(false)
  const inputRef = React.useRef<HTMLInputElement>(null)
  React.useEffect(() => {
    if (!attachment || !document) return
    let cancelled = false
    attachment
      .forDocument(documentId)
      .getAttachments()
      .wait(
        (result) => {
          if (!cancelled) setAttachments(result)
        },
        () => {
          if (!cancelled) setAttachments([])
        }
      )
    return () => {
      cancelled = true
    }
  }, [attachment, document, documentId, version])
  const refresh = () => setVersion((value) => value + 1)
  const handleDownload = (item: PdfAttachmentObject) => {
    attachment
      ?.forDocument(documentId)
      .downloadAttachment(item)
      .wait(
        (buffer) =>
          downloadArrayBuffer(
            buffer,
            item.name,
            item.mimeType || "application/octet-stream"
          ),
        () => notify("The attachment could not be downloaded.", "error")
      )
  }
  const handleRemove = (item: PdfAttachmentObject) => {
    if (!registry || !document) return
    setBusy(true)
    registry
      .getEngine()
      .removeAttachment(document, item)
      .wait(
        () => {
          setBusy(false)
          refresh()
          notify("Attachment removed.", "success")
        },
        () => {
          setBusy(false)
          notify("The attachment could not be removed.", "error")
        }
      )
  }
  const handleAdd = async (file: File) => {
    if (!registry || !document) return
    setBusy(true)
    try {
      const data = await file.arrayBuffer()
      registry
        .getEngine()
        .addAttachment(document, {
          name: file.name,
          description: "",
          mimeType: file.type || "application/octet-stream",
          data,
        })
        .wait(
          () => {
            setBusy(false)
            refresh()
            notify("Attachment added.", "success")
          },
          () => {
            setBusy(false)
            notify("The attachment could not be added.", "error")
          }
        )
    } catch {
      setBusy(false)
      notify("The file could not be read.", "error")
    }
  }
  const canModify = permissions.canModifyContents && !busy
  return (
    <div className="flex h-full flex-col">
      <div className="flex shrink-0 items-center gap-2 border-b px-3 py-2">
        <input
          ref={inputRef}
          type="file"
          className="sr-only"
          tabIndex={-1}
          onChange={(event) => {
            const file = event.target.files?.[0]
            if (file) void handleAdd(file)
            event.currentTarget.value = ""
          }}
        />
        <Button
          type="button"
          size="sm"
          variant="outline"
          disabled={!canModify}
          onClick={() => inputRef.current?.click()}
        >
          <PlusGlyph className="size-4" />
          Attach file
        </Button>
        {busy ? <InlineSpinner className="size-4" /> : null}
      </div>
      {attachments === null ? (
        <div className="grid flex-1 place-items-center">
          <InlineSpinner className="size-4" />
        </div>
      ) : attachments.length === 0 ? (
        <PdfEditorEmptyState
          glyph={PaperclipGlyph}
          title="No attachments"
          description="Files embedded in the PDF appear here."
        />
      ) : (
        <PdfEditorScrollArea className="min-h-0 flex-1" viewportClassName="p-2">
          <div className="space-y-1">
            {attachments.map((item) => (
              <div
                key={`${item.index}-${item.name}`}
                className="flex items-center gap-2 rounded-lg border px-2.5 py-2"
              >
                <div className="grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground">
                  <PaperclipGlyph className="size-4" />
                </div>
                <div className="min-w-0 flex-1">
                  <div className="truncate text-sm font-medium">
                    {item.name}
                  </div>
                  <div className="truncate text-xs text-muted-foreground">
                    {[item.mimeType, formatBytes(item.size), item.description]
                      .filter(Boolean)
                      .join(" · ")}
                  </div>
                </div>
                <PdfEditorToolButton
                  label="Download"
                  size="icon-xs"
                  onClick={() => handleDownload(item)}
                >
                  <DownloadGlyph className="size-3.5" />
                </PdfEditorToolButton>
                <PdfEditorToolButton
                  label="Remove"
                  size="icon-xs"
                  disabled={!canModify}
                  onClick={() => handleRemove(item)}
                >
                  <TrashGlyph className="size-3.5" />
                </PdfEditorToolButton>
              </div>
            ))}
          </div>
        </PdfEditorScrollArea>
      )}
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Pages (organizer)                                                          */
/* -------------------------------------------------------------------------- */
const PAGE_TILE_WIDTH = 76
type PdfLibModule = {
  PDFDocument: typeof PDFDocument
  ParseSpeeds: typeof ParseSpeeds
  degrees: typeof degrees
}
let pdfLibPromise: Promise<PdfLibModule> | null = null
function loadPdfLib() {
  pdfLibPromise ??= import("pdf-lib")
  return pdfLibPromise
}
export function PdfEditorPagesPanel({
  documentId,
  activePage,
}: {
  documentId: string
  activePage: number
}) {
  const { registry } = useRegistry()
  const documentState = useDocumentState(documentId)
  const { state: annotationState, provides: annotation } =
    useAnnotation(documentId)
  const {
    fileName,
    getDocumentBuffer,
    notify,
    openDialog,
    permissions,
    replaceDocument,
    scrollToPage,
  } = usePdfEditor()
  const document = documentState?.document ?? null
  const pages = React.useMemo(() => document?.pages ?? [], [document])
  const [selected, setSelected] = React.useState<Set<number>>(() => new Set())
  const [busy, setBusy] = React.useState<string | null>(null)
  const anchorRef = React.useRef<number | null>(null)
  const appendInputRef = React.useRef<HTMLInputElement>(null)
  React.useEffect(() => {
    void loadPdfLib()
  }, [])
  const metas = React.useMemo<ThumbMeta[]>(
    () =>
      pages.map((page) => {
        const height = Math.max(
          1,
          Math.round(PAGE_TILE_WIDTH * (page.size.height / page.size.width))
        )
        return {
          pageIndex: page.index,
          width: PAGE_TILE_WIDTH,
          height,
          wrapperHeight: height,
          top: 0,
          labelHeight: 0,
          padding: 0,
        }
      }),
    [pages]
  )
  const selectedIndices = React.useMemo(
    () => [...selected].sort((a, b) => a - b),
    [selected]
  )
  const isEncrypted = document?.isEncrypted ?? false
  const canAssemble =
    permissions.canAssembleDocument && !isEncrypted && busy === null
  const hasSelection = selectedIndices.length > 0
  const isContiguous =
    hasSelection &&
    selectedIndices[selectedIndices.length - 1] - selectedIndices[0] ===
      selectedIndices.length - 1
  const select = (pageIndex: number, mode: "replace" | "toggle" | "range") => {
    setSelected((previous) => {
      if (mode === "range" && anchorRef.current !== null) {
        const start = Math.min(anchorRef.current, pageIndex)
        const end = Math.max(anchorRef.current, pageIndex)
        const next = new Set<number>()
        for (let index = start; index <= end; index += 1) next.add(index)
        return next
      }
      if (mode === "toggle") {
        const next = new Set(previous)
        if (next.has(pageIndex)) next.delete(pageIndex)
        else next.add(pageIndex)
        anchorRef.current = pageIndex
        return next
      }
      anchorRef.current = pageIndex
      return new Set([pageIndex])
    })
  }
  const runOperation = async (
    label: string,
    mutate: (doc: PDFDocument, lib: PdfLibModule) => Promise<void> | void,
    options: {
      replace?: boolean
    } = { replace: true }
  ) => {
    setBusy(label)
    try {
      const [buffer, lib] = await Promise.all([
        getDocumentBuffer(),
        loadPdfLib(),
      ])
      const doc = await lib.PDFDocument.load(buffer, {
        ignoreEncryption: true,
        parseSpeed: lib.ParseSpeeds.Fastest,
        updateMetadata: false,
      })
      await mutate(doc, lib)
      if (options.replace !== false) {
        const bytes = await doc.save({ objectsPerTick: 500 })
        replaceDocument(toArrayBuffer(bytes), fileName)
        notify(`${label} applied.`, "success")
      }
    } catch (error) {
      console.error(error)
      notify(`${label} failed.`, "error")
    } finally {
      setBusy(null)
    }
  }
  const rotateSelected = (delta: 90 | -90) =>
    runOperation("Rotate pages", (doc, lib) => {
      for (const index of selectedIndices) {
        const page = doc.getPage(index)
        page.setRotation(
          lib.degrees((page.getRotation().angle + delta + 360) % 360)
        )
      }
    })
  const deleteSelected = () => {
    if (selectedIndices.length >= pages.length) {
      notify("A document needs at least one page.", "error")
      return
    }
    openDialog({
      type: "confirm",
      title: `Delete ${selectedIndices.length === 1 ? "this page" : `${selectedIndices.length} pages`}?`,
      description:
        "Content and annotations on the deleted pages are removed permanently.",
      confirmLabel: "Delete",
      destructive: true,
      onConfirm: async () => {
        setBusy("Delete pages")
        try {
          if (!registry || !document) {
            throw new Error("The document is not ready.")
          }
          if (annotation && annotationState.hasPendingChanges) {
            await annotation.commit().toPromise()
          }
          const engine = registry.getEngine()
          for (const index of [...selectedIndices].reverse()) {
            await engine.deletePage(document, index).toPromise()
          }
          const buffer = await engine.saveAsCopy(document).toPromise()
          replaceDocument(buffer, fileName)
          notify("Delete pages applied.", "success")
        } catch (error) {
          console.error(error)
          notify("Delete pages failed.", "error")
        } finally {
          setBusy(null)
        }
      },
    })
  }
  const moveSelected = (direction: -1 | 1) => {
    const first = selectedIndices[0]
    const last = selectedIndices[selectedIndices.length - 1]
    if (direction === -1 && first === 0) return
    if (direction === 1 && last === pages.length - 1) return
    void runOperation("Move pages", (doc) => {
      // pdf-lib has no move API: detach the neighbouring page from the page
      // tree and re-insert it on the other side of the selected block.
      if (direction === -1) {
        const page = doc.getPage(first - 1)
        doc.removePage(first - 1)
        doc.insertPage(last, page)
      } else {
        const page = doc.getPage(last + 1)
        doc.removePage(last + 1)
        doc.insertPage(first, page)
      }
    })
  }
  const insertBlankAfter = () => {
    const referenceIndex = hasSelection
      ? selectedIndices[selectedIndices.length - 1]
      : Math.max(0, activePage - 1)
    void runOperation("Insert page", (doc) => {
      const reference = doc.getPage(referenceIndex)
      const { width, height } = reference.getSize()
      doc.insertPage(referenceIndex + 1, [width, height])
    })
  }
  const extractSelected = () =>
    runOperation(
      "Extract pages",
      async (doc, lib) => {
        const output = await lib.PDFDocument.create()
        const copied = await output.copyPages(doc, selectedIndices)
        copied.forEach((page) => output.addPage(page))
        const bytes = await output.save()
        downloadArrayBuffer(bytes, withFileNameSuffix(fileName, "-pages"))
        notify(
          `Extracted ${selectedIndices.length} page${selectedIndices.length === 1 ? "" : "s"}.`,
          "success"
        )
      },
      { replace: false }
    )
  const appendFile = (file: File) =>
    runOperation("Append PDF", async (doc, lib) => {
      const source = await lib.PDFDocument.load(await file.arrayBuffer(), {
        ignoreEncryption: true,
        parseSpeed: lib.ParseSpeeds.Fastest,
        updateMetadata: false,
      })
      const copied = await doc.copyPages(source, source.getPageIndices())
      copied.forEach((page) => doc.addPage(page))
    })
  return (
    <div className="flex h-full flex-col">
      <div className="flex shrink-0 flex-wrap items-center gap-0.5 border-b px-2 py-1.5">
        <PdfEditorToolButton
          label="Rotate left"
          disabled={!canAssemble || !hasSelection}
          onClick={() => rotateSelected(-90)}
        >
          <RotateGlyph className="size-4 -scale-x-100" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Rotate right"
          disabled={!canAssemble || !hasSelection}
          onClick={() => rotateSelected(90)}
        >
          <RotateGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolbarSeparator />
        <PdfEditorToolButton
          label="Move up"
          disabled={!canAssemble || !isContiguous || selectedIndices[0] === 0}
          onClick={() => moveSelected(-1)}
        >
          <ArrowUpGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Move down"
          disabled={
            !canAssemble ||
            !isContiguous ||
            selectedIndices[selectedIndices.length - 1] === pages.length - 1
          }
          onClick={() => moveSelected(1)}
        >
          <ArrowDownGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolbarSeparator />
        <PdfEditorToolButton
          label="Insert blank page after"
          disabled={!canAssemble}
          onClick={insertBlankAfter}
        >
          <FilePlusGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Append PDF"
          disabled={!canAssemble}
          onClick={() => appendInputRef.current?.click()}
        >
          <MergeGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Extract selected pages"
          disabled={busy !== null || !hasSelection}
          onClick={() => void extractSelected()}
        >
          <ScissorsGlyph className="size-4" />
        </PdfEditorToolButton>
        <PdfEditorToolButton
          label="Delete pages"
          disabled={!canAssemble || !hasSelection}
          onClick={deleteSelected}
        >
          <TrashGlyph className="size-4" />
        </PdfEditorToolButton>
        <input
          ref={appendInputRef}
          type="file"
          accept="application/pdf,.pdf"
          className="sr-only"
          tabIndex={-1}
          onChange={(event) => {
            const file = event.target.files?.[0]
            if (file) void appendFile(file)
            event.currentTarget.value = ""
          }}
        />
      </div>
      <div className="flex shrink-0 items-center justify-between gap-2 border-b px-3 py-1.5 text-xs text-muted-foreground">
        <span>
          {hasSelection
            ? `${selectedIndices.length} of ${pages.length} selected`
            : `${pages.length} page${pages.length === 1 ? "" : "s"}`}
        </span>
        {busy ? (
          <span className="flex items-center gap-1.5">
            <InlineSpinner className="size-3" />
            {busy}…
          </span>
        ) : isEncrypted ? (
          <span>Remove protection to edit pages.</span>
        ) : hasSelection ? (
          <button
            type="button"
            className="underline-offset-2 hover:underline"
            onClick={() => setSelected(new Set())}
          >
            Clear
          </button>
        ) : null}
      </div>
      <PdfEditorScrollArea
        className="min-h-0 flex-1"
        viewportClassName="px-2.5 py-3"
      >
        <div
          data-slot="pdf-editor-pages-grid"
          className="grid grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] items-start gap-2"
        >
          {metas.map((meta) => {
            const pageNumber = meta.pageIndex + 1
            const isSelected = selected.has(meta.pageIndex)
            const isActive = pageNumber === activePage
            return (
              <button
                key={meta.pageIndex}
                type="button"
                aria-pressed={isSelected}
                className={cn(
                  "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",
                  isSelected
                    ? "border-primary bg-primary/5"
                    : "border-transparent hover:bg-sidebar-accent",
                  isActive && !isSelected && "border-border"
                )}
                onClick={(event) => {
                  select(
                    meta.pageIndex,
                    event.shiftKey
                      ? "range"
                      : event.metaKey || event.ctrlKey
                        ? "toggle"
                        : "replace"
                  )
                }}
                onDoubleClick={() => scrollToPage(pageNumber)}
              >
                <span className="grid w-full place-items-center rounded-md border border-border/60 bg-muted/30 p-1">
                  <span
                    className="grid w-full place-items-center overflow-hidden rounded-sm bg-white shadow-xs"
                    style={{ aspectRatio: `${meta.width} / ${meta.height}` }}
                  >
                    <ThumbImg
                      documentId={documentId}
                      meta={meta}
                      className="block size-full object-contain"
                    />
                  </span>
                </span>
                <span
                  className={cn(
                    "tabular-nums",
                    isSelected ? "text-foreground" : "text-muted-foreground"
                  )}
                >
                  {pageNumber}
                </span>
              </button>
            )
          })}
        </div>
      </PdfEditorScrollArea>
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Comments                                                                   */
/* -------------------------------------------------------------------------- */
function CommentComposer({
  placeholder,
  initialValue = "",
  autoFocus,
  submitLabel = "Save",
  onSubmit,
  onCancel,
}: {
  placeholder: string
  initialValue?: string
  autoFocus?: boolean
  submitLabel?: string
  onSubmit: (value: string) => void
  onCancel?: () => void
}) {
  const [value, setValue] = React.useState(initialValue)
  return (
    <div className="space-y-1.5">
      <textarea
        value={value}
        autoFocus={autoFocus}
        rows={2}
        placeholder={placeholder}
        onChange={(event) => setValue(event.target.value)}
        onKeyDown={(event) => {
          if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
            event.preventDefault()
            if (value.trim()) onSubmit(value.trim())
          }
          if (event.key === "Escape") onCancel?.()
        }}
        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"
      />
      <div className="flex justify-end gap-1">
        {onCancel ? (
          <Button type="button" size="xs" variant="ghost" onClick={onCancel}>
            Cancel
          </Button>
        ) : null}
        <Button
          type="button"
          size="xs"
          disabled={!value.trim()}
          onClick={() => onSubmit(value.trim())}
        >
          {submitLabel}
        </Button>
      </div>
    </div>
  )
}
function CommentCard({
  entry,
  isSelected,
  readOnly,
  onSelect,
  onUpdate,
  onDelete,
  onReply,
}: {
  entry: SidebarAnnotationEntry
  isSelected: boolean
  readOnly: boolean
  onSelect: () => void
  onUpdate: (annotation: TrackedAnnotation, contents: string) => void
  onDelete: (annotation: TrackedAnnotation) => void
  onReply: (annotation: TrackedAnnotation, contents: string) => void
}) {
  const { annotation, replies } = entry
  const [isEditing, setIsEditing] = React.useState(false)
  const [isReplying, setIsReplying] = React.useState(false)
  const meta = getAnnotationMeta(annotation.object)
  const contents =
    typeof annotation.object.contents === "string"
      ? annotation.object.contents
      : ""
  const quotedText =
    annotation.object.custom &&
    typeof annotation.object.custom === "object" &&
    typeof (
      annotation.object.custom as {
        text?: unknown
      }
    ).text === "string"
      ? ((
          annotation.object.custom as {
            text: string
          }
        ).text as string)
      : null
  const Glyph = meta.glyph
  return (
    <div
      role="button"
      tabIndex={0}
      onClick={onSelect}
      onKeyDown={(event) => {
        if (event.key === "Enter") onSelect()
      }}
      className={cn(
        "rounded-lg border bg-background p-3 text-left shadow-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
        isSelected
          ? "border-primary ring-1 ring-primary/40"
          : "hover:bg-accent/40"
      )}
    >
      <div className="flex items-start gap-2">
        <span
          className="grid size-7 shrink-0 place-items-center rounded-full bg-muted"
          style={meta.color ? { color: meta.color } : undefined}
        >
          <Glyph className="size-3.5" />
        </span>
        <div className="min-w-0 flex-1">
          <div className="flex items-center justify-between gap-2">
            <div className="min-w-0">
              <div className="truncate text-sm font-medium">
                {annotation.object.author || "Guest"}
              </div>
              <div className="text-[11px] text-muted-foreground">
                {meta.label} · page {annotation.object.pageIndex + 1}
                {annotation.object.modified || annotation.object.created
                  ? ` · ${formatRelativeDate(annotation.object.modified ?? annotation.object.created)}`
                  : ""}
              </div>
            </div>
            {!readOnly ? (
              <div className="flex shrink-0 items-center gap-0.5">
                <PdfEditorToolButton
                  label={contents ? "Edit comment" : "Add comment"}
                  size="icon-xs"
                  onClick={(event) => {
                    event.stopPropagation()
                    setIsEditing(true)
                  }}
                >
                  <EditGlyph className="size-3.5" />
                </PdfEditorToolButton>
                <PdfEditorToolButton
                  label="Delete"
                  size="icon-xs"
                  onClick={(event) => {
                    event.stopPropagation()
                    onDelete(annotation)
                  }}
                >
                  <TrashGlyph className="size-3.5" />
                </PdfEditorToolButton>
              </div>
            ) : null}
          </div>
          {quotedText ? (
            <blockquote className="mt-1.5 line-clamp-3 border-l-2 pl-2 text-xs text-muted-foreground italic">
              {quotedText}
            </blockquote>
          ) : null}
          {isEditing ? (
            <div className="mt-2" onClick={(event) => event.stopPropagation()}>
              <CommentComposer
                placeholder="Write a comment…"
                initialValue={contents}
                autoFocus
                onSubmit={(value) => {
                  onUpdate(annotation, value)
                  setIsEditing(false)
                }}
                onCancel={() => setIsEditing(false)}
              />
            </div>
          ) : contents ? (
            <p className="mt-1.5 text-sm whitespace-pre-wrap">{contents}</p>
          ) : (
            <p className="mt-1.5 text-xs text-muted-foreground">
              No comment yet.
            </p>
          )}
        </div>
      </div>
      {replies.length > 0 ? (
        <div className="mt-2 space-y-2 border-l pl-3">
          {replies.map((reply) => (
            <div key={reply.object.id} className="text-sm">
              <div className="flex items-center justify-between gap-2">
                <span className="text-xs font-medium">
                  {reply.object.author || "Guest"}
                  <span className="ml-1.5 font-normal text-muted-foreground">
                    {formatRelativeDate(
                      reply.object.modified ?? reply.object.created
                    )}
                  </span>
                </span>
                {!readOnly ? (
                  <PdfEditorToolButton
                    label="Delete reply"
                    size="icon-xs"
                    onClick={(event) => {
                      event.stopPropagation()
                      onDelete(reply)
                    }}
                  >
                    <TrashGlyph className="size-3" />
                  </PdfEditorToolButton>
                ) : null}
              </div>
              <p className="whitespace-pre-wrap">{reply.object.contents}</p>
            </div>
          ))}
        </div>
      ) : null}
      {!readOnly ? (
        <div className="mt-2" onClick={(event) => event.stopPropagation()}>
          {isReplying ? (
            <CommentComposer
              placeholder="Reply…"
              autoFocus
              submitLabel="Reply"
              onSubmit={(value) => {
                onReply(annotation, value)
                setIsReplying(false)
              }}
              onCancel={() => setIsReplying(false)}
            />
          ) : (
            <Button
              type="button"
              size="xs"
              variant="ghost"
              className="text-muted-foreground"
              onClick={() => setIsReplying(true)}
            >
              Reply
            </Button>
          )}
        </div>
      ) : null}
    </div>
  )
}
export function PdfEditorCommentsPanel({ documentId }: { documentId: string }) {
  const { provides: annotation, state } = useAnnotation(documentId)
  const { annotationAuthor, permissions, scrollToAnnotation } = usePdfEditor()
  const grouped = React.useMemo(
    () => getSidebarAnnotationsWithRepliesGroupedByPage(state),
    [state]
  )
  const selectedLeaderIds = React.useMemo(() => {
    const ids = new Set<string>()
    for (const item of getSelectedAnnotations(state)) {
      ids.add(getGroupLeaderId(state, item.object.id) ?? item.object.id)
    }
    return ids
  }, [state])
  const pageNumbers = React.useMemo(
    () =>
      Object.keys(grouped)
        .map(Number)
        .sort((a, b) => a - b),
    [grouped]
  )
  const readOnly = !permissions.canModifyAnnotations
  const cardRefs = React.useRef<Map<string, HTMLDivElement>>(new Map())
  const firstSelectedId = selectedLeaderIds.values().next().value
  React.useEffect(() => {
    if (!firstSelectedId) return
    const element = cardRefs.current.get(firstSelectedId)
    element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
  }, [firstSelectedId])
  if (pageNumbers.length === 0) {
    return (
      <PdfEditorEmptyState
        glyph={CommentsGlyph}
        title="No comments yet"
        description="Annotations and notes you add to the document show up here."
      />
    )
  }
  return (
    <PdfEditorScrollArea className="h-full w-full" viewportClassName="p-3">
      <div className="space-y-5">
        {pageNumbers.map((pageIndex) => (
          <div key={pageIndex} className="space-y-2">
            <div className="text-xs font-medium text-muted-foreground">
              Page {pageIndex + 1}
            </div>
            <div className="space-y-2">
              {grouped[pageIndex].map((entry) => (
                <div
                  key={entry.annotation.object.id}
                  ref={(element) => {
                    if (element) {
                      cardRefs.current.set(entry.annotation.object.id, element)
                    } else {
                      cardRefs.current.delete(entry.annotation.object.id)
                    }
                  }}
                >
                  <CommentCard
                    entry={entry}
                    isSelected={selectedLeaderIds.has(
                      entry.annotation.object.id
                    )}
                    readOnly={readOnly}
                    onSelect={() => {
                      annotation?.selectAnnotation(
                        entry.annotation.object.pageIndex,
                        entry.annotation.object.id
                      )
                      scrollToAnnotation(entry.annotation.object)
                    }}
                    onUpdate={(item, contents) =>
                      annotation?.updateAnnotation(
                        item.object.pageIndex,
                        item.object.id,
                        {
                          contents,
                          modified: new Date(),
                        }
                      )
                    }
                    onDelete={(item) =>
                      annotation?.deleteAnnotation(
                        item.object.pageIndex,
                        item.object.id
                      )
                    }
                    onReply={(parent, contents) =>
                      annotation?.createAnnotation(parent.object.pageIndex, {
                        id: uuidV4(),
                        type: PdfAnnotationSubtype.TEXT,
                        pageIndex: parent.object.pageIndex,
                        rect: {
                          origin: {
                            x: parent.object.rect.origin.x,
                            y: parent.object.rect.origin.y,
                          },
                          size: { width: 24, height: 24 },
                        },
                        contents,
                        inReplyToId: parent.object.id,
                        flags: ["noRotate", "noZoom", "print"],
                        name: PdfAnnotationName.Comment,
                        author: annotationAuthor,
                        created: new Date(),
                        modified: new Date(),
                      })
                    }
                  />
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </PdfEditorScrollArea>
  )
}
/* -------------------------------------------------------------------------- */
/* Redactions                                                                 */
/* -------------------------------------------------------------------------- */
export function PdfEditorRedactionsPanel({
  documentId,
}: {
  documentId: string
}) {
  const { state, provides } = useRedaction(documentId)
  const { provides: scroll } = useScrollCapability()
  const { openDialog, permissions, notify } = usePdfEditor()
  const items = React.useMemo(
    () =>
      Object.entries(state.pending)
        .flatMap(([page, pageItems]) =>
          pageItems.map((item) => ({ item, pageNumber: Number(page) + 1 }))
        )
        .sort((a, b) => a.pageNumber - b.pageNumber),
    [state.pending]
  )
  const [isApplying, setIsApplying] = React.useState(false)
  const canApply = permissions.canModifyContents && !isApplying
  const selectItem = (item: RedactionItem) => {
    provides?.selectPending(item.page, item.id)
    scroll?.forDocument(documentId).scrollToPage({
      pageNumber: item.page + 1,
      pageCoordinates: { x: item.rect.origin.x, y: item.rect.origin.y },
      alignX: 50,
      alignY: 25,
      behavior: "smooth",
    })
  }
  const applyAll = () =>
    openDialog({
      type: "confirm",
      title: `Apply ${items.length} redaction${items.length === 1 ? "" : "s"}?`,
      description:
        "Redaction permanently removes the marked content from the document. This cannot be undone.",
      confirmLabel: "Apply redactions",
      onConfirm: () => {
        if (!provides) return
        setIsApplying(true)
        provides.commitAllPending().wait(
          () => setIsApplying(false),
          () => {
            setIsApplying(false)
            notify("The redactions could not be applied.", "error")
          }
        )
      },
    })
  const applyOne = (item: RedactionItem) =>
    openDialog({
      type: "confirm",
      title: "Apply this redaction?",
      description: "The marked content is removed permanently.",
      confirmLabel: "Apply",
      onConfirm: () => {
        provides?.commitPending(item.page, item.id).wait(
          () => undefined,
          () => notify("The redaction could not be applied.", "error")
        )
      },
    })
  return (
    <div className="flex h-full flex-col">
      {items.length === 0 ? (
        <PdfEditorEmptyState
          glyph={EyeOffGlyph}
          title="No pending redactions"
          description="Use the Redact tools to mark text or areas. Marks stay editable until you apply them."
        />
      ) : (
        <PdfEditorScrollArea className="min-h-0 flex-1" viewportClassName="p-2">
          <div className="space-y-1.5">
            {items.map(({ item, pageNumber }) => {
              const isSelected = state.selected?.id === item.id
              const Glyph = item.kind === "text" ? EyeOffGlyph : RedactAreaGlyph
              return (
                <div
                  key={item.id}
                  role="button"
                  tabIndex={0}
                  onClick={() => selectItem(item)}
                  onKeyDown={(event) => {
                    if (event.key === "Enter") selectItem(item)
                  }}
                  className={cn(
                    "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",
                    isSelected
                      ? "border-primary ring-1 ring-primary/40"
                      : "hover:bg-accent/40"
                  )}
                >
                  <span
                    className="mt-0.5 grid size-6 shrink-0 place-items-center rounded-md"
                    style={{
                      backgroundColor: `${item.markColor}22`,
                      color: item.markColor,
                    }}
                  >
                    <Glyph className="size-3.5" />
                  </span>
                  <div className="min-w-0 flex-1">
                    <div className="text-sm font-medium">
                      {item.kind === "text" ? "Text" : "Area"}
                      <span className="ml-1.5 text-xs font-normal text-muted-foreground">
                        page {pageNumber}
                      </span>
                    </div>
                    {item.kind === "text" && item.text ? (
                      <div className="truncate text-xs text-muted-foreground italic">
                        “{item.text}”
                      </div>
                    ) : null}
                  </div>
                  <div className="flex shrink-0 items-center gap-0.5">
                    <PdfEditorToolButton
                      label="Apply"
                      size="icon-xs"
                      disabled={!canApply}
                      onClick={(event) => {
                        event.stopPropagation()
                        applyOne(item)
                      }}
                    >
                      <CheckGlyph className="size-3.5" />
                    </PdfEditorToolButton>
                    <PdfEditorToolButton
                      label="Remove mark"
                      size="icon-xs"
                      onClick={(event) => {
                        event.stopPropagation()
                        provides?.removePending(item.page, item.id)
                      }}
                    >
                      <TrashGlyph className="size-3.5" />
                    </PdfEditorToolButton>
                  </div>
                </div>
              )
            })}
          </div>
        </PdfEditorScrollArea>
      )}
      {items.length > 0 ? (
        <div className="flex shrink-0 gap-2 border-t p-2">
          <Button
            type="button"
            variant="outline"
            size="sm"
            className="flex-1"
            disabled={isApplying}
            onClick={() => provides?.clearPending()}
          >
            Clear all
          </Button>
          <InlineLoadingButton
            type="button"
            size="sm"
            className="flex-1"
            loading={isApplying}
            disabled={!canApply}
            onClick={applyAll}
          >
            Apply all
          </InlineLoadingButton>
        </div>
      ) : null}
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Stamps                                                                     */
/* -------------------------------------------------------------------------- */
const CUSTOM_STAMP_LIBRARY_ID = "custom"
const STAMP_THUMB_WIDTH = 120
const UNSUPPORTED_STAMP_SOURCE_TYPES = new Set<PdfAnnotationSubtype>([
  PdfAnnotationSubtype.REDACT,
  PdfAnnotationSubtype.HIGHLIGHT,
  PdfAnnotationSubtype.SQUIGGLY,
  PdfAnnotationSubtype.UNDERLINE,
  PdfAnnotationSubtype.STRIKEOUT,
  PdfAnnotationSubtype.CARET,
  PdfAnnotationSubtype.WIDGET,
  PdfAnnotationSubtype.LINK,
])
export function PdfEditorStampsPanel({ documentId }: { documentId: string }) {
  const { libraries } = useStampLibraries()
  const { provides: stampCapability } = useStampCapability()
  const { provides: annotation, state: annotationState } =
    useAnnotation(documentId)
  const { notify, permissions } = usePdfEditor()
  const [libraryId, setLibraryId] = React.useState("all")
  const stamps = useStampsByLibrary(libraryId === "all" ? undefined : libraryId)
  const activeStamp = useActiveStamp(documentId)
  const [busy, setBusy] = React.useState(false)
  const importInputRef = React.useRef<HTMLInputElement>(null)
  const selected = React.useMemo(
    () => getSelectedAnnotations(annotationState),
    [annotationState]
  )
  const canCreateFromSelection =
    selected.length === 1 &&
    !UNSUPPORTED_STAMP_SOURCE_TYPES.has(selected[0].object.type)
  const customLibrary = libraries.find(
    (library) => library.id === CUSTOM_STAMP_LIBRARY_ID
  )
  const libraryOptions = [
    { label: "All libraries", value: "all" },
    ...libraries.map((library) => ({
      label: library.name,
      value: library.id,
    })),
  ]
  if (
    libraryId !== "all" &&
    !libraries.some((library) => library.id === libraryId)
  ) {
    setLibraryId("all")
  }
  const placeStamp = (targetLibraryId: string, stamp: StampDefinition) => {
    if (!stampCapability) return
    const isActive =
      activeStamp?.libraryId === targetLibraryId &&
      activeStamp.stamp.id === stamp.id
    if (isActive) {
      annotation?.setActiveTool(null)
      return
    }
    stampCapability
      .forDocument(documentId)
      .activateStampPlacement(targetLibraryId, stamp)
      .wait(
        () => undefined,
        () => notify("The stamp could not be activated.", "error")
      )
  }
  const createFromSelection = () => {
    if (!stampCapability || !canCreateFromSelection) return
    const count = customLibrary?.stamps.length ?? 0
    setBusy(true)
    stampCapability
      .forDocument(documentId)
      .createStampFromAnnotation(
        selected[0].object,
        {
          name: PdfAnnotationName.Custom,
          subject: `Custom stamp ${count + 1}`,
          categories: ["custom", "sidebar"],
        },
        CUSTOM_STAMP_LIBRARY_ID
      )
      .wait(
        () => {
          setBusy(false)
          setLibraryId(CUSTOM_STAMP_LIBRARY_ID)
          notify("Stamp saved to your custom library.", "success")
        },
        () => {
          setBusy(false)
          notify("The stamp could not be created from the selection.", "error")
        }
      )
  }
  const exportLibrary = (targetLibraryId: string) => {
    stampCapability?.exportLibrary(targetLibraryId).wait(
      (exported) => downloadArrayBuffer(exported.pdf, `${exported.name}.pdf`),
      () => notify("The library could not be exported.", "error")
    )
  }
  const importLibrary = async (file: File) => {
    if (!stampCapability) return
    setBusy(true)
    try {
      const buffer = await file.arrayBuffer()
      const lib = await import("pdf-lib")
      const pageCount = (
        await lib.PDFDocument.load(buffer, { ignoreEncryption: true })
      ).getPageCount()
      const name = file.name.replace(/\.pdf$/i, "") || "Imported stamps"
      stampCapability
        .loadLibrary({
          name,
          pdf: buffer,
          categories: ["sidebar"],
          stamps: Array.from({ length: pageCount }, (_, index) => ({
            id: `page-${index + 1}`,
            pageIndex: index,
            name: PdfAnnotationName.Custom,
            subject: `${name} ${index + 1}`,
          })),
        })
        .wait(
          (id) => {
            setBusy(false)
            setLibraryId(id)
            notify(
              `Imported ${pageCount} stamp${pageCount === 1 ? "" : "s"}.`,
              "success"
            )
          },
          () => {
            setBusy(false)
            notify("The stamp library could not be imported.", "error")
          }
        )
    } catch {
      setBusy(false)
      notify("The file could not be read as a PDF.", "error")
    }
  }
  return (
    <div className="flex h-full flex-col">
      <div className="shrink-0 space-y-2 border-b px-3 py-2">
        <div className="flex items-center gap-1.5">
          <Select
            items={libraryOptions}
            value={libraryId}
            onValueChange={(value) => setLibraryId(String(value))}
          >
            <SelectTrigger size="sm" className="min-w-0 flex-1">
              <SelectValue placeholder="Library" />
            </SelectTrigger>
            <SelectContent alignItemWithTrigger={false}>
              {libraryOptions.map((option) => (
                <SelectItem key={option.value} value={option.value}>
                  {option.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <PdfEditorToolButton
            label="Import stamps from a PDF"
            disabled={busy}
            onClick={() => importInputRef.current?.click()}
          >
            <UploadGlyph className="size-4" />
          </PdfEditorToolButton>
          <PdfEditorToolButton
            label="Export library"
            disabled={
              busy ||
              (libraryId === "all"
                ? libraries.length === 0
                : !libraries.some((library) => library.id === libraryId))
            }
            onClick={() => {
              const targets =
                libraryId === "all"
                  ? libraries.map((library) => library.id)
                  : [libraryId]
              targets.forEach(exportLibrary)
            }}
          >
            <DownloadGlyph className="size-4" />
          </PdfEditorToolButton>
        </div>
        <Button
          type="button"
          size="sm"
          variant="outline"
          className="w-full"
          disabled={
            !canCreateFromSelection || busy || !permissions.canModifyAnnotations
          }
          onClick={createFromSelection}
        >
          <StampGlyph className="size-4" />
          Save selection as stamp
        </Button>
        <input
          ref={importInputRef}
          type="file"
          accept="application/pdf,.pdf"
          className="sr-only"
          tabIndex={-1}
          onChange={(event) => {
            const file = event.target.files?.[0]
            if (file) void importLibrary(file)
            event.currentTarget.value = ""
          }}
        />
      </div>
      {stamps.length === 0 ? (
        <PdfEditorEmptyState
          glyph={StampGlyph}
          title={libraries.length === 0 ? "Loading stamps…" : "No stamps"}
          description="Pick a library, import stamps from a PDF, or save a selected annotation as a stamp."
        />
      ) : (
        <PdfEditorScrollArea className="min-h-0 flex-1" viewportClassName="p-3">
          <div className="grid grid-cols-2 gap-2.5">
            {stamps.map(({ library, stamp }) => {
              const isActive =
                activeStamp?.libraryId === library.id &&
                activeStamp.stamp.id === stamp.id
              return (
                <div
                  key={`${library.id}-${stamp.id}`}
                  className="group relative"
                >
                  <button
                    type="button"
                    aria-label={stamp.label ?? stamp.subject ?? "Rubber stamp"}
                    aria-pressed={isActive}
                    disabled={!permissions.canModifyAnnotations}
                    className={cn(
                      "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",
                      isActive
                        ? "border-primary bg-primary/5 ring-1 ring-primary/40"
                        : "hover:bg-accent/50"
                    )}
                    onClick={() => placeStamp(library.id, stamp)}
                  >
                    <StampImg
                      libraryId={library.id}
                      pageIndex={stamp.pageIndex}
                      width={STAMP_THUMB_WIDTH}
                      style={{
                        maxWidth: "85%",
                        maxHeight: "85%",
                        objectFit: "contain",
                      }}
                    />
                  </button>
                  {!library.readonly ? (
                    <button
                      type="button"
                      aria-label="Remove stamp"
                      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"
                      onClick={() =>
                        stampCapability
                          ?.removeStampFromLibrary(library.id, stamp.id)
                          .wait(
                            () => undefined,
                            () =>
                              notify("The stamp could not be removed.", "error")
                          )
                      }
                    >
                      <TrashGlyph className="size-3" />
                    </button>
                  ) : null}
                </div>
              )
            })}
          </div>
        </PdfEditorScrollArea>
      )}
      {activeStamp ? (
        <div className="shrink-0 border-t px-3 py-2 text-xs text-muted-foreground">
          Click a page to place{" "}
          <span className="font-medium text-foreground">
            {activeStamp.stamp.subject}
          </span>
          .
        </div>
      ) : null}
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Signatures                                                                 */
/* -------------------------------------------------------------------------- */
export function PdfEditorSignaturesPanel({
  documentId,
}: {
  documentId: string
}) {
  const { entries, provides } = useSignatureEntries()
  const activePlacement = useActivePlacement(documentId)
  const { openDialog, permissions } = usePdfEditor()
  const canPlace = permissions.canModifyAnnotations
  const isActive = (entryId: string, kind: SignatureFieldKind) =>
    activePlacement?.entryId === entryId && activePlacement.kind === kind
  const togglePlacement = (entryId: string, kind: SignatureFieldKind) => {
    if (!provides) return
    const scope = provides.forDocument(documentId)
    if (isActive(entryId, kind)) {
      scope.deactivatePlacement()
      return
    }
    if (kind === SignatureFieldKind.Initials) {
      scope.activateInitialsPlacement(entryId)
    } else {
      scope.activateSignaturePlacement(entryId)
    }
  }
  return (
    <div className="flex h-full flex-col">
      <div className="shrink-0 border-b px-3 py-2">
        <Button
          type="button"
          size="sm"
          className="w-full"
          disabled={!canPlace}
          onClick={() => openDialog({ type: "signature" })}
        >
          <PlusGlyph className="size-4" />
          Create signature
        </Button>
      </div>
      {entries.length === 0 ? (
        <PdfEditorEmptyState
          glyph={SignatureGlyph}
          title="No signatures yet"
          description="Draw, type, or upload a signature, then click it to place it on the page."
        />
      ) : (
        <PdfEditorScrollArea className="min-h-0 flex-1" viewportClassName="p-3">
          <div className="space-y-3">
            {entries.map((entry) => (
              <div key={entry.id} className="space-y-2 rounded-lg border p-2.5">
                <div className="flex items-center justify-between">
                  <span className="text-[11px] font-medium text-muted-foreground">
                    Saved {formatRelativeDate(new Date(entry.createdAt))}
                  </span>
                  <PdfEditorToolButton
                    label="Delete signature"
                    size="icon-xs"
                    onClick={() => provides?.removeEntry(entry.id)}
                  >
                    <TrashGlyph className="size-3.5" />
                  </PdfEditorToolButton>
                </div>
                <div className="flex gap-2">
                  <button
                    type="button"
                    disabled={!canPlace}
                    aria-pressed={isActive(
                      entry.id,
                      SignatureFieldKind.Signature
                    )}
                    className={cn(
                      "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",
                      isActive(entry.id, SignatureFieldKind.Signature)
                        ? "border-primary ring-1 ring-primary/40"
                        : "hover:border-muted-foreground/60"
                    )}
                    onClick={() =>
                      togglePlacement(entry.id, SignatureFieldKind.Signature)
                    }
                  >
                    <img
                      src={entry.signature.previewDataUrl}
                      alt="Signature"
                      className="h-12 max-w-[90%] object-contain"
                    />
                  </button>
                  {entry.initials ? (
                    <button
                      type="button"
                      disabled={!canPlace}
                      aria-pressed={isActive(
                        entry.id,
                        SignatureFieldKind.Initials
                      )}
                      className={cn(
                        "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",
                        isActive(entry.id, SignatureFieldKind.Initials)
                          ? "border-primary ring-1 ring-primary/40"
                          : "hover:border-muted-foreground/60"
                      )}
                      onClick={() =>
                        togglePlacement(entry.id, SignatureFieldKind.Initials)
                      }
                    >
                      <img
                        src={entry.initials.previewDataUrl}
                        alt="Initials"
                        className="h-10 max-w-[80%] object-contain"
                      />
                    </button>
                  ) : null}
                </div>
              </div>
            ))}
          </div>
        </PdfEditorScrollArea>
      )}
      {activePlacement ? (
        <div className="shrink-0 border-t px-3 py-2 text-xs text-muted-foreground">
          Click a page to place your{" "}
          {activePlacement.kind === SignatureFieldKind.Initials
            ? "initials"
            : "signature"}
          .
        </div>
      ) : null}
    </div>
  )
}
/* -------------------------------------------------------------------------- */
/* Forms                                                                      */
/* -------------------------------------------------------------------------- */
export function PdfEditorFormsPanel({ documentId }: { documentId: string }) {
  const { provides: form } = useFormCapability()
  const { provides: annotation, state: annotationState } =
    useAnnotation(documentId)
  const {
    features,
    fileName,
    formDesignMode,
    notify,
    permissions,
    scrollToAnnotation,
    setFormDesignMode,
  } = usePdfEditor()
  const scope = React.useMemo(
    () => form?.forDocument(documentId) ?? null,
    [documentId, form]
  )
  const fieldStore = React.useMemo(() => createFormFieldStore(scope), [scope])
  const fields = React.useSyncExternalStore(
    fieldStore.subscribe,
    fieldStore.getSnapshot,
    fieldStore.getSnapshot
  )
  const importInputRef = React.useRef<HTMLInputElement>(null)
  const designModeId = React.useId()
  const widgetsByName = React.useMemo(() => {
    const map = new Map<string, PdfWidgetAnnoObject>()
    for (const tracked of Object.values(annotationState.byUid)) {
      if (tracked.object.type !== PdfAnnotationSubtype.WIDGET) continue
      const widget = tracked.object as PdfWidgetAnnoObject
      if (!map.has(widget.field.name)) map.set(widget.field.name, widget)
    }
    return map
  }, [annotationState.byUid])
  const focusField = (field: FormFieldInfo) => {
    const widget = widgetsByName.get(field.name)
    if (!widget) return
    if (formDesignMode) {
      annotation?.selectAnnotation(widget.pageIndex, widget.id)
    } else {
      scope?.selectField(widget.id)
    }
    scrollToAnnotation(widget)
  }
  const exportValues = () => {
    if (!scope) return
    const values = scope.getFormValues()
    downloadBlob(
      new Blob([JSON.stringify(values, null, 2)], { type: "application/json" }),
      `${fileName.replace(/\.pdf$/i, "")}-form-values.json`
    )
  }
  const importValues = async (file: File) => {
    if (!scope) return
    try {
      const parsed = JSON.parse(await file.text()) as Record<string, unknown>
      const values = Object.fromEntries(
        Object.entries(parsed).map(([key, value]) => [key, String(value ?? "")])
      )
      scope.setFormValues(values).wait(
        () => notify("Form values imported.", "success"),
        () => notify("The form values could not be applied.", "error")
      )
    } catch {
      notify("The file is not valid JSON.", "error")
    }
  }
  const clearValues = () => {
    if (!scope || fields.length === 0) return
    const values = Object.fromEntries(
      fields.map((field) => [
        field.name,
        field.type === PDF_FORM_FIELD_TYPE.CHECKBOX ||
        field.type === PDF_FORM_FIELD_TYPE.RADIOBUTTON
          ? "Off"
          : "",
      ])
    )
    scope.setFormValues(values).wait(
      () => notify("Form cleared.", "success"),
      () => notify("The form could not be cleared.", "error")
    )
  }
  return (
    <div className="flex h-full flex-col">
      <div className="shrink-0 space-y-2 border-b px-3 py-2">
        {features.forms && permissions.canModifyAnnotations ? (
          <label
            htmlFor={designModeId}
            data-slot="label"
            className="flex cursor-pointer items-center justify-between gap-4 py-1"
          >
            <span className="flex min-w-0 flex-col gap-0.5">
              <span className="text-sm font-medium">Design mode</span>
              <span className="text-xs text-muted-foreground">
                Move, resize, and configure fields instead of filling them.
              </span>
            </span>
            <Switch
              id={designModeId}
              checked={formDesignMode}
              onCheckedChange={setFormDesignMode}
            />
          </label>
        ) : null}
        <div className="flex flex-wrap gap-1.5">
          <Button
            type="button"
            size="xs"
            variant="outline"
            disabled={fields.length === 0}
            onClick={exportValues}
          >
            <DownloadGlyph className="size-3.5" />
            Export values
          </Button>
          <Button
            type="button"
            size="xs"
            variant="outline"
            disabled={!permissions.canFillForms}
            onClick={() => importInputRef.current?.click()}
          >
            <UploadGlyph className="size-3.5" />
            Import values
          </Button>
          <Button
            type="button"
            size="xs"
            variant="outline"
            disabled={fields.length === 0 || !permissions.canFillForms}
            onClick={clearValues}
          >
            Clear
          </Button>
        </div>
        <input
          ref={importInputRef}
          type="file"
          accept="application/json,.json"
          className="sr-only"
          tabIndex={-1}
          onChange={(event) => {
            const file = event.target.files?.[0]
            if (file) void importValues(file)
            event.currentTarget.value = ""
          }}
        />
      </div>
      {fields.length === 0 ? (
        <PdfEditorEmptyState
          glyph={TextFieldGlyph}
          title="No form fields"
          description={
            features.forms
              ? "Switch to the Forms mode to add text fields, checkboxes, radio buttons, dropdowns, and list boxes."
              : "This document has no fillable fields."
          }
        />
      ) : (
        <PdfEditorScrollArea className="min-h-0 flex-1" viewportClassName="p-2">
          <div className="space-y-1">
            {fields.map((field) => (
              <button
                key={field.name}
                type="button"
                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"
                onClick={() => focusField(field)}
              >
                <div className="min-w-0 flex-1">
                  <div className="flex items-center gap-1.5">
                    <span className="truncate text-sm font-medium">
                      {field.name}
                    </span>
                    {field.readOnly ? (
                      <Badge variant="outline" className="h-4 px-1 text-[10px]">
                        Read only
                      </Badge>
                    ) : null}
                  </div>
                  <div className="truncate text-xs text-muted-foreground">
                    {getFormFieldTypeLabel(field.type)}
                    {field.value ? ` · ${field.value}` : ""}
                  </div>
                </div>
              </button>
            ))}
          </div>
        </PdfEditorScrollArea>
      )}
    </div>
  )
}
export { GridGlyph as PdfEditorPagesGlyph }
function InlineLoadingButton({
  loading,
  disabled,
  children,
  className,
  ...props
}: React.ComponentProps<typeof Button> & {
  loading?: boolean
}) {
  return (
    <Button
      {...props}
      className={cn("relative", className)}
      disabled={disabled || loading}
      aria-busy={loading || undefined}
    >
      <span className={cn("contents", loading && "invisible")}>{children}</span>
      {loading ? (
        <InlineSpinner className="pointer-events-none absolute" />
      ) : null}
    </Button>
  )
}
function InlineSpinner({ className, ...props }: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="LoaderCircle"
      tabler="IconLoader2"
      hugeicons="Loading03Icon"
      phosphor="CircleNotchIcon"
      remixicon="RiLoader4Line"
      role="status"
      aria-label="Loading"
      className={cn("size-4 animate-spin", className)}
      {...props}
    />
  )
}
type InlineRegistryIconProps = Omit<
  React.ComponentProps<"svg">,
  "children" | "strokeWidth"
> & { strokeWidth?: number }
lib/pdf-thumbnail-utils.ts
import type { PdfDocumentObject, PdfEngine } from "@embedpdf/models"

const PDFIUM_VERSION = "2.15.0"
const PDFIUM_WASM_URL = `https://cdn.jsdelivr.net/npm/@embedpdf/pdfium@${PDFIUM_VERSION}/dist/pdfium.wasm`

let sharedEnginePromise: Promise<PdfEngine> | null = null
const pdfDocumentCache = new Map<string, Promise<PdfDocumentObject>>()
const thumbnailUrlCache = new Map<string, Promise<string | null>>()

export function loadSharedPdfEngine() {
  sharedEnginePromise ??= import("@embedpdf/engines/pdfium-worker-engine").then(
    ({ createPdfiumEngine }) => createPdfiumEngine(PDFIUM_WASM_URL, {})
  )

  return sharedEnginePromise
}

export async function loadPdfDocument(url: string) {
  let documentPromise = pdfDocumentCache.get(url)

  if (!documentPromise) {
    documentPromise = loadSharedPdfEngine().then((engine) =>
      engine
        .openDocumentUrl(
          { id: url, url },
          { mode: url.startsWith("blob:") ? "full-fetch" : "auto" }
        )
        .toPromise()
    )
    pdfDocumentCache.set(url, documentPromise)
  }

  return documentPromise
}

export async function getPdfPageCount(url: string) {
  return (await loadPdfDocument(url)).pageCount
}

export function renderPdfThumbnailUrl({
  dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1,
  pageIndex,
  url,
  width,
}: {
  dpr?: number
  pageIndex: number
  url: string
  width: number
}) {
  const cacheKey = `${url}#${pageIndex}@${width}x${dpr}`
  let thumbnailPromise = thumbnailUrlCache.get(cacheKey)

  if (!thumbnailPromise) {
    thumbnailPromise = (async () => {
      const [engine, document] = await Promise.all([
        loadSharedPdfEngine(),
        loadPdfDocument(url),
      ])
      const page = document.pages[pageIndex]

      if (!page) return null

      const blob = await engine
        .renderThumbnail(document, page, {
          dpr,
          imageType: "image/png",
          scaleFactor: width / page.size.width,
          withAnnotations: true,
        })
        .toPromise()

      return URL.createObjectURL(blob)
    })()
    thumbnailUrlCache.set(cacheKey, thumbnailPromise)
  }

  return thumbnailPromise
}

More from extend

Browse all components in this library →