# Bento Collaboration — React Bento Grid

> A multiplayer bento: named cursors glide across a shared board, comments pop into a thread, a teammate types live and reactions float up from a decision.

- Category: [Bento Grids](https://pixelpapa.com/components/bento.md)
- Tags: bento, collaboration, cursors, comments, multiplayer
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/bento/bento-collaboration

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/bento-collaboration.json"
```

The file is written to `components/pixelpapa/bento-collaboration.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { MousePointer2 } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";

const ease = [0.22, 1, 0.36, 1] as const;

function useTick(ms: number) {
  const reduce = useReducedMotion();
  const [tick, setTick] = useState(0);
  useEffect(() => {
    if (reduce) return;
    const id = setInterval(() => setTick((t) => t + 1), ms);
    return () => clearInterval(id);
  }, [reduce, ms]);
  return tick;
}

function Tile({ title, body, className, children }: { title: string; body: string; className?: string; children: ReactNode }) {
  return (
    <motion.div
      variants={{ hidden: { opacity: 0, y: 24 }, shown: { opacity: 1, y: 0, transition: { duration: 0.7, ease } } }}
      className={cn("flex flex-col overflow-hidden rounded-3xl border border-border bg-card", className)}
    >
      <div className="relative min-h-44 flex-1">{children}</div>
      <div className="px-6 pb-6">
        <h3 className="font-medium text-card-foreground">{title}</h3>
        <p className="mt-1 text-sm text-muted-foreground">{body}</p>
      </div>
    </motion.div>
  );
}

const cursors = [
  { name: "Mira", color: "var(--chart-1)", x: ["12%", "38%", "30%", "58%", "12%"], y: ["20%", "34%", "62%", "44%", "20%"], dur: 9 },
  { name: "Dev", color: "var(--chart-3)", x: ["74%", "52%", "66%", "40%", "74%"], y: ["66%", "50%", "22%", "70%", "66%"], dur: 11 },
  { name: "Ines", color: "var(--chart-5)", x: ["46%", "80%", "70%", "22%", "46%"], y: ["80%", "28%", "56%", "74%", "80%"], dur: 13 },
];

const notes = [
  { text: "Onboarding drops at step 3", cls: "top-[12%] left-[8%] -rotate-2 bg-chart-4/25" },
  { text: "Try a 2-field signup", cls: "top-[18%] left-[46%] rotate-1 bg-chart-2/25" },
  { text: "Interview 5 churned trials", cls: "top-[54%] left-[22%] rotate-2 bg-chart-3/20" },
  { text: "Ship checklist by Friday", cls: "top-[58%] left-[60%] -rotate-1 bg-chart-5/25" },
];

function Board() {
  const reduce = useReducedMotion();
  return (
    <div className="absolute inset-0 [background-image:radial-gradient(var(--border)_1px,transparent_1px)] [background-size:20px_20px]">
      {notes.map((n, i) => (
        <motion.div
          key={n.text}
          initial={{ opacity: 0, scale: 0.8 }}
          whileInView={{ opacity: 1, scale: 1 }}
          viewport={{ once: true }}
          transition={{ delay: 0.2 + i * 0.1, type: "spring", stiffness: 300, damping: 20 }}
          className={cn("absolute w-32 rounded-lg p-3 text-xs font-medium text-card-foreground shadow-sm sm:w-36", n.cls)}
        >
          {n.text}
        </motion.div>
      ))}
      {cursors.map((c) => (
        <motion.div
          key={c.name}
          className="absolute"
          style={{ left: c.x[0], top: c.y[0], color: c.color }}
          animate={reduce ? undefined : { left: c.x, top: c.y }}
          transition={{ duration: c.dur, repeat: Infinity, ease: "easeInOut" }}
        >
          <MousePointer2 className="size-5 fill-current" />
          <span className="ml-4 block rounded-full px-2 py-0.5 text-[11px] font-medium text-white" style={{ background: c.color }}>
            {c.name}
          </span>
        </motion.div>
      ))}
    </div>
  );
}

const thread = [
  { who: "MO", color: "bg-chart-1", text: "Step 3 asks for a card. That is the drop." },
  { who: "DA", color: "bg-chart-3", text: "Agree. Can we defer it to day 7?" },
  { who: "IV", color: "bg-chart-5", text: "Mocked it up, link in the board." },
];

function Comments() {
  const tick = useTick(1400);
  const reduce = useReducedMotion();
  const shown = reduce ? thread.length : (tick % (thread.length + 2)) + 1;
  return (
    <div className="flex h-full flex-col justify-end gap-2 p-6">
      <AnimatePresence initial={false}>
        {thread.slice(0, shown).map((m) => (
          <motion.div
            key={m.text}
            layout
            initial={{ opacity: 0, y: 16, scale: 0.9 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={{ opacity: 0, transition: { duration: 0.15 } }}
            transition={{ type: "spring", stiffness: 380, damping: 26 }}
            className="flex origin-bottom-left items-start gap-2"
          >
            <span className={cn("flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white", m.color)}>{m.who}</span>
            <span className="rounded-2xl rounded-tl-md bg-muted px-3 py-2 text-xs text-card-foreground">{m.text}</span>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

const sentence = "Decision: move the card step to day seven and measure activation for two weeks.";

function Typing() {
  const tick = useTick(60);
  const reduce = useReducedMotion();
  const n = reduce ? sentence.length : Math.min(tick % (sentence.length + 40), sentence.length);
  return (
    <div className="p-6">
      <p className="text-[11px] font-medium tracking-wide text-muted-foreground uppercase">Meeting notes</p>
      <p className="mt-3 text-sm leading-8 text-card-foreground">
        {sentence.slice(0, n)}
        <span className="relative ml-0.5 inline-block h-4 w-0.5 translate-y-0.5 bg-chart-3">
          <span className="absolute -top-3.5 left-0 rounded-sm bg-chart-3 px-1 py-px text-[9px] leading-none font-medium text-white">Dev</span>
        </span>
      </p>
    </div>
  );
}

const chips = [
  ["👍", 9],
  ["🎉", 4],
  ["🚀", 2],
] as const;

function Reactions() {
  const tick = useTick(900);
  return (
    <div className="flex h-full items-center p-6">
      <div className="relative flex w-full flex-wrap items-center gap-x-6 gap-y-4 rounded-2xl border border-border bg-background p-4 text-sm">
        <div className="min-w-0 flex-1">
          <p className="font-medium text-foreground">Card step moves to day 7</p>
          <p className="mt-0.5 text-xs text-muted-foreground">Proposed by Mira · approved by 6 of 6</p>
        </div>
        <div className="flex">
          {["MO", "DA", "IV", "RK", "TS", "LP"].map((who, i) => (
            <span
              key={who}
              className={cn("-ml-1 flex size-7 items-center justify-center rounded-full text-[10px] font-semibold text-white ring-2 ring-background first:ml-0", ["bg-chart-1", "bg-chart-3", "bg-chart-5", "bg-chart-4", "bg-chart-2", "bg-chart-1"][i])}
            >
              {who}
            </span>
          ))}
        </div>
        <div className="flex gap-2">
          {chips.map(([e, base], i) => (
            <span key={e} className="relative flex items-center gap-1.5 rounded-full border border-border bg-card px-2.5 py-1 text-xs text-card-foreground tabular-nums">
              {e} {base + Math.floor((tick + 2 - i) / 3)}
              <AnimatePresence>
                {tick % 3 === i && (
                  <motion.span
                    key={tick}
                    aria-hidden
                    initial={{ opacity: 1, y: 0, scale: 0.8 }}
                    animate={{ opacity: 0, y: -48, scale: 1.4 }}
                    transition={{ duration: 1.2, ease: "easeOut" }}
                    className="pointer-events-none absolute left-1.5 text-base"
                  >
                    {e}
                  </motion.span>
                )}
              </AnimatePresence>
            </span>
          ))}
        </div>
      </div>
    </div>
  );
}

export default function BentoCollaboration() {
  return (
    <section className="w-full bg-background px-6 py-20 sm:py-24">
      <div className="mx-auto max-w-6xl">
        <div className="max-w-2xl">
          <p className="text-sm font-medium text-primary">Wexford Boards</p>
          <h2 className="mt-3 text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">
            Think out loud, together, without booking a room
          </h2>
        </div>
        <motion.div
          initial="hidden"
          whileInView="shown"
          viewport={{ once: true, amount: 0.15 }}
          transition={{ staggerChildren: 0.08 }}
          className="mt-12 grid gap-4 md:grid-cols-3"
        >
          <Tile className="md:col-span-2 md:row-span-2 [&>div:first-child]:min-h-80" title="A board everyone is on at once" body="Up to 200 live cursors, with zero merge conflicts and full history.">
            <Board />
          </Tile>
          <Tile title="Comments where the work is" body="Threads attach to a note, a frame or a single word.">
            <Comments />
          </Tile>
          <Tile title="Notes that write themselves live" body="See each keystroke, labelled with its author.">
            <Typing />
          </Tile>
          <Tile className="md:col-span-3 [&>div:first-child]:min-h-24" title="Decide in the open" body="Call a vote on any note. Reactions and approvals are recorded with the decision.">
            <Reactions />
          </Tile>
        </motion.div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Bento Collaboration" component from PixelPapa UI to this project.

What it is: A multiplayer bento: named cursors glide across a shared board, comments pop into a thread, a teammate types live and reactions float up from a decision.
Category: Bento Grids. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/bento-collaboration.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/bento-collaboration.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/bento-collaboration" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More bento grids

- [Bento AI Capabilities](https://pixelpapa.com/components/bento/bento-ai-capabilities.md): An AI feature bento: an answer streams word by word with citations, an agent ticks through its steps, a voice waveform pulses and a reply cross-fades between languages.
- [Bento Analytics](https://pixelpapa.com/components/bento/bento-analytics.md) (Pro): A metrics bento: an MRR curve that morphs between ranges, a donut whose legend isolates segments, a ticking live-visitor count, weekday bars and a row of sparklines.
- [Bento Developer](https://pixelpapa.com/components/bento/bento-developer.md) (Pro): A developer bento: a terminal replays a deploy line by line, an API response swaps with the selected endpoint, a p95 latency graph scrolls live and uptime bars fill in.
- [Bento Hover Expand](https://pixelpapa.com/components/bento/bento-hover-expand.md): Four tall tiles share a row; the hovered or focused one springs wide to reveal its detail and metric while its siblings narrow and dim.
- [Bento Product Overview](https://pixelpapa.com/components/bento/bento-product-overview.md) (Pro): Five product tiles that each run a tiny working demo: a scrubbable revenue line, a self-typing command bar, spring switches, a fanning avatar stack and a live timer.
- [Bento Security Globe](https://pixelpapa.com/components/bento/bento-security-globe.md) (Pro): A security bento around a slowly rotating dotted SVG globe with region pins and traffic arcs, flanked by a ticking threat counter, a key-rotation ring and a residency picker.
