// Card / arrow annotation — a draggable dark box connected to its anchor by a // dashed line. PC-only feature. // // Data shape (one of): // { id, nodeId, text, offset:{dx,dy} } ← attached to a node // { id, targetEdgeId, text, offset:{dx,dy} } ← attached to an edge midpoint // // Canvas resolves the anchor (x,y) up-front and passes it in via `anchor`, so // the components below don't care about node-vs-edge attachment. const DEFAULT_OFFSET = { dx: 120, dy: 80 }; function NoteBox({ note, anchor, selected, onSelect, onDrag, onDragEnd, worldZoom }){ if (!anchor) return null; const off = note.offset || DEFAULT_OFFSET; const x = anchor.x + (off.dx ?? DEFAULT_OFFSET.dx); const y = anchor.y + (off.dy ?? DEFAULT_OFFSET.dy); const moveState = React.useRef(null); const [dragging, setDragging] = React.useState(false); const onPointerDown = (e) => { e.stopPropagation(); onSelect(note.id); const ox = off.dx ?? DEFAULT_OFFSET.dx; const oy = off.dy ?? DEFAULT_OFFSET.dy; moveState.current = { startX: e.clientX, startY: e.clientY, ox, oy, moved: false }; try { e.currentTarget.setPointerCapture(e.pointerId); } catch(_){} }; const onPointerMove = (e) => { if (!moveState.current) return; const { startX, startY, ox, oy } = moveState.current; const dx = (e.clientX - startX) / worldZoom; const dy = (e.clientY - startY) / worldZoom; if (Math.abs(dx)+Math.abs(dy) > 2) { moveState.current.moved = true; if (!dragging) setDragging(true); } onDrag(note.id, { dx: ox + dx, dy: oy + dy }); }; const onPointerUp = (e) => { if (!moveState.current) return; const moved = moveState.current.moved; moveState.current = null; setDragging(false); onDragEnd(note.id, moved); try { e.currentTarget.releasePointerCapture(e.pointerId); } catch(_){} }; return (