Skip to content
Tools— browse docs
On this page

Step 2: Drop a pin

One field for the pins, one click handler for dropping them — measured against the board's own box.

A pin board needs pins. Each one has a position and a label, and — since two people can click different empty spots on the board at the same time — a field.map, the same field Getting Started used for notes.

Edit the codec below. Replace the empty defineStateCodec({}) with:

export type Pin = {
  x: number
  y: number
  label: string
}

export const codec = defineStateCodec({
  // A per-KEY map: two people dropping different pins at once BOTH land.
  pins: field.map<Pin>(),
})

Then give Board a click handler that writes one. The board measures the click against its own bounding box — never a parent's — so the pin lands exactly where you clicked:

const onBoardClick = (e: ReactMouseEvent<HTMLDivElement>) => {
  if (!canEdit) return
  const rect = boardRef.current!.getBoundingClientRect()
  actions.map("pins").set(newId(), {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
    label: "Pin",
  })
}
Drop a pin
src/codec.ts + src/tool.tsx Lines 14 to 23 and 41 to 51 highlighted.

Edit and the example re-runs. Tab indents; press Escape to leave the editor.

Running · your edits, live
Starting the example…

You should see: the empty-state text is replaced by the board's grid, and clicking anywhere on it drops a 📍 exactly under your cursor.

Note:

Why e.target isn't checked yet

Click near an existing pin right now and you'll drop a second one practically on top of it — that's fine for a minute. Once pins can be dragged, in the next step, an unguarded click handler would also fire on the mouse-up that ends a drag. You'll fix both at once.

Next steps