ns-ui / raft-moor

Raft Moor

A presence rail down the left edge. Four teammates are reading this document right now — each one a small initialed chip riding at their live scroll position. Watch the rail: when two of them land on the same stretch, the later arrival moors alongside instead of hiding behind the first.

Sam, Mara and Jonas drift between sections every few seconds. Aiko stays parked on the FAQ and fades to half-opacity once idle. Click any chip to jump straight to that person's section — focus follows you there.

Install

Zero dependencies beyond React. Position and rafting are plain DOM math — a per-chip spring and a clustering pass, no canvas, no SVG.

npx shadcn add @ns-ui/raft-moor

API

fractionis the governing scalar — 0..1 through the document — and sets a chip's y directly. Chips within 24px of each other raft: earliest arrival stays flush to the rail, everyone later moors one chip-width further out.

collaboratorsRaftMoorCollaborator[]required
idleThresholdMsnumber60000
railHeightVhnumber70

Examples

Feed it whatever a presence channel gives you — id, name, current scroll fraction, and the nearest section:

<RaftMoor
  collaborators={[
    { id: "sam", name: "Sam Okafor", fraction: 0.5,
      sectionId: "api", sectionLabel: "API" },
  ]}
/>

Every chip is a real button — tab to one and its position is announced through its accessible name, never streamed live.

FAQ

Why not a "+N" badge on overlap? Co-location is the interesting moment — two people on the same paragraph is exactly when you want both names, not a count. Rafting keeps every identity legible instead of collapsing the signal that made it worth showing.

Under reduced motion, chips reposition instantly with no spring — only the idle-opacity fade keeps its ease, since that's not spatial motion.