---
phase: 05-pdf-fill-and-field-mapping
plan: 02
subsystem: frontend/documents
tags: [dnd-kit, pdf-fields, field-placer, coordinate-conversion, drag-and-drop]
dependency_graph:
  requires: [05-01]
  provides: [field-placement-ui, signature-field-overlay]
  affects: [documents-detail-page, pdf-viewer]
tech_stack:
  added: ["@dnd-kit/core@^6.3.1", "@dnd-kit/utilities@^3.2.2"]
  patterns: [dnd-kit-drag-overlay, y-flip-coordinate-conversion, droppable-pdf-zone]
key_files:
  created:
    - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
  modified:
    - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
decisions:
  - "DragOverlay used for ghost rendering during drag — avoids transform-based dragging which breaks coordinate math relative to the droppable container"
  - "containerRef.getBoundingClientRect() called at drop time (not stale pageInfo.width) — captures current rendered size after zoom changes"
  - "activatorEvent + delta pattern for final drop coordinates — activatorEvent gives the client position where drag started, delta gives displacement"
  - "DroppableZone assigns both setNodeRef (dnd-kit) and containerRef (coordinate math) to same DOM node via combined ref callback"
  - "top: top - heightPx applied to overlay divs — pdfToScreenCoords returns y of bottom-left corner, must shift up by field height for DOM top-left origin"
metrics:
  duration: 1 min
  completed_date: "2026-03-20"
  tasks_completed: 2
  files_created: 1
  files_modified: 1
---

# Phase 5 Plan 02: Drag-and-Drop Signature Field Placer Summary

**One-liner:** dnd-kit DndContext with draggable palette token + droppable PDF zone; Y-flip coordinate conversion persists fields to DB via PUT /api/documents/[id]/fields.

## What Was Built

A complete drag-and-drop field placement system integrated into the document detail page:

1. **FieldPlacer.tsx** (321 lines) — New client component providing:
   - A `DndContext` wrapping both palette and PDF canvas
   - `DraggableToken` sub-component using `useDraggable` for the "Signature Field" palette item
   - `DroppableZone` sub-component using `useDroppable` to make the PDF canvas a valid drop target
   - `DragOverlay` showing a blue ghost rectangle during drag
   - `screenToPdfCoords()` — Y-flip formula converting DOM coordinates to PDF user space
   - `pdfToScreenCoords()` — inverse formula for rendering stored fields as overlays
   - Server sync: GET on mount (load existing), PUT on every add/remove (persist)
   - Per-page filtering (`field.page === currentPage`) so page 1 fields don't show on page 2

2. **PdfViewer.tsx** (updated) — Extended with:
   - `pageInfo` state (`PageInfo | null`) populated via `onLoadSuccess` callback
   - `Math.max(page.view[0], page.view[2])` / `Math.max(page.view[1], page.view[3])` for robust `originalWidth`/`originalHeight` (handles non-standard mediaBox ordering)
   - `FieldPlacer` wraps the `Document`/`Page` tree, receiving `docId`, `pageInfo`, `currentPage`
   - Only `scale` prop used on `<Page>` (not both `width` + `scale`)

## Verification Results

- `npm run build` — compiled successfully (both tasks)
- FieldPlacer.tsx: 321 lines (minimum 80 required)
- `@dnd-kit/core` and `@dnd-kit/utilities` present in package.json
- PdfViewerWrapper.tsx unchanged (dynamic import wrapper still passes docId through)

## Coordinate Conversion Details

The Y-flip formula is critical for correct field placement:
- **DOM**: Y=0 at top, increases downward
- **PDF user space**: Y=0 at bottom, increases upward
- `pdfY = ((renderedH - screenY) / renderedH) * originalHeight`
- This means dragging to the visual top of a page stores a high pdfY value (near originalHeight)
- `containerRect.width/height` used at drop time (not stale pageInfo) to handle zoom changes correctly

## Commits

| Task | Commit | Description |
|------|--------|-------------|
| Task 1 | 6069ae5 | feat(05-02): install dnd-kit and create FieldPlacer component |
| Task 2 | 7a36736 | feat(05-02): extend PdfViewer with pageInfo state and FieldPlacer integration |

## Deviations from Plan

None — plan executed exactly as written.

The DroppableZone component uses a combined ref callback to assign both the dnd-kit `setNodeRef` and the `containerRef` simultaneously to the same DOM node — this was a minor implementation detail not explicitly specified in the plan but required for correct operation.

## Self-Check: PASSED

- FOUND: FieldPlacer.tsx (321 lines)
- FOUND: PdfViewer.tsx (updated)
- FOUND commit: 6069ae5
- FOUND commit: 7a36736
- @dnd-kit/core and @dnd-kit/utilities in package.json
- Build: compiled successfully
