# Calendar Week

> A week calendar you can push around: drag events to move or stretch them on a 15-minute snap, drag empty time to create one, nudge with the arrow keys, and watch the now line creep down the day.

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

- Page, with a live preview: https://shadcn.exchange/r/devl/calendar-week
- Screenshot: https://shadcn.exchange/shot/devl/calendar-week/light.png
- Registry item: https://devl.dev/r/calendar-week.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/calendar-week.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/button.json`, `https://coss.com/ui/r/calendar.json`, `https://coss.com/ui/r/kbd.json`, `https://coss.com/ui/r/popover.json`

## Files

### `registry/devl/calendar-week.tsx`

```tsx
"use client";

import { AnimatePresence, MotionConfig, motion } from "motion/react";
import {
  CalendarDaysIcon,
  CheckIcon,
  ChevronDownIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  ClockIcon,
  MapPinIcon,
  PlusIcon,
  Trash2Icon,
  UsersIcon,
  VideoIcon,
  XIcon,
} from "lucide-react";
import {
  type KeyboardEvent,
  type PointerEvent as ReactPointerEvent,
  type RefObject,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Kbd } from "@/components/ui/kbd";
import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";

export type CalendarTone = "teal" | "violet" | "sky" | "amber" | "rose";

export interface CalendarSource {
  id: string;
  name: string;
  tone: CalendarTone;
  visible?: boolean;
}

export interface CalendarEvent {
  id: string;
  title: string;
  calendar: string;
  date: string;
  start: number;
  end: number;
  allDay?: boolean;
  days?: number;
  location?: string;
  video?: boolean;
  people?: string[];
}

export interface CalendarWeekProps {
  calendars?: CalendarSource[];
  events?: CalendarEvent[];
  now?: string;
  className?: string;
}

const toneDot: Record<CalendarTone, string> = {
  teal: "bg-teal-500",
  violet: "bg-violet-500",
  sky: "bg-sky-500",
  amber: "bg-amber-500",
  rose: "bg-rose-500",
};

const toneBlock: Record<CalendarTone, string> = {
  teal: "bg-[color-mix(in_oklab,var(--color-teal-500)_14%,var(--background))] text-teal-950 before:bg-teal-500 dark:bg-[color-mix(in_oklab,var(--color-teal-400)_16%,var(--background))] dark:text-teal-50",
  violet: "bg-[color-mix(in_oklab,var(--color-violet-500)_14%,var(--background))] text-violet-950 before:bg-violet-500 dark:bg-[color-mix(in_oklab,var(--color-violet-400)_18%,var(--background))] dark:text-violet-50",
  sky: "bg-[color-mix(in_oklab,var(--color-sky-500)_14%,var(--background))] text-sky-950 before:bg-sky-500 dark:bg-[color-mix(in_oklab,var(--color-sky-400)_16%,var(--background))] dark:text-sky-50",
  amber: "bg-[color-mix(in_oklab,var(--color-amber-500)_18%,var(--background))] text-amber-950 before:bg-amber-500 dark:bg-[color-mix(in_oklab,var(--color-amber-400)_16%,var(--background))] dark:text-amber-50",
  rose: "bg-[color-mix(in_oklab,var(--color-rose-500)_14%,var(--background))] text-rose-950 before:bg-rose-500 dark:bg-[color-mix(in_oklab,var(--color-rose-400)_16%,var(--background))] dark:text-rose-50",
};

const toneSolid: Record<CalendarTone, string> = {
  teal: "bg-teal-600 text-white before:bg-teal-800 dark:bg-teal-500",
  violet: "bg-violet-600 text-white before:bg-violet-800 dark:bg-violet-500",
  sky: "bg-sky-600 text-white before:bg-sky-800 dark:bg-sky-500",
  amber: "bg-amber-500 text-amber-950 before:bg-amber-700",
  rose: "bg-rose-600 text-white before:bg-rose-800 dark:bg-rose-500",
};

const toneCheck: Record<CalendarTone, string> = {
  teal: "border-teal-500 bg-teal-500",
  violet: "border-violet-500 bg-violet-500",
  sky: "border-sky-500 bg-sky-500",
  amber: "border-amber-500 bg-amber-500",
  rose: "border-rose-500 bg-rose-500",
};

const HOUR = 48;
const PX = HOUR / 60;
const SNAP = 15;
const ease = [0.23, 1, 0.32, 1] as const;
const settle = { type: "spring", stiffness: 640, damping: 46 } as const;

const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const DOW = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];

function parse(iso: string) {
  const [y, m, d] = iso.split("-").map(Number);
  return new Date(Date.UTC(y, m - 1, d));
}
function iso(d: Date) {
  return d.toISOString().slice(0, 10);
}
function addDays(date: string, n: number) {
  const d = parse(date);
  d.setUTCDate(d.getUTCDate() + n);
  return iso(d);
}
function weekday(date: string) {
  return (parse(date).getUTCDay() + 6) % 7;
}
function startOfWeek(date: string) {
  return addDays(date, -weekday(date));
}
function isoWeek(date: string) {
  const d = parse(date);
  d.setUTCDate(d.getUTCDate() + 3 - ((d.getUTCDay() + 6) % 7));
  const jan4 = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
  return 1 + Math.round(((d.getTime() - jan4.getTime()) / 86400000 - 3 + ((jan4.getUTCDay() + 6) % 7)) / 7);
}

function diffDays(a: string, b: string) {
  return Math.round((parse(a).getTime() - parse(b).getTime()) / 86400000);
}
function fmtTime(min: number, withPeriod = true) {
  const h = Math.floor(min / 60) % 24;
  const m = min % 60;
  const h12 = ((h + 11) % 12) + 1;
  const period = h >= 12 ? "PM" : "AM";
  return `${h12}${m ? `:${String(m).padStart(2, "0")}` : ""}${withPeriod ? ` ${period}` : ""}`;
}
function fmtRange(a: number, b: number) {
  const samePeriod = a < 720 === b < 720;
  return `${fmtTime(a, !samePeriod)} – ${fmtTime(b)}`;
}
function fmtDuration(min: number) {
  const h = Math.floor(min / 60);
  const m = min % 60;
  return h ? `${h}h${m ? ` ${m}m` : ""}` : `${m}m`;
}
const snap = (min: number) => Math.round(min / SNAP) * SNAP;

const defaultCalendars: CalendarSource[] = [
  { id: "me", name: "Maya Okafor", tone: "teal", visible: true },
  { id: "team", name: "Team", tone: "violet", visible: true },
  { id: "product", name: "Product reviews", tone: "sky", visible: true },
  { id: "launches", name: "Launches", tone: "amber", visible: true },
  { id: "holidays", name: "Holidays", tone: "rose", visible: true },
];

function buildEvents(): CalendarEvent[] {
  const wk = "2026-10-19";
  const d = (n: number) => addDays(wk, n);
  const t = (h: number, m = 0) => h * 60 + m;
  const standups: CalendarEvent[] = [];
  for (let w = -1; w <= 1; w++) {
    for (let i = 0; i < 5; i++) {
      standups.push({
        id: `standup-${w}-${i}`,
        title: "Standup",
        calendar: "team",
        date: addDays(wk, w * 7 + i),
        start: t(9),
        end: t(9, 30),
        video: true,
      });
    }
  }
  return [
    ...standups,
    { id: "e1", title: "Pricing model review", calendar: "me", date: d(0), start: t(11), end: t(12), location: "Room Atlas", people: ["Jonas Weber", "Priya Raman"] },
    { id: "e2", title: "Onboarding v3 working session", calendar: "product", date: d(0), start: t(14), end: t(15, 30), video: true, people: ["Priya Raman", "Leo Marchetti"] },
    { id: "e3", title: "1:1 Jonas", calendar: "me", date: d(0), start: t(16), end: t(16, 30), video: true, people: ["Jonas Weber"] },
    { id: "e4", title: "Research readout", calendar: "team", date: d(1), start: t(10), end: t(11), location: "Big room", people: ["Ana Sousa"] },
    { id: "e5", title: "Lunch with Ellie", calendar: "me", date: d(1), start: t(12, 30), end: t(13, 30), location: "Canal Café" },
    { id: "e6", title: "Interview: Staff Engineer", calendar: "me", date: d(1), start: t(15), end: t(16), video: true, people: ["Sam Whitfield"] },
    { id: "e7", title: "Hiring sync", calendar: "team", date: d(1), start: t(15, 30), end: t(16, 15), people: ["Ana Sousa", "Sam Whitfield"] },
    { id: "e8", title: "Incident review: webhooks", calendar: "team", date: d(2), start: t(13), end: t(14), video: true, people: ["Sam Whitfield"] },
    { id: "e9", title: "Board prep", calendar: "me", date: d(2), start: t(16), end: t(17, 30), location: "Room Atlas" },
    { id: "e10", title: "Q4 pricing decision", calendar: "me", date: d(3), start: t(10), end: t(10, 45), video: true, people: ["Jonas Weber", "Priya Raman"] },
    { id: "e11", title: "Customer call: Fieldnote", calendar: "me", date: d(3), start: t(11, 30), end: t(12), video: true, people: ["Noor Haddad"] },
    { id: "e12", title: "Design crit", calendar: "product", date: d(3), start: t(14), end: t(15), location: "Studio", people: ["Priya Raman", "Leo Marchetti", "Ana Sousa"] },
    { id: "e13", title: "Release notes review", calendar: "launches", date: d(3), start: t(14, 30), end: t(15), people: ["Jonas Weber"] },
    { id: "e14", title: "Climbing", calendar: "me", date: d(3), start: t(18), end: t(19, 30), location: "Boulder Hall" },
    { id: "e15", title: "Focus: Q4 plan", calendar: "me", date: d(4), start: t(10), end: t(12) },
    { id: "e16", title: "Starter pricing goes live", calendar: "launches", date: d(4), start: t(15), end: t(15, 30) },
    { id: "e17", title: "Team drinks", calendar: "team", date: d(4), start: t(17), end: t(18, 30), location: "The Lantern" },
    { id: "e18", title: "Run club", calendar: "me", date: d(5), start: t(9), end: t(10) },
    { id: "a1", title: "Leo out", calendar: "team", date: d(3), start: 0, end: 0, allDay: true, days: 2 },
    { id: "a2", title: "Clocks go back", calendar: "holidays", date: d(6), start: 0, end: 0, allDay: true },
    { id: "n1", title: "Roadmap sync", calendar: "product", date: addDays(wk, 7), start: t(11), end: t(12), video: true },
    { id: "n2", title: "Lisbon offsite", calendar: "team", date: addDays(wk, 9), start: 0, end: 0, allDay: true, days: 2 },
    { id: "p1", title: "Quarterly planning", calendar: "product", date: addDays(wk, -5), start: t(13), end: t(16) },
  ];
}

interface Placed {
  ev: CalendarEvent;
  col: number;
  cols: number;
}

function placeDay(events: CalendarEvent[]): Placed[] {
  const sorted = [...events].sort((a, b) => a.start - b.start || b.end - a.end);
  const out: Placed[] = [];
  let cluster: Placed[] = [];
  let clusterEnd = -1;
  const flush = () => {
    const cols = Math.max(1, ...cluster.map((p) => p.col + 1));
    for (const p of cluster) p.cols = cols;
    out.push(...cluster);
    cluster = [];
  };
  for (const ev of sorted) {
    if (ev.start >= clusterEnd && cluster.length) flush();
    const used = new Set(cluster.filter((p) => p.ev.end > ev.start).map((p) => p.col));
    let col = 0;
    while (used.has(col)) col++;
    cluster.push({ ev, col, cols: 1 });
    clusterEnd = Math.max(clusterEnd, ev.end);
  }
  if (cluster.length) flush();
  return out;
}

function packAllDay(list: CalendarEvent[], first: string, count: number) {
  const rows: number[][] = [];
  return [...list]
    .sort((a, b) => a.date.localeCompare(b.date) || (b.days ?? 1) - (a.days ?? 1))
    .map((e) => {
      const startIdx = Math.max(0, diffDays(e.date, first));
      const endIdx = Math.min(count - 1, diffDays(addDays(e.date, (e.days ?? 1) - 1), first));
      let row = rows.findIndex((r) => r.every((c) => c < startIdx || c > endIdx));
      if (row < 0) row = rows.push([]) - 1;
      for (let c = startIdx; c <= endIdx; c++) rows[row].push(c);
      return { e, row, startIdx, endIdx };
    });
}

function toDate(date: string) {
  const [y, m, d] = date.split("-").map(Number);
  return new Date(y, m - 1, d);
}
function fromDate(d: Date) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}

function MiniMonth({
  month,
  onMonth,
  selected,
  today,
  weekStart,
  wholeWeek,
  onPick,
  busy,
}: {
  month: string;
  onMonth: (m: string) => void;
  selected: string;
  today: string;
  weekStart: string;
  wholeWeek: boolean;
  onPick: (d: string) => void;
  busy: Set<string>;
}) {
  const range = wholeWeek
    ? { from: toDate(weekStart), to: toDate(addDays(weekStart, 6)) }
    : { from: toDate(selected), to: toDate(selected) };
  return (
    <Calendar
      className="mx-auto p-0 [--cell-size:--spacing(8)] sm:[--cell-size:--spacing(8)]"
      mode="range"
      modifiers={{ busy: [...busy].filter((d) => d !== today).map(toDate) }}
      modifiersClassNames={{
        busy: "*:before:pointer-events-none *:before:absolute *:before:bottom-1 *:before:start-1/2 *:before:size-[3px] *:before:-translate-x-1/2 *:before:rounded-full *:before:bg-muted-foreground/60 [&[data-selected]:not(.range-middle)>*]:before:bg-primary-foreground/72",
      }}
      month={toDate(month)}
      onDayClick={(day) => onPick(fromDate(day))}
      onMonthChange={(m) => onMonth(fromDate(m))}
      onSelect={() => {}}
      selected={range}
      today={toDate(today)}
      weekStartsOn={1}
    />
  );
}

type DragState = {
  id: string;
  mode: "move" | "resize" | "create";
  x: number;
  y: number;
  start: number;
  end: number;
  date: string;
  moved: boolean;
  anchorMin: number;
};

type ViewMode = "day" | "week";

export function CalendarWeek({
  calendars: initialCalendars = defaultCalendars,
  events: initialEvents,
  now = "2026-10-22T10:40",
  className,
}: CalendarWeekProps) {
  const today = now.slice(0, 10);
  const [hh, mm] = now.slice(11, 16).split(":").map(Number);
  const [nowMin, setNowMin] = useState(hh * 60 + mm);
  const [calendars, setCalendars] = useState(initialCalendars);
  const [events, setEvents] = useState<CalendarEvent[]>(() => initialEvents ?? buildEvents());
  const [view, setView] = useState<ViewMode>("week");
  const [focusDay, setFocusDay] = useState(today);
  const [month, setMonth] = useState(today.slice(0, 8) + "01");
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [editing, setEditing] = useState<string | null>(null);
  const [drag, setDrag] = useState<DragState | null>(null);
  const [narrow, setNarrow] = useState(false);
  const [docked, setDocked] = useState(true);
  const [toast, setToast] = useState<{ key: number; text: string; undo?: () => void } | null>(null);
  const rootRef = useRef<HTMLDivElement>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const gridRef = useRef<HTMLDivElement>(null);
  const titleRef = useRef<HTMLInputElement>(null);
  const mainRef = useRef<HTMLElement>(null);
  const [allDayAt, setAllDayAt] = useState({ left: 12, top: 112 });

  useEffect(() => {
    const id = window.setInterval(() => setNowMin((m) => Math.min(m + 1, 24 * 60 - 1)), 60000);
    return () => window.clearInterval(id);
  }, []);

  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const ro = new ResizeObserver(([e]) => {
      setNarrow(e.contentRect.width < 672);
      setDocked(e.contentRect.width >= 896);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  useLayoutEffect(() => {
    if (scrollRef.current) scrollRef.current.scrollTop = 8 * HOUR - 12;
  }, []);

  useEffect(() => {
    if (!toast) return;
    const id = window.setTimeout(() => setToast(null), 4500);
    return () => window.clearTimeout(id);
  }, [toast]);

  const mode: ViewMode = narrow ? "day" : view;
  const weekStart = startOfWeek(focusDay);
  const days = mode === "week" ? Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)) : [focusDay];
  const visibleCal = new Set(calendars.filter((c) => c.visible !== false).map((c) => c.id));
  const calOf = (id: string) => calendars.find((c) => c.id === id) ?? calendars[0];
  const shown = events.filter((e) => visibleCal.has(e.calendar));
  const timed = shown.filter((e) => !e.allDay);
  const allDay = shown.filter((e) => e.allDay && diffDays(addDays(e.date, (e.days ?? 1) - 1), days[0]) >= 0 && diffDays(e.date, days[days.length - 1]) <= 0);
  const busy = new Set(shown.map((e) => e.date));
  const selected = events.find((e) => e.id === selectedId) ?? null;

  const patch = (id: string, p: Partial<CalendarEvent>) => setEvents((es) => es.map((e) => (e.id === id ? { ...e, ...p } : e)));

  const goto = (d: string) => {
    setFocusDay(d);
    setMonth(d.slice(0, 8) + "01");
  };
  const step = (dir: number) => goto(addDays(focusDay, dir * (mode === "week" ? 7 : 1)));

  const header = () => {
    const a = parse(days[0]);
    const b = parse(days[days.length - 1]);
    if (mode === "day" && !narrow) return { main: `${MONTHS[a.getUTCMonth()]} ${a.getUTCDate()}`, sub: DAY_NAMES[weekday(days[0])] };
    if (a.getUTCMonth() === b.getUTCMonth()) return { main: MONTHS[a.getUTCMonth()], sub: String(a.getUTCFullYear()) };
    return { main: `${MONTHS[a.getUTCMonth()].slice(0, 3)} – ${MONTHS[b.getUTCMonth()].slice(0, 3)}`, sub: String(b.getUTCFullYear()) };
  };
  const head = header();

  const columnWidth = () => (gridRef.current ? gridRef.current.getBoundingClientRect().width / days.length : 1);

  const minuteAt = (clientY: number) => {
    const r = gridRef.current?.getBoundingClientRect();
    if (!r) return 0;
    return Math.max(0, Math.min(24 * 60, (clientY - r.top) / PX));
  };

  const beginDrag = (e: ReactPointerEvent, ev: CalendarEvent, m: "move" | "resize") => {
    if (e.button !== 0) return;
    e.stopPropagation();
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
    setDrag({ id: ev.id, mode: m, x: e.clientX, y: e.clientY, start: ev.start, end: ev.end, date: ev.date, moved: false, anchorMin: 0 });
  };

  const beginCreate = (e: ReactPointerEvent, date: string) => {
    if (e.button !== 0) return;
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
    const at = Math.floor(minuteAt(e.clientY) / SNAP) * SNAP;
    const ev: CalendarEvent = { id: `new-${Date.now()}`, title: "", calendar: calendars[0]?.id ?? "me", date, start: at, end: at + 30 };
    setEvents((es) => [...es, ev]);
    setSelectedId(null);
    setEditing(null);
    setDrag({ id: ev.id, mode: "create", x: e.clientX, y: e.clientY, start: at, end: at + 30, date, moved: false, anchorMin: at });
  };

  const onPointerMove = (e: ReactPointerEvent) => {
    if (!drag) return;
    const dy = e.clientY - drag.y;
    const dx = e.clientX - drag.x;
    const moved = drag.moved || Math.abs(dy) > 3 || Math.abs(dx) > 3;
    if (!moved) return;
    if (!drag.moved) setDrag({ ...drag, moved: true });
    const deltaMin = snap(dy / PX);
    if (drag.mode === "move") {
      const len = drag.end - drag.start;
      const start = Math.max(0, Math.min(24 * 60 - len, drag.start + deltaMin));
      const dDay = mode === "week" ? Math.round(dx / columnWidth()) : 0;
      const idx = Math.max(0, Math.min(days.length - 1, diffDays(drag.date, days[0]) + dDay));
      patch(drag.id, { start, end: start + len, date: days[idx] });
    } else if (drag.mode === "resize") {
      const end = Math.max(drag.start + SNAP, Math.min(24 * 60, drag.end + deltaMin));
      patch(drag.id, { end });
    } else {
      const at = snap(minuteAt(e.clientY));
      const a = Math.min(drag.anchorMin, at);
      const b = Math.max(drag.anchorMin + SNAP, at);
      patch(drag.id, { start: a, end: Math.max(a + SNAP, b) });
    }
  };

  const onPointerUp = () => {
    if (!drag) return;
    const d = drag;
    setDrag(null);
    if (d.mode === "create") {
      setSelectedId(d.id);
      setEditing(d.id);
      requestAnimationFrame(() => titleRef.current?.focus());
      return;
    }
    if (!d.moved) {
      setSelectedId(d.id);
      setEditing(null);
      return;
    }
    const ev = events.find((x) => x.id === d.id);
    if (ev && (ev.start !== d.start || ev.end !== d.end || ev.date !== d.date)) {
      setSelectedId(d.id);
      setToast({
        key: Date.now(),
        text: `${ev.title || "Event"} → ${DOW[weekday(ev.date)]} ${fmtRange(ev.start, ev.end)}`,
        undo: () => {
          patch(d.id, { start: d.start, end: d.end, date: d.date });
          setToast(null);
          rootRef.current?.focus({ preventScroll: true });
        },
      });
    }
  };

  const remove = (id: string) => {
    const ev = events.find((e) => e.id === id);
    if (!ev) return;
    setEvents((es) => es.filter((e) => e.id !== id));
    setSelectedId(null);
    setEditing(null);
    if (ev.title) {
      setToast({
        key: Date.now(),
        text: `Deleted “${ev.title}”`,
        undo: () => {
          setEvents((es) => [...es, ev]);
          setToast(null);
          rootRef.current?.focus({ preventScroll: true });
        },
      });
    }
    rootRef.current?.focus({ preventScroll: true });
  };

  const closeEditor = () => {
    if (editing) {
      const ev = events.find((e) => e.id === editing);
      if (ev && !ev.title.trim()) setEvents((es) => es.filter((e) => e.id !== editing));
    }
    setEditing(null);
    setSelectedId(null);
    rootRef.current?.focus({ preventScroll: true });
  };

  const newEventNow = () => {
    const date = mode === "day" ? focusDay : days.includes(today) ? today : days[0];
    const start = Math.ceil((date === today ? nowMin : 9 * 60) / 30) * 30;
    const ev: CalendarEvent = { id: `new-${Date.now()}`, title: "", calendar: calendars[0]?.id ?? "me", date, start, end: start + 30 };
    setEvents((es) => [...es, ev]);
    setSelectedId(ev.id);
    setEditing(ev.id);
    if (scrollRef.current) scrollRef.current.scrollTop = Math.max(0, start * PX - 120);
    requestAnimationFrame(() => titleRef.current?.focus());
  };

  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
    const t = e.target as HTMLElement;
    if (t.tagName === "INPUT" || t.tagName === "TEXTAREA") return;
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    if (e.key === "Escape") {
      if (editing || selectedId) closeEditor();
      return;
    }
    if (selected && !selected.allDay) {
      const len = selected.end - selected.start;
      if (e.key === "ArrowUp" || e.key === "ArrowDown") {
        e.preventDefault();
        const d = e.key === "ArrowUp" ? -SNAP : SNAP;
        if (e.shiftKey) patch(selected.id, { end: Math.max(selected.start + SNAP, Math.min(1440, selected.end + d)) });
        else {
          const start = Math.max(0, Math.min(1440 - len, selected.start + d));
          patch(selected.id, { start, end: start + len });
        }
        return;
      }
      if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
        e.preventDefault();
        const date = addDays(selected.date, e.key === "ArrowLeft" ? -1 : 1);
        patch(selected.id, { date });
        if (!days.includes(date)) goto(date);
        return;
      }
      if (e.key === "Backspace" || e.key === "Delete") {
        e.preventDefault();
        remove(selected.id);
        return;
      }
      if (e.key === "Enter") {
        e.preventDefault();
        setEditing(selected.id);
        requestAnimationFrame(() => titleRef.current?.focus());
        return;
      }
    }
    const k = e.key.toLowerCase();
    if (e.key === "ArrowLeft" || k === "k") step(-1);
    else if (e.key === "ArrowRight" || k === "j") step(1);
    else if (k === "t") goto(today);
    else if (k === "d") setView("day");
    else if (k === "w") setView("week");
    else if (k === "n" || k === "c") {
      e.preventDefault();
      newEventNow();
    }
  };

  const panel = selected ? { ev: selected, cal: calOf(selected.calendar), dayIndex: days.indexOf(selected.date) } : null;

  const sidebar = (
    <div className="flex h-full flex-col gap-6 px-3 pt-3 pb-4">
      <Button className="w-full justify-start transition-[box-shadow,scale] duration-150 ease-out active:scale-[0.97]" onClick={newEventNow}>
        <PlusIcon />
        New event
        <Kbd className="ms-auto bg-primary-foreground/16 text-primary-foreground">N</Kbd>
      </Button>
      <MiniMonth
        busy={busy}
        month={month}
        onMonth={setMonth}
        onPick={(d) => {
          setFocusDay(d);
          if (mode === "day" || narrow) setView("day");
        }}
        selected={focusDay}
        today={today}
        weekStart={weekStart}
        wholeWeek={mode === "week"}
      />
      <div>
        <div className="mb-1.5 ps-1.5 font-medium text-muted-foreground text-xs">Calendars</div>
        <ul className="flex flex-col gap-0.5">
          {calendars.map((c) => {
            const on = c.visible !== false;
            return (
              <li key={c.id}>
                <button
                  aria-pressed={on}
                  className="flex h-8 w-full cursor-pointer items-center gap-2.5 rounded-lg px-1.5 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
                  onClick={() => setCalendars((cs) => cs.map((x) => (x.id === c.id ? { ...x, visible: !on } : x)))}
                  type="button"
                >
                  <span
                    className={cn(
                      "flex size-4 shrink-0 items-center justify-center rounded-[5px] border-2 transition-colors duration-150",
                      on ? toneCheck[c.tone] : "border-muted-foreground/40 bg-transparent",
                    )}
                  >
                    <CheckIcon className={cn("size-3 text-white transition-opacity duration-150", on ? "opacity-100" : "opacity-0")} strokeWidth={3} />
                  </span>
                  <span className={cn("truncate", !on && "text-muted-foreground")}>{c.name}</span>
                </button>
              </li>
            );
          })}
        </ul>
      </div>
      <div className="mt-auto hidden flex-col gap-1.5 text-muted-foreground text-xs @4xl:flex">
        <div className="flex items-center gap-1.5">
          <Kbd>←</Kbd>
          <Kbd>→</Kbd>
          <span>week</span>
          <Kbd className="ms-2">T</Kbd>
          <span>today</span>
        </div>
        <div className="flex items-center gap-1.5">
          <Kbd>↑</Kbd>
          <Kbd>↓</Kbd>
          <span>move event</span>
          <Kbd className="ms-2">⇧</Kbd>
          <span>resize</span>
        </div>
      </div>
    </div>
  );

  const nowTop = nowMin * PX;
  const todayIdx = days.indexOf(today);

  return (
    <MotionConfig reducedMotion="user">
      <div
        className={cn(
          "@container relative isolate flex h-[760px] w-full select-none overflow-hidden bg-sidebar text-foreground text-sm outline-none",
          className,
        )}
        onKeyDown={onKeyDown}
        ref={rootRef}
        tabIndex={-1}
      >
        <aside aria-label="Calendars" className="hidden w-64 shrink-0 overflow-y-auto @4xl:block">
          {sidebar}
        </aside>

        <main
          className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-background @4xl:my-2 @4xl:me-2 @4xl:rounded-2xl @4xl:border @4xl:shadow-xs/5"
          ref={mainRef}
        >
          <header className="flex h-14 shrink-0 items-center gap-2 border-b px-3 @lg:px-4">
            {docked ? (
              <h2 className="flex h-9 items-center gap-1.5">
                <span className="font-heading font-semibold text-lg tracking-tight">{head.main}</span>
                <span className="text-lg text-muted-foreground tabular-nums">{head.sub}</span>
              </h2>
            ) : (
            <Popover>
              <PopoverTrigger className="-ms-1.5 flex h-9 cursor-pointer items-center gap-1.5 rounded-lg px-1.5 outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring data-popup-open:bg-accent">
                <span className="font-heading font-semibold text-lg tracking-tight">{head.main}</span>
                <span className="text-lg text-muted-foreground tabular-nums">{head.sub}</span>
                <ChevronDownIcon className="size-4 text-muted-foreground" />
              </PopoverTrigger>
              <PopoverPopup align="start" className="w-72">
                <div className="p-1">
                  <MiniMonth busy={busy} month={month} onMonth={setMonth} onPick={(d) => setFocusDay(d)} selected={focusDay} today={today} weekStart={weekStart} wholeWeek={mode === "week"} />
                  <div className="mt-3 flex flex-wrap gap-1.5 border-t pt-3">
                    {calendars.map((c) => {
                      const on = c.visible !== false;
                      return (
                        <button
                          aria-pressed={on}
                          className={cn(
                            "flex h-7 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 text-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
                            on ? "bg-accent" : "text-muted-foreground",
                          )}
                          key={c.id}
                          onClick={() => setCalendars((cs) => cs.map((x) => (x.id === c.id ? { ...x, visible: !on } : x)))}
                          type="button"
                        >
                          <span className={cn("size-2 rounded-full", on ? toneDot[c.tone] : "bg-muted-foreground/40")} />
                          {c.name.split(" ")[0]}
                        </button>
                      );
                    })}
                  </div>
                </div>
              </PopoverPopup>
            </Popover>
            )}
            <div className="ms-auto flex items-center gap-1.5">
              <Button onClick={() => goto(today)} size="sm" variant="outline">
                Today
              </Button>
              <div className="flex">
                <Button aria-label={mode === "week" ? "Previous week" : "Previous day"} onClick={() => step(-1)} size="icon-sm" variant="ghost">
                  <ChevronLeftIcon />
                </Button>
                <Button aria-label={mode === "week" ? "Next week" : "Next day"} onClick={() => step(1)} size="icon-sm" variant="ghost">
                  <ChevronRightIcon />
                </Button>
              </div>
              <div aria-label="View" className="hidden rounded-lg bg-muted p-0.5 @2xl:flex" role="group">
                {(["day", "week"] as const).map((v) => (
                  <button
                    aria-pressed={view === v}
                    className={cn(
                      "relative h-7 cursor-pointer rounded-md px-2.5 font-medium text-xs capitalize outline-none focus-visible:ring-2 focus-visible:ring-ring",
                      view === v ? "text-foreground" : "text-muted-foreground hover:text-foreground",
                    )}
                    key={v}
                    onClick={() => setView(v)}
                    type="button"
                  >
                    {view === v && (
                      <motion.span
                        className="absolute inset-0 rounded-md bg-background shadow-xs/5 dark:bg-accent"
                        layoutId="cal-view"
                        transition={{ type: "spring", stiffness: 520, damping: 42 }}
                      />
                    )}
                    <span className="relative flex items-center gap-1.5">
                      {v}
                      <span className="text-[10px] text-muted-foreground uppercase">{v[0]}</span>
                    </span>
                  </button>
                ))}
              </div>
              <Button aria-label="New event" className="@4xl:hidden" onClick={newEventNow} size="icon-sm">
                <PlusIcon />
              </Button>
            </div>
          </header>

          {narrow && (
            <div className="grid shrink-0 grid-cols-7 gap-1 border-b px-2 py-2">
              {Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)).map((d) => {
                const on = d === focusDay;
                const isToday = d === today;
                return (
                  <button
                    aria-label={`${DOW[weekday(d)]} ${parse(d).getUTCDate()}`}
                    aria-pressed={on}
                    className="relative flex h-12 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"
                    key={d}
                    onClick={() => setFocusDay(d)}
                    type="button"
                  >
                    {on && (
                      <motion.span
                        className="absolute inset-0 rounded-xl bg-accent"
                        layoutId="cal-strip"
                        transition={{ type: "spring", stiffness: 520, damping: 42 }}
                      />
                    )}
                    <span className={cn("relative text-[11px]", isToday ? "font-medium text-destructive-foreground" : "text-muted-foreground")}>
                      {DOW[weekday(d)].slice(0, 2)}
                    </span>
                    <span className={cn("relative font-medium tabular-nums", isToday && "text-destructive-foreground")}>{parse(d).getUTCDate()}</span>
                    {busy.has(d) && <span className="absolute bottom-1 size-1 rounded-full bg-muted-foreground/60" />}
                  </button>
                );
              })}
            </div>
          )}

          <div className="flex shrink-0 overflow-y-hidden border-b [scrollbar-gutter:stable]">
            <div className="flex w-14 shrink-0 flex-col items-end pe-2 text-[10px] text-muted-foreground">
              {!narrow && (
                <span className="flex h-11 flex-col items-end justify-center leading-tight">
                  <span className="font-medium text-foreground/80 tabular-nums">W{isoWeek(days[0])}</span>
                  <span>CEST</span>
                </span>
              )}
              <span className="flex h-8 items-center">All day</span>
            </div>
            <div className="min-w-0 flex-1">
              {!narrow && (
                <div className="grid" style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}>
                  {days.map((d) => {
                    const isToday = d === today;
                    return (
                      <button
                        className="flex h-11 cursor-pointer items-center gap-1.5 border-s ps-2.5 text-left outline-none hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
                        key={d}
                        onClick={() => {
                          setFocusDay(d);
                          setView("day");
                        }}
                        type="button"
                      >
                        <span className={cn("text-xs", isToday ? "font-medium text-destructive-foreground" : "text-muted-foreground")}>{DOW[weekday(d)]}</span>
                        <span
                          className={cn(
                            "flex h-6 min-w-6 items-center justify-center rounded-full px-1 font-medium tabular-nums",
                            isToday && "bg-destructive text-white",
                          )}
                        >
                          {parse(d).getUTCDate()}
                        </span>
                      </button>
                    );
                  })}
                </div>
              )}
              <div className="relative grid min-h-8 py-1" style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}>
                {days.map((d) => (
                  <div className="border-s" key={d} style={{ gridColumn: "span 1", gridRow: "1 / span 3" }} />
                ))}
                {packAllDay(allDay, days[0], days.length).map(({ e, row, startIdx, endIdx }) => {
                  const cal = calOf(e.calendar);
                  return (
                    <button
                      className={cn(
                        "relative z-10 mx-1 my-px flex h-6 cursor-pointer items-center truncate rounded-md ps-2.5 pe-2 text-left font-medium text-xs outline-none before:absolute before:inset-y-1 before:left-1 before:w-0.5 before:rounded-full focus-visible:ring-2 focus-visible:ring-ring",
                        selectedId === e.id ? toneSolid[cal.tone] : toneBlock[cal.tone],
                      )}
                      key={e.id}
                      onClick={(evt) => {
                        const r = evt.currentTarget.getBoundingClientRect();
                        const m = mainRef.current?.getBoundingClientRect();
                        if (m) setAllDayAt({ left: Math.max(12, Math.min(r.left - m.left, m.width - 268)), top: r.bottom - m.top + 6 });
                        setSelectedId(e.id);
                        setEditing(null);
                      }}
                      style={{ gridColumn: `${startIdx + 1} / ${endIdx + 2}`, gridRow: row + 1 }}
                      type="button"
                    >
                      {e.title}
                    </button>
                  );
                })}
              </div>
            </div>
          </div>

          <div className="relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden [scrollbar-width:thin]" ref={scrollRef}>
            <div className="relative flex" style={{ height: 24 * HOUR }}>
              <div aria-hidden className="relative w-14 shrink-0">
                {Array.from({ length: 23 }, (_, i) => i + 1).map((h) => (
                  <span
                    className={cn(
                      "absolute right-2 -translate-y-1/2 text-[10px] text-muted-foreground tabular-nums transition-opacity",
                      todayIdx >= 0 && Math.abs(h * 60 - nowMin) < 24 && "opacity-0",
                    )}
                    key={h}
                    style={{ top: h * HOUR }}
                  >
                    {fmtTime(h * 60)}
                  </span>
                ))}
                {todayIdx >= 0 && (
                  <span
                    className="absolute right-1 z-20 -translate-y-1/2 rounded-md bg-destructive px-1 py-px font-medium text-[10px] text-white tabular-nums"
                    style={{ top: nowTop }}
                  >
                    {fmtTime(nowMin).replace(" AM", "").replace(" PM", "")}
                  </span>
                )}
              </div>

              <div
                className="relative grid min-w-0 flex-1"
                onPointerCancel={onPointerUp}
                onPointerMove={onPointerMove}
                onPointerUp={onPointerUp}
                ref={gridRef}
                style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}
              >
                <div aria-hidden className="pointer-events-none absolute inset-0">
                  {Array.from({ length: 24 }, (_, h) => (
                    <div className="absolute inset-x-0 border-t border-border/70" key={h} style={{ top: h * HOUR }} />
                  ))}
                  {Array.from({ length: 24 }, (_, h) => (
                    <div className="absolute inset-x-0 border-t border-border/30 border-dashed" key={`h${h}`} style={{ top: h * HOUR + HOUR / 2 }} />
                  ))}
                  {todayIdx >= 0 && nowMin <= 24 * 60 && <div className="absolute inset-x-0 h-px -translate-y-1/2 bg-destructive/25" style={{ top: nowTop }} />}
                </div>

                {days.map((d) => {
                  const placed = placeDay(timed.filter((e) => e.date === d));
                  const isToday = d === today;
                  return (
                    <div
                      aria-label={`${DOW[weekday(d)]} ${parse(d).getUTCDate()}, drag to create an event`}
                      className={cn("relative border-s", weekday(d) >= 5 && "bg-muted/40")}
                      key={d}
                      onPointerDown={(e) => beginCreate(e, d)}
                      role="group"
                    >
                      {isToday && nowMin <= 24 * 60 && (
                        <div aria-hidden className="pointer-events-none absolute inset-x-0 z-20" style={{ top: nowTop }}>
                          <div className="absolute inset-x-0 h-0.5 -translate-y-1/2 bg-destructive" />
                          <div className="absolute -left-1.5 size-3 -translate-y-1/2 rounded-full bg-destructive ring-2 ring-background" />
                        </div>
                      )}
                      <AnimatePresence initial={false}>
                        {placed.map(({ ev, col, cols }) => {
                          const cal = calOf(ev.calendar);
                          const isSel = selectedId === ev.id;
                          const isDrag = drag?.id === ev.id && (drag.moved || drag.mode === "create");
                          const height = Math.max((ev.end - ev.start) * PX, 18);
                          const short = height < 40;
                          const past = d < today || (d === today && ev.end <= nowMin);
                          return (
                            <motion.div
                              animate={{ opacity: 1, scale: 1, y: ev.start * PX }}
                              aria-label={`${ev.title || "New event"}, ${fmtRange(ev.start, ev.end)}`}
                              className={cn(
                                "group/ev @container absolute top-0 z-10 cursor-grab touch-none overflow-hidden rounded-md bg-clip-padding text-left outline-none before:absolute before:inset-y-1 before:left-1 before:w-0.5 before:rounded-full focus-visible:ring-2 focus-visible:ring-ring",
                                isSel || isDrag ? toneSolid[cal.tone] : toneBlock[cal.tone],
                                isDrag && "z-30 cursor-grabbing shadow-lg/20",
                                past && !isSel && !isDrag && "[&>div]:opacity-65 before:opacity-60",
                                cols > 1 && "ring-1 ring-background",
                              )}
                              exit={{ opacity: 0, scale: 0.96 }}
                              initial={{ opacity: 0, scale: 0.96, y: ev.start * PX }}
                              key={ev.id}
                              onDoubleClick={() => {
                                setSelectedId(ev.id);
                                setEditing(ev.id);
                                requestAnimationFrame(() => titleRef.current?.focus());
                              }}
                              onKeyDown={(e) => {
                                if (e.key === " ") {
                                  e.preventDefault();
                                  setSelectedId(ev.id);
                                }
                              }}
                              onPointerDown={(e) => beginDrag(e, ev, "move")}
                              role="button"
                              style={{
                                height,
                                left: `calc(${(col / cols) * 100}% + 2px)`,
                                width: `calc(${(col === cols - 1 ? 1 / cols : Math.min(1 - col / cols, (mode === "week" ? 1.7 : 1) / cols)) * 100}% - 6px)`,
                                zIndex: isDrag ? 30 : 10 + col,
                              }}
                              tabIndex={0}
                              onFocus={() => setSelectedId(ev.id)}
                              transition={isDrag ? { duration: 0 } : { y: settle, opacity: { duration: 0.15 }, scale: { duration: 0.15 } }}
                            >
                              <div className={cn("flex h-full flex-col ps-3 pe-1.5", short ? "flex-row items-center gap-1.5 py-0" : "py-1")}>
                                <span className={cn("truncate font-medium text-xs leading-4", !ev.title && "italic opacity-70")}>
                                  {ev.title || "New event"}
                                </span>
                                <span className={cn("truncate text-[11px] tabular-nums leading-4 opacity-75", short && "hidden shrink-0 @[8rem]:inline")}>
                                  {short ? fmtTime(ev.start) : fmtRange(ev.start, ev.end)}
                                </span>
                                {!short && height > 64 && ev.location && (
                                  <span className="mt-0.5 flex items-center gap-1 truncate text-[11px] leading-4 opacity-75">
                                    <MapPinIcon className="size-3 shrink-0" />
                                    {ev.location}
                                  </span>
                                )}
                              </div>
                              {isDrag && height >= 40 && (
                                <span className="pointer-events-none absolute right-1.5 bottom-1.5 rounded bg-black/20 px-1 font-medium text-[10px] tabular-nums">
                                  {fmtDuration(ev.end - ev.start)}
                                </span>
                              )}
                              <span
                                aria-hidden
                                className="absolute inset-x-0 bottom-0 h-2.5 cursor-ns-resize"
                                onPointerDown={(e) => beginDrag(e, ev, "resize")}
                              >
                                <span className="absolute bottom-0.5 left-1/2 h-0.5 w-5 -translate-x-1/2 rounded-full bg-current opacity-0 transition-opacity group-hover/ev:opacity-40" />
                              </span>
                            </motion.div>
                          );
                        })}
                      </AnimatePresence>
                    </div>
                  );
                })}

                <AnimatePresence>
                  {panel && !drag && panel.dayIndex >= 0 && !panel.ev.allDay && (
                    <EventCard
                      calendars={calendars}
                      cols={days.length}
                      dayIndex={panel.dayIndex}
                      editing={editing === panel.ev.id}
                      ev={panel.ev}
                      key={panel.ev.id}
                      onCalendar={(id) => patch(panel.ev.id, { calendar: id })}
                      onClose={closeEditor}
                      onDelete={() => remove(panel.ev.id)}
                      onEdit={() => {
                        setEditing(panel.ev.id);
                        requestAnimationFrame(() => titleRef.current?.focus());
                      }}
                      onSave={() => {
                        if (!panel.ev.title.trim()) patch(panel.ev.id, { title: "Busy" });
                        setEditing(null);
                        setToast({ key: Date.now(), text: `Saved for ${DOW[weekday(panel.ev.date)]} ${fmtRange(panel.ev.start, panel.ev.end)}` });
                        rootRef.current?.focus({ preventScroll: true });
                      }}
                      onTitle={(title) => patch(panel.ev.id, { title })}
                      scrollRef={scrollRef}
                      titleRef={titleRef}
                    />
                  )}
                </AnimatePresence>
              </div>
            </div>
          </div>

          <AnimatePresence>
            {selected?.allDay && (
              <motion.div
                animate={{ opacity: 1, scale: 1 }}
                className="absolute z-40 w-64 origin-top-left rounded-xl border bg-popover p-3 shadow-lg/10"
                exit={{ opacity: 0, scale: 0.97 }}
                initial={{ opacity: 0, scale: 0.97 }}
                style={allDayAt}
                transition={{ duration: 0.16, ease }}
              >
                <div className="flex items-start gap-2">
                  <span className={cn("mt-1.5 size-2 shrink-0 rounded-full", toneDot[calOf(selected.calendar).tone])} />
                  <div className="min-w-0 flex-1">
                    <div className="font-medium">{selected.title}</div>
                    <div className="text-muted-foreground text-xs">
                      All day{selected.days && selected.days > 1 ? ` · ${selected.days} days` : ""} · {calOf(selected.calendar).name}
                    </div>
                  </div>
                  <Button aria-label="Close" onClick={closeEditor} size="icon-xs" variant="ghost">
                    <XIcon />
                  </Button>
                </div>
              </motion.div>
            )}
          </AnimatePresence>

          <div aria-live="polite" className="pointer-events-none absolute inset-x-0 bottom-4 z-40 flex justify-center px-4 @2xl:justify-start @2xl:ps-[4.5rem]">
            <AnimatePresence mode="popLayout">
              {toast && (
                <motion.div
                  animate={{ opacity: 1, y: 0, scale: 1 }}
                  className="pointer-events-auto flex h-10 max-w-full items-center gap-3 rounded-full border bg-popover ps-4 pe-1.5 shadow-lg/10"
                  exit={{ opacity: 0, y: 6, scale: 0.98 }}
                  initial={{ opacity: 0, y: 8, scale: 0.98 }}
                  key={toast.key}
                  transition={{ duration: 0.2, ease }}
                >
                  <span className="truncate text-sm tabular-nums">{toast.text}</span>
                  {toast.undo ? (
                    <Button className="rounded-full" onClick={toast.undo} size="xs" variant="secondary">
                      Undo
                    </Button>
                  ) : (
                    <span className="w-1" />
                  )}
                </motion.div>
              )}
            </AnimatePresence>
          </div>
        </main>
      </div>
    </MotionConfig>
  );
}

function EventCard({
  ev,
  calendars,
  dayIndex,
  cols,
  editing,
  titleRef,
  scrollRef,
  onTitle,
  onCalendar,
  onSave,
  onClose,
  onDelete,
  onEdit,
}: {
  ev: CalendarEvent;
  calendars: CalendarSource[];
  dayIndex: number;
  cols: number;
  editing: boolean;
  titleRef: RefObject<HTMLInputElement | null>;
  scrollRef: RefObject<HTMLDivElement | null>;
  onTitle: (t: string) => void;
  onCalendar: (id: string) => void;
  onSave: () => void;
  onClose: () => void;
  onDelete: () => void;
  onEdit: () => void;
}) {
  const cal = calendars.find((c) => c.id === ev.calendar) ?? calendars[0];
  const cardRef = useRef<HTMLDivElement>(null);
  useLayoutEffect(() => {
    const card = cardRef.current;
    const box = scrollRef.current;
    if (!card || !box) return;
    const top = card.offsetTop;
    const bottom = top + card.offsetHeight;
    if (bottom > box.scrollTop + box.clientHeight - 12) box.scrollTo({ top: bottom - box.clientHeight + 12, behavior: "smooth" });
    else if (top < box.scrollTop + 8) box.scrollTo({ top: top - 8, behavior: "smooth" });
  }, [scrollRef, editing]);
  const toRight = cols === 1 ? false : dayIndex < cols - 2;
  const single = cols === 1;
  const top = Math.max(4, ev.start * PX - 8);
  const style = single
    ? { top: ev.end * PX + 8, right: 12, width: "min(22rem, calc(100% - 24px))" }
    : toRight
      ? { top, left: `calc(${((dayIndex + 1) / cols) * 100}% + 6px)` }
      : { top, right: `calc(${((cols - dayIndex) / cols) * 100}% + 6px)` };
  return (
    <motion.div
      animate={{ opacity: 1, scale: 1 }}
      className={cn(
        "absolute z-40 w-72 cursor-default select-text rounded-xl border bg-popover text-popover-foreground shadow-lg/10",
        single ? "origin-top" : toRight ? "origin-top-left" : "origin-top-right",
      )}
      exit={{ opacity: 0, scale: 0.97 }}
      initial={{ opacity: 0, scale: 0.97 }}
      onPointerDown={(e) => e.stopPropagation()}
      ref={cardRef}
      role="dialog"
      aria-label={editing ? "Edit event" : ev.title}
      style={style}
      transition={{ duration: 0.16, ease }}
    >
      <div className="flex items-start gap-2 p-3 pb-2">
        <span className={cn("mt-1.5 size-2.5 shrink-0 rounded-full", toneDot[cal.tone])} />
        {editing ? (
          <input
            aria-label="Event title"
            className="min-w-0 flex-1 bg-transparent font-medium text-sm outline-none placeholder:text-muted-foreground/70"
            onChange={(e) => onTitle(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter") onSave();
              if (e.key === "Escape") onClose();
            }}
            placeholder="Add a title"
            ref={titleRef}
            value={ev.title}
          />
        ) : (
          <button className="min-w-0 flex-1 cursor-text text-left font-medium leading-5 outline-none" onClick={onEdit} type="button">
            {ev.title}
          </button>
        )}
        <Button aria-label="Close" className="-me-1 -mt-0.5" onClick={onClose} size="icon-xs" variant="ghost">
          <XIcon />
        </Button>
      </div>
      <div className="flex flex-col gap-1.5 px-3 pb-3 text-muted-foreground text-xs">
        <div className="flex items-center gap-2">
          <ClockIcon className="size-3.5 shrink-0" />
          <span className="tabular-nums">
            {DOW[weekday(ev.date)]} {MONTHS[parse(ev.date).getUTCMonth()].slice(0, 3)} {parse(ev.date).getUTCDate()} · {fmtRange(ev.start, ev.end)}
          </span>
          <span className="ms-auto tabular-nums">{fmtDuration(ev.end - ev.start)}</span>
        </div>
        {ev.location && (
          <div className="flex items-center gap-2">
            <MapPinIcon className="size-3.5 shrink-0" />
            {ev.location}
          </div>
        )}
        {ev.video && (
          <div className="flex items-center gap-2">
            <VideoIcon className="size-3.5 shrink-0" />
            <span className="text-info-foreground">meet.kestrel.dev/{ev.id}</span>
          </div>
        )}
        {ev.people?.length ? (
          <div className="flex items-start gap-2">
            <UsersIcon className="mt-px size-3.5 shrink-0" />
            <span className="text-pretty">{ev.people.join(", ")}</span>
          </div>
        ) : null}
        <div className="flex items-center gap-2">
          <CalendarDaysIcon className="size-3.5 shrink-0" />
          {editing ? (
            <div className="flex flex-wrap gap-1">
              {calendars
                .filter((c) => c.id !== "holidays")
                .map((c) => (
                  <button
                    aria-pressed={c.id === ev.calendar}
                    className={cn(
                      "flex h-6 cursor-pointer items-center gap-1 rounded-full border px-2 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
                      c.id === ev.calendar ? "border-foreground/20 bg-accent text-foreground" : "hover:text-foreground",
                    )}
                    key={c.id}
                    onClick={() => onCalendar(c.id)}
                    type="button"
                  >
                    <span className={cn("size-1.5 rounded-full", toneDot[c.tone])} />
                    {c.name.split(" ")[0]}
                  </button>
                ))}
            </div>
          ) : (
            cal.name
          )}
        </div>
      </div>
      <div className="flex items-center gap-1.5 border-t px-2 py-2">
        {editing ? (
          <>
            <Button className="transition-[box-shadow,scale] duration-150 ease-out active:scale-[0.97]" onClick={onSave} size="xs">
              Save
            </Button>
            <span className="text-muted-foreground text-xs">
              <Kbd className="h-4 text-[10px]">↵</Kbd>
            </span>
          </>
        ) : (
          <span className="flex items-center gap-1 ps-1 text-muted-foreground text-xs">
            <Kbd className="h-4 text-[10px]">↑↓</Kbd> move
            <Kbd className="ms-1 h-4 text-[10px]">⇧↑↓</Kbd> resize
          </span>
        )}
        <Button aria-label="Delete event" className="ms-auto" onClick={onDelete} size="icon-xs" variant="ghost">
          <Trash2Icon />
        </Button>
      </div>
    </motion.div>
  );
}
```
