# Video Call

> A video call where the grid reflows as people drop in and out, whoever is talking gets a soft ring and a pulse, reactions float up the stage, and chat and people slide in beside it.

A shadcn/ui block from the [@devl](https://devl.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/devl/video-call).

- Page, with a live preview: https://shadcn.exchange/r/devl/video-call
- Screenshot: https://shadcn.exchange/shot/devl/video-call/light.png
- Registry item: https://devl.dev/r/video-call.json
- Categories: screen, Comms
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/devl/video-call.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `utils`, `https://coss.com/ui/r/avatar.json`, `https://coss.com/ui/r/button.json`, `https://coss.com/ui/r/input-group.json`, `https://coss.com/ui/r/kbd.json`, `https://coss.com/ui/r/menu.json`, `https://coss.com/ui/r/popover.json`, `https://coss.com/ui/r/tooltip.json`

## Files

### `registry/devl/video-call.tsx`

```tsx
"use client";

import {
  CheckIcon,
  ChevronUpIcon,
  CopyIcon,
  EllipsisIcon,
  HandIcon,
  LayoutGridIcon,
  MessageSquareIcon,
  MicIcon,
  MicOffIcon,
  MonitorUpIcon,
  PhoneOffIcon,
  PresentationIcon,
  SendHorizontalIcon,
  SmilePlusIcon,
  ThumbsDownIcon,
  ThumbsUpIcon,
  UsersIcon,
  VideoIcon,
  VideoOffIcon,
  XIcon,
} from "lucide-react";
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";
import {
  Menu,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuPopup,
  MenuRadioGroup,
  MenuRadioItem,
  MenuSeparator,
  MenuTrigger,
} from "@/components/ui/menu";
import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";

function useReducedMotionSafe() {
  const prefers = useReducedMotion();
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  return mounted && Boolean(prefers);
}

export type CallHue = "rose" | "amber" | "emerald" | "sky" | "violet" | "orange" | "teal" | "indigo";

export interface CallPerson {
  id: string;
  name: string;
  title?: string;
  hue: CallHue;
  muted?: boolean;
  cameraOff?: boolean;
  host?: boolean;
}

export interface CallMessage {
  id: string;
  authorId: string;
  text: string;
  time: string;
}

export interface VideoCallProps {
  className?: string;
  title?: string;
  you?: CallPerson;
  participants?: CallPerson[];
  dropIns?: CallPerson[];
  messages?: CallMessage[];
  scriptedMessages?: { authorId: string; text: string }[];
  elapsedSeconds?: number;
  autoplay?: boolean;
  startTime?: string;
  meetingCode?: string;
}

const spring = { type: "spring", stiffness: 420, damping: 40, mass: 1 } as const;
const easeOut = [0.23, 1, 0.32, 1] as const;
const press = "transition-[color,background-color,box-shadow,scale] duration-150 ease-out active:scale-[0.97]";
const menuPop =
  "transition-[opacity,scale] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:duration-100";
const REACTIONS = ["👏", "🎉", "❤️", "😂", "👍", "🤔"];

const hues: Record<CallHue, { wash: string; glowA: string; glowB: string; avatar: string; solid: string }> = {
  rose: {
    wash: "bg-rose-50 dark:bg-rose-950/60",
    glowA: "bg-rose-300/55 dark:bg-rose-500/22",
    glowB: "bg-orange-200/70 dark:bg-orange-400/14",
    avatar: "bg-rose-500/15 text-rose-700 dark:bg-rose-400/20 dark:text-rose-200",
    solid: "bg-rose-600 text-white dark:bg-rose-500",
  },
  amber: {
    wash: "bg-amber-50 dark:bg-amber-950/60",
    glowA: "bg-amber-300/55 dark:bg-amber-500/22",
    glowB: "bg-orange-200/70 dark:bg-orange-400/14",
    avatar: "bg-amber-500/15 text-amber-700 dark:bg-amber-400/20 dark:text-amber-200",
    solid: "bg-amber-600 text-white dark:bg-amber-500",
  },
  emerald: {
    wash: "bg-emerald-50 dark:bg-emerald-950/60",
    glowA: "bg-emerald-300/55 dark:bg-emerald-500/22",
    glowB: "bg-teal-200/70 dark:bg-teal-400/14",
    avatar: "bg-emerald-500/15 text-emerald-700 dark:bg-emerald-400/20 dark:text-emerald-200",
    solid: "bg-emerald-600 text-white dark:bg-emerald-500",
  },
  sky: {
    wash: "bg-sky-50 dark:bg-sky-950/60",
    glowA: "bg-sky-300/55 dark:bg-sky-500/22",
    glowB: "bg-indigo-200/70 dark:bg-indigo-400/14",
    avatar: "bg-sky-500/15 text-sky-700 dark:bg-sky-400/20 dark:text-sky-200",
    solid: "bg-sky-600 text-white dark:bg-sky-500",
  },
  violet: {
    wash: "bg-violet-50 dark:bg-violet-950/60",
    glowA: "bg-violet-300/55 dark:bg-violet-500/22",
    glowB: "bg-fuchsia-200/70 dark:bg-fuchsia-400/14",
    avatar: "bg-violet-500/15 text-violet-700 dark:bg-violet-400/20 dark:text-violet-200",
    solid: "bg-violet-600 text-white dark:bg-violet-500",
  },
  orange: {
    wash: "bg-orange-50 dark:bg-orange-950/60",
    glowA: "bg-orange-300/55 dark:bg-orange-500/22",
    glowB: "bg-rose-200/70 dark:bg-rose-400/14",
    avatar: "bg-orange-500/15 text-orange-700 dark:bg-orange-400/20 dark:text-orange-200",
    solid: "bg-orange-600 text-white dark:bg-orange-500",
  },
  teal: {
    wash: "bg-teal-50 dark:bg-teal-950/60",
    glowA: "bg-teal-300/55 dark:bg-teal-500/22",
    glowB: "bg-cyan-200/70 dark:bg-cyan-400/14",
    avatar: "bg-teal-500/15 text-teal-700 dark:bg-teal-400/20 dark:text-teal-200",
    solid: "bg-teal-600 text-white dark:bg-teal-500",
  },
  indigo: {
    wash: "bg-indigo-50 dark:bg-indigo-950/60",
    glowA: "bg-indigo-300/55 dark:bg-indigo-500/22",
    glowB: "bg-violet-200/70 dark:bg-violet-400/14",
    avatar: "bg-indigo-500/15 text-indigo-700 dark:bg-indigo-400/20 dark:text-indigo-200",
    solid: "bg-indigo-600 text-white dark:bg-indigo-500",
  },
};

const defaultYou: CallPerson = { id: "you", name: "Noor Haddad", title: "Product design", hue: "teal" };

const defaultParticipants: CallPerson[] = [
  { id: "tomas", name: "Tomás Aguilar", title: "Design lead", hue: "sky", host: true },
  { id: "priya", name: "Priya Raman", title: "Product", hue: "violet" },
  { id: "kenji", name: "Kenji Mori", title: "Engineering", hue: "amber", cameraOff: true },
  { id: "ada", name: "Ada Okoye", title: "Research", hue: "emerald", muted: true },
  { id: "lena", name: "Lena Fischer", title: "Engineering", hue: "rose" },
];

const defaultDropIns: CallPerson[] = [
  { id: "felix", name: "Felix Brandt", title: "Support", hue: "orange", muted: true },
  { id: "sol", name: "Sol Marín", title: "Marketing", hue: "indigo" },
  { id: "ines", name: "Inês Duarte", title: "Data", hue: "rose", cameraOff: true, muted: true },
];

const defaultMessages: CallMessage[] = [
  { id: "m1", authorId: "tomas", text: "Agenda: onboarding v3 flows, then empty states. Figma link in the invite.", time: "10:31" },
  { id: "m2", authorId: "priya", text: "Can we look at step 3 first? Support flagged it twice this week.", time: "10:33" },
];

const defaultScript = [
  { authorId: "lena", text: "The skip link on step 2 is tiny on mobile, fwiw" },
  { authorId: "ada", text: "Agree. 4 of 6 interviewees missed it entirely." },
  { authorId: "tomas", text: "Let's take that as an action item. Noor, can you own it?" },
];

const dropScript: { op: "join" | "leave"; index: number }[] = [
  { op: "join", index: 0 },
  { op: "join", index: 1 },
  { op: "leave", index: 0 },
  { op: "join", index: 2 },
  { op: "leave", index: 1 },
  { op: "leave", index: 2 },
];

function initials(name: string) {
  return name
    .split(/\s+/)
    .map((p) => p[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
}

function firstName(name: string) {
  return name.split(" ")[0];
}

function clock(total: number) {
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  const mm = String(m).padStart(h ? 2 : 1, "0");
  return `${h ? `${h}:` : ""}${mm}:${String(s).padStart(2, "0")}`;
}

function timeLabel(start: string, elapsed: number) {
  const [h, m] = start.split(":").map(Number);
  const mins = (h * 60 + m + Math.floor(elapsed / 60)) % (24 * 60);
  return `${String(Math.floor(mins / 60)).padStart(2, "0")}:${String(mins % 60).padStart(2, "0")}`;
}

function gridShape(n: number) {
  const wide = n <= 1 ? 1 : n <= 4 ? 2 : n <= 9 ? 3 : 4;
  const narrow = n <= 2 ? 1 : 2;
  return { c: wide, r: Math.ceil(n / wide), cs: narrow, rs: Math.ceil(n / narrow) };
}

type Live = CallPerson & { hand?: boolean };

function PersonAvatar({ person, size = 7, className }: { person: CallPerson; size?: 6 | 7 | 8 | 16; className?: string }) {
  const sizes = {
    6: "[--avatar-size:--spacing(6)]",
    7: "[--avatar-size:--spacing(7)]",
    8: "[--avatar-size:--spacing(8)]",
    16: "[--avatar-size:--spacing(16)]",
  };
  return (
    <Avatar className={cn(sizes[size], className)}>
      <AvatarFallback className={cn("font-semibold", hues[person.hue].avatar)}>{initials(person.name)}</AvatarFallback>
    </Avatar>
  );
}

function SpeakingBars({ reduce }: { reduce: boolean | null }) {
  return (
    <span aria-hidden className="flex h-3 items-center gap-[2px]">
      {[0, 1, 2].map((i) => (
        <motion.span
          animate={reduce ? { scaleY: 0.7 } : { scaleY: [0.35, 1, 0.5, 0.85, 0.35] }}
          className="h-3 w-[3px] origin-center rounded-full bg-emerald-500 dark:bg-emerald-400"
          key={i}
          transition={reduce ? { duration: 0 } : { duration: 0.9, repeat: Infinity, ease: "easeInOut", delay: i * 0.15 }}
        />
      ))}
    </span>
  );
}

function Ambient({ hue }: { hue: CallHue }) {
  return (
    <div aria-hidden className={cn("absolute inset-0", hues[hue].wash)}>
      <div className={cn("absolute -top-1/3 -left-1/4 h-full w-3/4 rounded-full blur-3xl", hues[hue].glowA)} />
      <div className={cn("absolute -right-1/4 -bottom-1/2 h-full w-3/4 rounded-full blur-3xl", hues[hue].glowB)} />
    </div>
  );
}

function TileAvatar({ person, live, speaking, compact }: { person: CallPerson; live: boolean; speaking: boolean; compact?: boolean }) {
  const reduce = useReducedMotionSafe();
  return (
    <div className="relative flex items-center justify-center">
      {speaking &&
        !reduce &&
        [0, 1].map((i) => (
          <motion.span
            animate={{ opacity: [0.45, 0], scale: [1, 1.45] }}
            aria-hidden
            className={cn("absolute inset-0 rounded-full", live ? hues[person.hue].solid : "bg-emerald-500/40")}
            key={i}
            transition={{ duration: 1.6, repeat: Infinity, ease: "easeOut", delay: i * 0.8 }}
          />
        ))}
      <span
        className={cn(
          "relative flex size-12 items-center justify-center rounded-full font-semibold text-base tracking-tight",
          !compact && "@[15rem]/tile:size-18 @[15rem]/tile:text-2xl @[26rem]/tile:size-22 @[26rem]/tile:text-3xl",
          live ? hues[person.hue].solid : hues[person.hue].avatar,
        )}
      >
        {initials(person.name)}
      </span>
    </div>
  );
}

function ScreenShare() {
  return (
    <div className="absolute inset-0 flex items-center justify-center bg-muted p-[6%]">
      <div className="flex aspect-video max-h-full w-full max-w-full flex-col gap-[5%] overflow-hidden rounded-lg border bg-card p-[4%] shadow-xs/5">
        <div className="flex items-center gap-2">
          <span className="size-2 rounded-full bg-orange-500 dark:bg-orange-400" />
          <span className="truncate font-medium text-[clamp(9px,1.6cqw,14px)]">Onboarding v3 · flow review</span>
        </div>
        <div className="flex flex-1 items-center gap-[4%]">
          {["Welcome", "Workspace", "Invite", "Done"].map((step, i) => (
            <div className="flex flex-1 flex-col gap-[8%] self-stretch rounded-md border bg-background p-[4%]" key={step}>
              <span className="truncate text-[clamp(7px,1.1cqw,11px)] text-muted-foreground">
                {i + 1}. {step}
              </span>
              <span className="h-[10%] w-3/4 rounded-sm bg-muted" />
              <span className="h-[10%] w-1/2 rounded-sm bg-muted" />
              <span
                className={cn(
                  "mt-auto h-[14%] rounded-sm",
                  i === 2 ? "bg-orange-500/70 dark:bg-orange-400/60" : "bg-primary/15",
                )}
              />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

type Tile = { kind: "person"; person: Live; isYou: boolean } | { kind: "screen" };

function tileKey(t: Tile) {
  return t.kind === "screen" ? "screen" : t.person.id;
}

function CallTile({
  tile,
  speaking,
  compact,
  style,
  className,
  you,
  ns,
}: {
  tile: Tile;
  speaking: boolean;
  compact?: boolean;
  style?: CSSProperties;
  className?: string;
  you: CallPerson;
  ns?: string;
}) {
  const reduce = useReducedMotionSafe();
  const p = tile.kind === "person" ? tile.person : null;
  const label = tile.kind === "screen" ? `${firstName(you.name)}'s screen` : tile.isYou ? "You" : p?.name;

  return (
    <motion.div
      animate={{ opacity: 1, scale: 1 }}
      aria-label={tile.kind === "screen" ? "Your shared screen" : `${p?.name}${p?.muted ? ", muted" : ""}${speaking ? ", speaking" : ""}`}
      className={cn("relative shrink-0", className)}
      exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.16, ease: easeOut } }}
      initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96 }}
      layout={!reduce && !!ns}
      layoutId={ns ? `${ns}-${tileKey(tile)}` : undefined}
      role="group"
      style={{ borderRadius: 16, ...style }}
      transition={reduce ? { duration: 0.15 } : spring}
    >
      <div className="@container/tile absolute inset-0 overflow-hidden rounded-[inherit] bg-muted">
        {tile.kind === "screen" ? (
          <ScreenShare />
        ) : p ? (
          <>
            {!p.cameraOff && <Ambient hue={p.hue} />}
            <motion.div className="absolute inset-0 flex items-center justify-center" layout={!reduce}>
              <TileAvatar compact={compact} live={!p.cameraOff} person={p} speaking={speaking} />
            </motion.div>
          </>
        ) : null}

        {p?.hand && (
          <motion.span
            animate={{ opacity: 1, y: 0 }}
            className="absolute top-2 left-2 flex h-6 items-center gap-1 rounded-full bg-amber-400 px-2 font-medium text-amber-950 text-xs"
            initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}
            layout={!reduce}
            transition={{ duration: 0.2, ease: easeOut }}
          >
            <HandIcon className="size-3.5" />
            {!compact && <span className="hidden @[10rem]/tile:inline">Hand raised</span>}
          </motion.span>
        )}

        {p && (p.muted || p.cameraOff) && (
          <motion.span className="absolute top-2 right-2 flex items-center gap-1" layout={!reduce}>
            {p.cameraOff && !compact && (
              <span className="flex size-6 items-center justify-center rounded-full bg-background/85 text-muted-foreground shadow-xs/5 backdrop-blur-sm">
                <VideoOffIcon aria-hidden className="size-3.5" />
              </span>
            )}
            {p.muted && (
              <span className="flex size-6 items-center justify-center rounded-full bg-background/85 text-destructive-foreground shadow-xs/5 backdrop-blur-sm">
                <MicOffIcon aria-hidden className="size-3.5" />
              </span>
            )}
          </motion.span>
        )}

        <motion.span
          className="absolute bottom-2 left-2 flex h-6 max-w-[calc(100%-1rem)] items-center gap-1.5 rounded-md bg-background/85 px-2 font-medium text-foreground text-xs shadow-xs/5 backdrop-blur-sm"
          layout={!reduce}
        >
          {tile.kind === "screen" && <MonitorUpIcon className="size-3.5 shrink-0 text-muted-foreground" />}
          {speaking && <SpeakingBars reduce={reduce} />}
          <span className={cn("truncate", compact && "max-w-16")}>{label}</span>
        </motion.span>
      </div>
      <motion.span
        animate={{ opacity: speaking ? 1 : 0 }}
        aria-hidden
        className="pointer-events-none absolute inset-0 z-10 rounded-[inherit] ring-2 ring-emerald-500 ring-inset dark:ring-emerald-400"
        initial={false}
        transition={{ duration: reduce ? 0 : 0.2, ease: easeOut }}
      />
    </motion.div>
  );
}

function ControlButton({
  label,
  shortcut,
  onClick,
  tone = "default",
  children,
  badge,
  className,
  pressed,
}: {
  label: string;
  shortcut?: string;
  onClick?: () => void;
  tone?: "default" | "off" | "on";
  children: ReactNode;
  badge?: number;
  className?: string;
  pressed?: boolean;
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={
          <button
            aria-label={label}
            aria-pressed={pressed}
            className={cn(
              "relative flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&_svg]:size-[18px]",
              press,
              tone === "off" && "bg-destructive text-white hover:bg-destructive/90",
              tone === "on" && "bg-primary text-primary-foreground hover:bg-primary/90",
              tone === "default" && "bg-secondary text-secondary-foreground hover:bg-accent",
              className,
            )}
            onClick={onClick}
            type="button"
          />
        }
      >
        {children}
        <AnimatePresence>
          {badge ? (
            <motion.span
              animate={{ opacity: 1, scale: 1 }}
              className="absolute -top-0.5 -right-0.5 flex h-4.5 min-w-4.5 items-center justify-center rounded-full bg-primary px-1 font-medium text-[10px] text-primary-foreground tabular-nums ring-2 ring-background"
              exit={{ opacity: 0, scale: 0.9 }}
              initial={{ opacity: 0, scale: 0.9 }}
              transition={{ duration: 0.15, ease: easeOut }}
            >
              {badge}
            </motion.span>
          ) : null}
        </AnimatePresence>
      </TooltipTrigger>
      <TooltipPopup className="flex items-center gap-2" side="top" sideOffset={8}>
        <span className="flex items-center gap-2">
          {label}
          {shortcut && <Kbd className="h-4 min-w-4 text-[10px]">{shortcut}</Kbd>}
        </span>
      </TooltipPopup>
    </Tooltip>
  );
}

const MICS = ["Studio Display microphone", "MacBook Pro microphone", "AirPods Pro"];
const CAMERAS = ["FaceTime HD camera", "Studio Display camera"];

function DeviceSplit({ kind, devices, children }: { kind: string; devices: string[]; children: ReactNode }) {
  const [device, setDevice] = useState(devices[0]);
  return (
    <div className="flex shrink-0 items-center rounded-full bg-secondary">
      {children}
      <Menu>
        <MenuTrigger
          aria-label={`${kind} settings`}
          className={cn(
            "-ms-0.5 hidden h-10 w-7 cursor-pointer items-center justify-center rounded-e-full pe-1 text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring data-popup-open:text-foreground @md:flex [&_svg]:size-4",
          )}
        >
          <ChevronUpIcon />
        </MenuTrigger>
        <MenuPopup align="start" className={cn("w-60", menuPop)} side="top" sideOffset={10}>
          <MenuGroup>
            <MenuGroupLabel>{kind}</MenuGroupLabel>
            <MenuRadioGroup onValueChange={(v) => setDevice(v as string)} value={device}>
              {devices.map((d) => (
                <MenuRadioItem key={d} value={d}>
                  {d}
                </MenuRadioItem>
              ))}
            </MenuRadioGroup>
          </MenuGroup>
        </MenuPopup>
      </Menu>
    </div>
  );
}

type Reaction = { id: number; emoji: string; name: string; x: number };
type Toast = { id: number; text: string; person?: CallPerson; icon?: ReactNode };

export function VideoCall({
  className,
  title = "Design crit · Onboarding v3",
  you = defaultYou,
  participants = defaultParticipants,
  dropIns = defaultDropIns,
  messages: initialMessages = defaultMessages,
  scriptedMessages = defaultScript,
  elapsedSeconds = 1453,
  autoplay = true,
  startTime = "10:20",
  meetingCode = "kst-wqpf-ndr",
}: VideoCallProps) {
  const reduce = useReducedMotionSafe();
  const rootRef = useRef<HTMLDivElement>(null);
  const chatEndRef = useRef<HTMLDivElement>(null);
  const sheetRef = useRef<HTMLElement>(null);
  const counter = useRef(0);

  const [people, setPeople] = useState<Record<string, Live>>(() =>
    Object.fromEntries([you, ...participants, ...dropIns].map((p) => [p.id, { ...p }])),
  );
  const [inCall, setInCall] = useState<string[]>(() => [...participants.map((p) => p.id), you.id]);
  const [speakerId, setSpeakerId] = useState<string | null>(participants.find((p) => !p.muted)?.id ?? null);
  const [layout, setLayout] = useState<"grid" | "spotlight">("grid");
  const [panel, setPanel] = useState<null | "chat" | "people">(null);
  const [messages, setMessages] = useState(initialMessages);
  const [unread, setUnread] = useState(0);
  const [draft, setDraft] = useState("");
  const [reactions, setReactions] = useState<Reaction[]>([]);
  const [toast, setToast] = useState<Toast | null>(null);
  const [elapsed, setElapsed] = useState(elapsedSeconds);
  const [sharing, setSharing] = useState(false);
  const [left, setLeft] = useState(false);
  const [feedback, setFeedback] = useState<null | "up" | "down">(null);
  const [reactOpen, setReactOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const [wide, setWide] = useState(true);

  const me = people[you.id];
  const nextId = () => {
    counter.current += 1;
    return counter.current;
  };

  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const ro = new ResizeObserver(([entry]) => setWide(entry.contentRect.width >= 896));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const showToast = (t: Omit<Toast, "id">) => setToast({ ...t, id: nextId() });

  useEffect(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(null), 2600);
    return () => clearTimeout(t);
  }, [toast]);

  useEffect(() => {
    if (!copied) return;
    const t = setTimeout(() => setCopied(false), 1800);
    return () => clearTimeout(t);
  }, [copied]);

  const addReaction = (emoji: string, name: string) => {
    const r = { id: nextId(), emoji, name, x: 8 + Math.random() * 30 };
    setReactions((all) => [...all.slice(-7), r]);
  };

  const tickRef = useRef(0);
  const state = useRef({ inCall, people, panel });
  const elapsedRef = useRef(elapsed);
  elapsedRef.current = elapsed;
  state.current = { inCall, people, panel };

  useEffect(() => {
    if (left) return;
    const t = setInterval(() => {
      setElapsed((e) => e + 1);
      if (!autoplay) return;
      tickRef.current += 1;
      const tick = tickRef.current;
      const { inCall: ids, people: ppl, panel: openPanel } = state.current;

      if (tick % 3 === 0) {
        const talkers = ids.filter((id) => id !== you.id && !ppl[id]?.muted);
        if (talkers.length) {
          setSpeakerId((cur) => {
            const pool = talkers.filter((id) => id !== cur);
            return pool.length ? pool[Math.floor(Math.random() * pool.length)] : talkers[0];
          });
        } else setSpeakerId(null);
      }

      if (tick % 8 === 4 && dropIns.length) {
        const step = dropScript[Math.floor(tick / 8) % dropScript.length];
        const person = dropIns[step.index % dropIns.length];
        if (step.op === "join" && !ids.includes(person.id)) {
          setInCall((cur) => [...cur.filter((id) => id !== you.id), person.id, you.id]);
          showToast({ text: `${firstName(person.name)} joined`, person });
        } else if (step.op === "leave" && ids.includes(person.id)) {
          setInCall((cur) => cur.filter((id) => id !== person.id));
          setSpeakerId((cur) => (cur === person.id ? null : cur));
          showToast({ text: `${firstName(person.name)} left`, person });
        }
      }

      if (tick % 5 === 2) {
        const others = ids.filter((id) => id !== you.id);
        const who = ppl[others[Math.floor(Math.random() * others.length)]];
        if (who) addReaction(REACTIONS[Math.floor(Math.random() * 5)], firstName(who.name));
      }

      const line = tick % 14 === 10 ? scriptedMessages[Math.floor(tick / 14)] : undefined;
      if (line && ids.includes(line.authorId)) {
        setMessages((m) => [...m, { id: `s${tick}`, authorId: line.authorId, text: line.text, time: timeLabel(startTime, elapsedRef.current) }]);
        if (openPanel !== "chat") setUnread((u) => u + 1);
      }
    }, 1000);
    return () => clearInterval(t);
  }, [left, autoplay]);

  const sheetOpen = !wide && !!panel && !left;
  useEffect(() => {
    if (sheetOpen) sheetRef.current?.focus({ preventScroll: true });
  }, [sheetOpen]);

  useEffect(() => {
    if (panel === "chat") {
      setUnread(0);
      const box = chatEndRef.current?.parentElement;
      box?.scrollTo({ top: box.scrollHeight });
    }
  }, [panel, messages.length]);

  const patchMe = (patch: Partial<Live>) => setPeople((p) => ({ ...p, [you.id]: { ...p[you.id], ...patch } }));
  const toggleMic = () => patchMe({ muted: !me.muted });
  const toggleCam = () => patchMe({ cameraOff: !me.cameraOff });
  const toggleHand = () => {
    patchMe({ hand: !me.hand });
    if (!me.hand) showToast({ text: "You raised your hand", icon: <HandIcon className="size-3.5" /> });
  };
  const toggleShare = () => {
    setSharing((s) => !s);
    if (!sharing) {
      setLayout("spotlight");
      showToast({ text: "You're presenting to everyone", icon: <MonitorUpIcon className="size-3.5" /> });
    } else setLayout("grid");
  };
  const muteAll = () => {
    setPeople((p) =>
      Object.fromEntries(Object.entries(p).map(([id, v]) => [id, id === you.id ? v : { ...v, muted: true }])),
    );
    setSpeakerId(null);
    showToast({ text: "Everyone else is muted", icon: <MicOffIcon className="size-3.5" /> });
  };
  const removePerson = (id: string) => {
    setInCall((cur) => cur.filter((x) => x !== id));
    setSpeakerId((cur) => (cur === id ? null : cur));
    showToast({ text: `${firstName(people[id].name)} was removed`, person: people[id] });
  };
  const send = () => {
    const text = draft.trim();
    if (!text) return;
    setMessages((m) => [...m, { id: `u${nextId()}`, authorId: you.id, text, time: timeLabel(startTime, elapsed) }]);
    setDraft("");
  };
  const leave = () => {
    setLeft(true);
    setPanel(null);
    setSharing(false);
    setReactions([]);
    setToast(null);
  };
  const rejoin = () => {
    setLeft(false);
    setFeedback(null);
    setLayout("grid");
  };

  const ordered = [...inCall].sort((a, b) => Number(!!people[b]?.hand) - Number(!!people[a]?.hand));
  const tiles: Tile[] = [
    ...(sharing ? [{ kind: "screen" } as Tile] : []),
    ...ordered.map((id) => ({ kind: "person", person: people[id], isYou: id === you.id }) as Tile),
  ];
  const shape = gridShape(tiles.length);
  const spotKey = sharing ? "screen" : speakerId && inCall.includes(speakerId) ? speakerId : ordered[0];
  const spotTile = tiles.find((t) => tileKey(t) === spotKey) ?? tiles[0];
  const stripTiles = tiles.filter((t) => t !== spotTile);
  const host = participants.find((p) => p.host);

  const panelBody =
    panel === "chat" ? (
      <div className="flex min-h-0 flex-1 flex-col">
        <div className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
          {messages.length === 0 ? (
            <div className="flex h-full flex-col items-center justify-center gap-2 px-6 text-center">
              <span className="flex size-10 items-center justify-center rounded-xl bg-muted text-muted-foreground">
                <MessageSquareIcon className="size-4.5" />
              </span>
              <span className="font-medium">No messages yet</span>
              <span className="text-pretty text-muted-foreground text-xs">
                Only people in the call can see chat. It disappears when the call ends.
              </span>
            </div>
          ) : (
            <ul className="flex flex-col gap-4">
              <AnimatePresence initial={false}>
                {messages.map((m, i) => {
                  const author = people[m.authorId];
                  const grouped = messages[i - 1]?.authorId === m.authorId;
                  return (
                    <motion.li
                      animate={{ opacity: 1, y: 0 }}
                      className={cn("flex gap-2.5", grouped && "-mt-3")}
                      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}
                      key={m.id}
                      transition={{ duration: 0.2, ease: easeOut }}
                    >
                      {grouped ? (
                        <span className="w-7 shrink-0" />
                      ) : author ? (
                        <PersonAvatar person={author} />
                      ) : null}
                      <div className="flex min-w-0 flex-col gap-0.5">
                        {!grouped && (
                          <span className="flex items-baseline gap-2">
                            <span className="font-medium text-xs">
                              {m.authorId === you.id ? "You" : author?.name}
                            </span>
                            <span className="text-muted-foreground text-xs tabular-nums">{m.time}</span>
                          </span>
                        )}
                        <p className="text-pretty break-words">{m.text}</p>
                      </div>
                    </motion.li>
                  );
                })}
              </AnimatePresence>
            </ul>
          )}
          <div ref={chatEndRef} />
        </div>
        <form
          className="border-t p-3"
          onSubmit={(e) => {
            e.preventDefault();
            send();
          }}
        >
          <InputGroup>
            <InputGroupInput
              aria-label="Message everyone"
              onChange={(e) => setDraft(e.target.value)}
              placeholder="Message everyone"
              value={draft}
            />
            <InputGroupAddon align="inline-end">
              <Button aria-label="Send message" disabled={!draft.trim()} size="icon-xs" type="submit" variant="ghost">
                <SendHorizontalIcon />
              </Button>
            </InputGroupAddon>
          </InputGroup>
        </form>
      </div>
    ) : (
      <div className="flex min-h-0 flex-1 flex-col">
        <div className="flex items-center justify-between px-4 pt-3 pb-1">
          <span className="font-medium text-muted-foreground text-xs">
            In call · <span className="tabular-nums">{inCall.length}</span>
          </span>
          <Button className={press} onClick={muteAll} size="xs" variant="outline">
            <MicOffIcon /> Mute all
          </Button>
        </div>
        <ul className="min-h-0 flex-1 overflow-y-auto px-2 py-1">
          <AnimatePresence initial={false}>
            {ordered.map((id) => {
              const p = people[id];
              const isYou = id === you.id;
              return (
                <motion.li
                  animate={{ opacity: 1 }}
                  className="flex items-center gap-3 rounded-lg px-2 py-2"
                  exit={{ opacity: 0, transition: { duration: 0.12 } }}
                  initial={{ opacity: 0 }}
                  key={id}
                  layout={reduce ? false : "position"}
                  transition={spring}
                >
                  <PersonAvatar person={p} size={8} />
                  <div className="flex min-w-0 flex-1 flex-col">
                    <span className="flex items-center gap-1.5 truncate font-medium">
                      <span className="truncate">{p.name}</span>
                      {isYou && <span className="font-normal text-muted-foreground text-xs">you</span>}
                      {p.host && (
                        <span className="rounded-full bg-muted px-1.5 py-px font-medium text-[10px] text-muted-foreground">
                          Host
                        </span>
                      )}
                    </span>
                    <span className="truncate text-muted-foreground text-xs">{p.title}</span>
                  </div>
                  <span className="flex items-center gap-1.5 text-muted-foreground [&_svg]:size-4">
                    {p.hand && <HandIcon aria-label="Hand raised" className="text-amber-500 dark:text-amber-400" />}
                    {p.cameraOff && <VideoOffIcon aria-label="Camera off" />}
                    {p.muted ? (
                      <MicOffIcon aria-label="Muted" className="text-destructive-foreground" />
                    ) : speakerId === id ? (
                      <span className="flex size-4 items-center justify-center">
                        <SpeakingBars reduce={reduce} />
                      </span>
                    ) : (
                      <MicIcon aria-label="Mic on" />
                    )}
                  </span>
                  {!isYou && (
                    <Menu>
                      <MenuTrigger
                        aria-label={`Options for ${p.name}`}
                        render={<Button className="-me-1" size="icon-sm" variant="ghost" />}
                      >
                        <EllipsisIcon />
                      </MenuTrigger>
                      <MenuPopup align="end" className={cn("w-44", menuPop)}>
                        <MenuItem
                          disabled={p.muted}
                          onClick={() => setPeople((all) => ({ ...all, [id]: { ...all[id], muted: true } }))}
                        >
                          <MicOffIcon /> Mute
                        </MenuItem>
                        <MenuItem onClick={() => setSpeakerId(id)}>
                          <PresentationIcon /> Spotlight
                        </MenuItem>
                        <MenuSeparator />
                        <MenuItem onClick={() => removePerson(id)} variant="destructive">
                          <XIcon /> Remove from call
                        </MenuItem>
                      </MenuPopup>
                    </Menu>
                  )}
                </motion.li>
              );
            })}
          </AnimatePresence>
        </ul>
        <div className="border-t p-3">
          <Button className={cn("w-full", press)} onClick={() => setCopied(true)} variant="outline">
            <AnimatePresence initial={false} mode="popLayout">
              <motion.span
                animate={{ opacity: 1, scale: 1 }}
                className="flex items-center gap-2"
                exit={{ opacity: 0, scale: 0.95 }}
                initial={{ opacity: 0, scale: 0.95 }}
                key={copied ? "copied" : "copy"}
                transition={{ duration: 0.15, ease: easeOut }}
              >
                {copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
                {copied ? "Link copied" : "Copy invite link"}
              </motion.span>
            </AnimatePresence>
          </Button>
        </div>
      </div>
    );

  return (
    <TooltipProvider delay={500}>
      <div
        className={cn(
          "@container relative isolate flex h-[760px] w-full flex-col overflow-hidden bg-background text-foreground text-sm",
          className,
        )}
        onKeyDown={(e) => {
          if (e.key === "Escape" && panel && !wide) setPanel(null);
          const target = e.target as HTMLElement;
          if (left || e.metaKey || e.ctrlKey || e.altKey || target.closest("input, textarea")) return;
          if (e.key === "m") toggleMic();
          if (e.key === "v") toggleCam();
        }}
        ref={rootRef}
      >
        <header className="flex h-14 shrink-0 items-center gap-3 px-4">
          <div className="flex min-w-0 flex-1 items-center gap-3">
            <div className="flex min-w-0 flex-col">
              <span className="truncate font-heading font-semibold">{title}</span>
              <span className="flex items-center gap-1.5 text-muted-foreground text-xs">
                <span className="tabular-nums">{timeLabel(startTime, elapsed)}</span>
                <span aria-hidden>·</span>
                <span className="truncate">
                  {left ? "You left" : <><span className="tabular-nums">{inCall.length}</span> in call</>}
                </span>
              </span>
            </div>
            {!left && (
              <span className="flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-destructive/10 px-2 font-medium text-destructive-foreground text-xs tabular-nums">
                <span className="relative flex size-1.5">
                  {!reduce && <span className="absolute inset-0 animate-ping rounded-full bg-destructive/60" />}
                  <span className="relative size-1.5 rounded-full bg-destructive" />
                </span>
                {clock(elapsed)}
              </span>
            )}
          </div>
          {!left && (
            <div className="flex items-center gap-2">
              <div aria-label="Layout" className="relative hidden h-8 items-center rounded-lg bg-muted p-0.5 @md:flex" role="radiogroup">
                {(
                  [
                    { value: "grid", label: "Grid", icon: <LayoutGridIcon /> },
                    { value: "spotlight", label: "Spotlight", icon: <PresentationIcon /> },
                  ] as const
                ).map((o) => (
                  <button
                    aria-checked={layout === o.value}
                    aria-label={`${o.label} layout`}
                    className="relative flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2.5 font-medium text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-3.5"
                    key={o.value}
                    onClick={() => setLayout(o.value)}
                    role="radio"
                    type="button"
                  >
                    {layout === o.value && (
                      <motion.span
                        className="absolute inset-0 rounded-md bg-background shadow-xs/5 dark:bg-input"
                        layoutId="vc-layout-pill"
                        transition={reduce ? { duration: 0 } : spring}
                      />
                    )}
                    <span
                      className={cn(
                        "relative flex items-center gap-1.5 transition-colors",
                        layout === o.value ? "text-foreground" : "text-muted-foreground",
                      )}
                    >
                      {o.icon}
                      <span className="hidden @3xl:inline">{o.label}</span>
                    </span>
                  </button>
                ))}
              </div>
            </div>
          )}
        </header>

        <div className="flex min-h-0 flex-1 gap-3 px-3 pb-0 @md:px-4">
          <LayoutGroup id="vc-stage">
            <section aria-label="Participants" className="@container/stage relative min-w-0 flex-1">
              {left ? (
                <motion.div
                  animate={{ opacity: 1, y: 0 }}
                  className="flex h-full flex-col items-center justify-center gap-6 rounded-2xl border bg-card px-6 text-center"
                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}
                  transition={{ duration: 0.25, ease: easeOut }}
                >
                  <span className="flex size-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
                    <PhoneOffIcon className="size-5" />
                  </span>
                  <div className="flex flex-col gap-1">
                    <h2 className="text-balance font-heading font-semibold text-lg">You left the call</h2>
                    <p className="text-muted-foreground">
                      {title} · <span className="tabular-nums">{clock(elapsed)}</span>
                    </p>
                  </div>
                  <Button className={press} onClick={rejoin}>
                    Rejoin
                  </Button>
                  <div className="flex min-h-9 flex-col items-center gap-2">
                    <AnimatePresence initial={false} mode="popLayout">
                      {feedback ? (
                        <motion.span
                          animate={{ opacity: 1, scale: 1 }}
                          className="flex h-9 items-center gap-1.5 text-muted-foreground text-xs"
                          initial={{ opacity: 0, scale: 0.97 }}
                          key="thanks"
                          transition={{ duration: 0.2, ease: easeOut }}
                        >
                          <CheckIcon className="size-3.5 text-success-foreground" /> Thanks, that helps us tune call quality.
                        </motion.span>
                      ) : (
                        <motion.div
                          className="flex items-center gap-2 text-muted-foreground text-xs"
                          exit={{ opacity: 0, scale: 0.97, transition: { duration: 0.12 } }}
                          key="ask"
                        >
                          How was the audio and video?
                          <Button aria-label="Good" className={press} onClick={() => setFeedback("up")} size="icon-sm" variant="outline">
                            <ThumbsUpIcon />
                          </Button>
                          <Button aria-label="Poor" className={press} onClick={() => setFeedback("down")} size="icon-sm" variant="outline">
                            <ThumbsDownIcon />
                          </Button>
                        </motion.div>
                      )}
                    </AnimatePresence>
                  </div>
                </motion.div>
              ) : (
                <AnimatePresence initial={false} mode="popLayout">
                  <motion.div
                    animate={{ opacity: 1, scale: 1 }}
                    className="size-full"
                    exit={{ opacity: 0, transition: { duration: 0.12, ease: easeOut } }}
                    initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.985 }}
                    key={layout}
                    transition={{ duration: 0.2, ease: easeOut }}
                  >
                    {layout === "grid" ? (
                      <div
                        className="flex h-full flex-wrap content-center justify-center gap-2 [--cc:var(--cs)] [--rr:var(--rs)] @xl/stage:[--cc:var(--c)] @xl/stage:[--rr:var(--r)]"
                        style={{ "--c": shape.c, "--r": shape.r, "--cs": shape.cs, "--rs": shape.rs } as CSSProperties}
                      >
                        <AnimatePresence initial={false} mode="popLayout">
                          {tiles.map((t) => (
                            <CallTile
                              key={tileKey(t)}
                              ns="grid"
                              speaking={t.kind === "person" && t.person.id === speakerId && !t.person.muted}
                              style={{
                                width: "calc((100% - (var(--cc) - 1) * 0.5rem) / var(--cc))",
                                height: "calc((100% - (var(--rr) - 1) * 0.5rem) / var(--rr))",
                              }}
                              tile={t}
                              you={you}
                            />
                          ))}
                        </AnimatePresence>
                      </div>
                    ) : (
                      <div className="flex h-full flex-col gap-2">
                        <div className="relative min-h-0 flex-1">
                          <AnimatePresence initial={false} mode="popLayout">
                            {spotTile && (
                              <CallTile
                                className="size-full"
                                key={tileKey(spotTile)}
                                speaking={spotTile.kind === "person" && spotTile.person.id === speakerId && !spotTile.person.muted}
                                tile={spotTile}
                                you={you}
                              />
                            )}
                          </AnimatePresence>
                        </div>
                        <div className="-mx-1 -mb-1 flex h-26 shrink-0 justify-center-safe gap-2 overflow-x-auto p-1 [scrollbar-width:none] @xl/stage:h-30">
                          <AnimatePresence initial={false} mode="popLayout">
                            {stripTiles.map((t) => (
                              <CallTile
                                className="aspect-[4/3] h-full"
                                compact
                                key={tileKey(t)}
                                ns="strip"
                                speaking={t.kind === "person" && t.person.id === speakerId && !t.person.muted}
                                tile={t}
                                you={you}
                              />
                            ))}
                          </AnimatePresence>
                        </div>
                      </div>
                    )}
                  </motion.div>
                </AnimatePresence>
              )}

              <div aria-hidden className="pointer-events-none absolute inset-0 z-20 overflow-hidden rounded-2xl">
                <AnimatePresence>
                  {reactions.map((r) => (
                    <motion.div
                      animate={
                        reduce
                          ? { opacity: [0, 1, 1, 0] }
                          : { opacity: [0, 1, 1, 0], y: -260, x: [0, 10, -6, 4], scale: [0.9, 1.05, 1, 1] }
                      }
                      className="absolute bottom-3 flex flex-col items-center gap-1"
                      initial={{ opacity: 0, y: 0, scale: 0.9 }}
                      key={r.id}
                      onAnimationComplete={() => setReactions((all) => all.filter((x) => x.id !== r.id))}
                      style={{ left: `${r.x}%` }}
                      transition={{ duration: reduce ? 1.6 : 2.4, ease: easeOut, times: [0, 0.12, 0.7, 1] }}
                    >
                      <span className="text-3xl leading-none drop-shadow-sm">{r.emoji}</span>
                      <span className="rounded-md bg-background/85 px-1.5 py-px font-medium text-[10px] text-foreground shadow-xs/5 backdrop-blur-sm">{r.name}</span>
                    </motion.div>
                  ))}
                </AnimatePresence>
              </div>

              <div aria-live="polite" className="pointer-events-none absolute inset-x-0 top-3 z-30 flex justify-center px-3">
                <AnimatePresence mode="popLayout">
                  {toast && (
                    <motion.div
                      animate={{ opacity: 1, y: 0, scale: 1 }}
                      className="flex h-8 max-w-full items-center gap-2 rounded-full border bg-popover ps-1 pe-3 text-popover-foreground text-xs shadow-lg/5"
                      exit={{ opacity: 0, y: -4, scale: 0.97, transition: { duration: 0.15, ease: easeOut } }}
                      initial={reduce ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.97 }}
                      key={toast.id}
                      transition={{ duration: 0.22, ease: easeOut }}
                    >
                      {toast.person ? (
                        <PersonAvatar person={toast.person} size={6} />
                      ) : (
                        <span className="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground">
                          {toast.icon}
                        </span>
                      )}
                      <span className="truncate font-medium">{toast.text}</span>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            </section>
          </LayoutGroup>

          <AnimatePresence initial={false}>
            {wide && panel && !left && (
              <motion.aside
                animate={{ opacity: 1, x: 0 }}
                aria-label={panel === "chat" ? "Chat" : "People"}
                className="flex w-80 shrink-0 flex-col overflow-hidden rounded-2xl border bg-card"
                exit={{ opacity: 0, x: 16, transition: { duration: 0.15, ease: easeOut } }}
                initial={reduce ? { opacity: 0 } : { opacity: 0, x: 16 }}
                key="dock"
                transition={{ duration: 0.22, ease: easeOut }}
              >
                <PanelHeader messages={messages.length} onClose={() => setPanel(null)} onTab={setPanel} panel={panel} people={inCall.length} />
                {panelBody}
              </motion.aside>
            )}
          </AnimatePresence>
        </div>

        <footer className="flex h-[72px] shrink-0 items-center gap-3 px-4">
          <div className="hidden min-w-0 flex-1 items-center gap-2 text-muted-foreground text-xs @4xl:flex">
            <button
              className="flex h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2 font-mono outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
              onClick={() => setCopied(true)}
              type="button"
            >
              {copied ? <CheckIcon className="size-3.5 text-success-foreground" /> : <CopyIcon className="size-3.5" />}
              {meetingCode}
            </button>
            <span className="truncate">
              Hosted by {host ? (host.id === you.id ? "you" : firstName(host.name)) : "you"}
            </span>
          </div>
          {left ? (
            <div className="flex-1" />
          ) : (
            <div className="mx-auto flex items-center gap-2 @4xl:mx-0" role="toolbar" aria-label="Call controls">
              <DeviceSplit devices={MICS} kind="Microphone">
                <ControlButton
                  className={me.muted ? undefined : "bg-transparent"}
                  label={me.muted ? "Unmute" : "Mute"}
                  onClick={toggleMic}
                  pressed={me.muted}
                  shortcut="M"
                  tone={me.muted ? "off" : "default"}
                >
                  {me.muted ? <MicOffIcon /> : <MicIcon />}
                </ControlButton>
              </DeviceSplit>
              <DeviceSplit devices={CAMERAS} kind="Camera">
                <ControlButton
                  className={me.cameraOff ? undefined : "bg-transparent"}
                  label={me.cameraOff ? "Turn camera on" : "Turn camera off"}
                  onClick={toggleCam}
                  pressed={me.cameraOff}
                  shortcut="V"
                  tone={me.cameraOff ? "off" : "default"}
                >
                  {me.cameraOff ? <VideoOffIcon /> : <VideoIcon />}
                </ControlButton>
              </DeviceSplit>
              <ControlButton
                className="hidden @md:flex"
                label={sharing ? "Stop presenting" : "Present screen"}
                onClick={toggleShare}
                pressed={sharing}
                tone={sharing ? "on" : "default"}
              >
                <MonitorUpIcon />
              </ControlButton>
              <Popover onOpenChange={setReactOpen} open={reactOpen}>
                <PopoverTrigger
                  aria-label="Send a reaction"
                  className={cn(
                    "flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full bg-secondary text-secondary-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-popup-open:bg-accent [&_svg]:size-[18px]",
                    press,
                  )}
                >
                  <SmilePlusIcon />
                </PopoverTrigger>
                <PopoverPopup className={cn("rounded-full before:rounded-full", menuPop)} side="top" sideOffset={10} tooltipStyle>
                  <div aria-label="Reactions" className="flex items-center gap-0.5 py-0.5" role="group">
                    {REACTIONS.map((emoji) => (
                      <button
                        aria-label={`React ${emoji}`}
                        className="flex size-9 cursor-pointer items-center justify-center rounded-full text-xl outline-none transition-[background-color,scale] duration-150 ease-out hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.92]"
                        key={emoji}
                        onClick={() => addReaction(emoji, "You")}
                        type="button"
                      >
                        {emoji}
                      </button>
                    ))}
                  </div>
                </PopoverPopup>
              </Popover>
              <ControlButton
                className="hidden @md:flex"
                label={me.hand ? "Lower hand" : "Raise hand"}
                onClick={toggleHand}
                pressed={!!me.hand}
                tone={me.hand ? "on" : "default"}
              >
                <HandIcon />
              </ControlButton>
              <ControlButton
                badge={panel === "chat" ? 0 : unread}
                className="@4xl:hidden"
                label="Chat"
                onClick={() => setPanel(panel === "chat" ? null : "chat")}
                pressed={panel === "chat"}
                tone={panel === "chat" ? "on" : "default"}
              >
                <MessageSquareIcon />
              </ControlButton>
              <Menu>
                <MenuTrigger
                  aria-label="More options"
                  className={cn(
                    "flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full bg-secondary text-secondary-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-popup-open:bg-accent [&_svg]:size-[18px]",
                    press,
                  )}
                >
                  <EllipsisIcon />
                </MenuTrigger>
                <MenuPopup align="center" className={cn("w-52", menuPop)} side="top" sideOffset={10}>
                  <div className="@md:hidden">
                    <MenuItem onClick={toggleShare}>
                      <MonitorUpIcon /> {sharing ? "Stop presenting" : "Present screen"}
                    </MenuItem>
                    <MenuItem onClick={toggleHand}>
                      <HandIcon /> {me.hand ? "Lower hand" : "Raise hand"}
                    </MenuItem>
                  </div>
                  <MenuItem onClick={() => setPanel("people")}>
                    <UsersIcon /> People
                  </MenuItem>
                  <MenuSeparator />
                  <MenuGroup>
                    <MenuGroupLabel>Layout</MenuGroupLabel>
                    <MenuRadioGroup onValueChange={(v) => setLayout(v as "grid" | "spotlight")} value={layout}>
                      <MenuRadioItem value="grid">Grid</MenuRadioItem>
                      <MenuRadioItem value="spotlight">Spotlight</MenuRadioItem>
                    </MenuRadioGroup>
                  </MenuGroup>
                </MenuPopup>
              </Menu>
              <Tooltip>
                <TooltipTrigger
                  render={
                    <button
                      aria-label="Leave call"
                      className={cn(
                        "ms-1 flex h-10 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-destructive px-3.5 font-medium text-white outline-none hover:bg-destructive/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background @md:px-4",
                        press,
                      )}
                      onClick={leave}
                      type="button"
                    />
                  }
                >
                  <PhoneOffIcon className="size-[18px]" />
                  <span className="hidden @md:inline">Leave</span>
                </TooltipTrigger>
                <TooltipPopup side="top" sideOffset={8}>
                  Leave call
                </TooltipPopup>
              </Tooltip>
            </div>
          )}
          <div className="hidden flex-1 items-center justify-end gap-2 @4xl:flex">
            {!left && (
              <>
                <ControlButton
                  badge={panel === "chat" ? 0 : unread}
                  label="Chat"
                  onClick={() => setPanel(panel === "chat" ? null : "chat")}
                  pressed={panel === "chat"}
                  tone={panel === "chat" ? "on" : "default"}
                >
                  <MessageSquareIcon />
                </ControlButton>
                <ControlButton
                  label="People"
                  onClick={() => setPanel(panel === "people" ? null : "people")}
                  pressed={panel === "people"}
                  tone={panel === "people" ? "on" : "default"}
                >
                  <UsersIcon />
                </ControlButton>
              </>
            )}
          </div>
        </footer>

        <AnimatePresence>
          {!wide && panel && !left && (
            <>
              <motion.div
                animate={{ opacity: 1 }}
                aria-hidden
                className="absolute inset-0 z-40 bg-black/32"
                exit={{ opacity: 0 }}
                initial={{ opacity: 0 }}
                key="scrim"
                onClick={() => setPanel(null)}
                transition={{ duration: 0.2, ease: easeOut }}
              />
              <motion.aside
                animate={{ y: 0 }}
                aria-label={panel === "chat" ? "Chat" : "People"}
                aria-modal
                ref={sheetRef}
                tabIndex={-1}
                className="absolute inset-x-0 top-16 bottom-0 z-50 flex flex-col overflow-hidden rounded-t-2xl border-t bg-card shadow-lg/10 outline-none"
                exit={{ y: "100%", transition: { duration: 0.2, ease: easeOut } }}
                initial={{ y: "100%" }}
                key="sheet"
                role="dialog"
                transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 42 }}
              >
                <PanelHeader messages={messages.length} onClose={() => setPanel(null)} onTab={setPanel} panel={panel} people={inCall.length} />
                {panelBody}
              </motion.aside>
            </>
          )}
        </AnimatePresence>
      </div>
    </TooltipProvider>
  );
}

function PanelHeader({
  panel,
  onTab,
  onClose,
  messages,
  people,
}: {
  panel: "chat" | "people";
  onTab: (p: "chat" | "people") => void;
  onClose: () => void;
  messages: number;
  people: number;
}) {
  const reduce = useReducedMotionSafe();
  return (
    <div className="flex h-13 shrink-0 items-center gap-2 border-b ps-2 pe-2">
      <div className="flex items-center gap-0.5" role="tablist">
        {(
          [
            { value: "chat", label: "Chat", count: messages },
            { value: "people", label: "People", count: people },
          ] as const
        ).map((t) => (
          <button
            aria-selected={panel === t.value}
            className="relative flex h-8 cursor-pointer items-center gap-1.5 rounded-lg px-2.5 font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring"
            key={t.value}
            onClick={() => onTab(t.value)}
            role="tab"
            type="button"
          >
            {panel === t.value && (
              <motion.span
                className="absolute inset-0 rounded-lg bg-accent"
                layoutId="vc-panel-tab"
                transition={reduce ? { duration: 0 } : spring}
              />
            )}
            <span className={cn("relative transition-colors", panel === t.value ? "text-foreground" : "text-muted-foreground")}>
              {t.label}
            </span>
            <span className="relative text-muted-foreground text-xs tabular-nums">{t.count}</span>
          </button>
        ))}
      </div>
      <Button aria-label="Close panel" className="ms-auto" onClick={onClose} size="icon-sm" variant="ghost">
        <XIcon />
      </Button>
    </div>
  );
}
```
