Calendar Agenda
Original · freeagenda list of upcoming days
byWeb InnoventixReact + Tailwind
calendaragendacalendars
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/calendar-agenda.jsoncalendar-agenda.tsx
"use client";
import { useId, useMemo, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type CategoryKey = "meeting" | "focus" | "personal" | "deadline" | "health";
type IconProps = { className?: string };
interface AgendaEvent {
id: string;
title: string;
start: string;
end?: string;
category: CategoryKey;
location: string;
detail: string;
people: string[];
}
interface DayGroup {
id: string;
weekdayShort: string;
weekdayLong: string;
dayNum: string;
monthShort: string;
relative?: string;
isToday?: boolean;
events: AgendaEvent[];
}
const CATEGORIES: Record<
CategoryKey,
{ label: string; dot: string; spine: string; chip: string; chipOn: string }
> = {
meeting: {
label: "Meetings",
dot: "bg-indigo-500",
spine: "bg-indigo-400 dark:bg-indigo-500",
chip: "bg-indigo-50 text-indigo-700 ring-indigo-600/15 dark:bg-indigo-400/10 dark:text-indigo-300 dark:ring-indigo-400/25",
chipOn: "bg-indigo-600 text-white ring-indigo-600 dark:bg-indigo-500 dark:ring-indigo-500",
},
focus: {
label: "Focus",
dot: "bg-violet-500",
spine: "bg-violet-400 dark:bg-violet-500",
chip: "bg-violet-50 text-violet-700 ring-violet-600/15 dark:bg-violet-400/10 dark:text-violet-300 dark:ring-violet-400/25",
chipOn: "bg-violet-600 text-white ring-violet-600 dark:bg-violet-500 dark:ring-violet-500",
},
personal: {
label: "Personal",
dot: "bg-emerald-500",
spine: "bg-emerald-400 dark:bg-emerald-500",
chip: "bg-emerald-50 text-emerald-700 ring-emerald-600/15 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/25",
chipOn: "bg-emerald-600 text-white ring-emerald-600 dark:bg-emerald-500 dark:ring-emerald-500",
},
deadline: {
label: "Deadlines",
dot: "bg-rose-500",
spine: "bg-rose-400 dark:bg-rose-500",
chip: "bg-rose-50 text-rose-700 ring-rose-600/15 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/25",
chipOn: "bg-rose-600 text-white ring-rose-600 dark:bg-rose-500 dark:ring-rose-500",
},
health: {
label: "Health",
dot: "bg-sky-500",
spine: "bg-sky-400 dark:bg-sky-500",
chip: "bg-sky-50 text-sky-700 ring-sky-600/15 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-400/25",
chipOn: "bg-sky-600 text-white ring-sky-600 dark:bg-sky-500 dark:ring-sky-500",
},
};
const CATEGORY_ORDER: CategoryKey[] = ["meeting", "focus", "personal", "deadline", "health"];
const AVATAR_TONES = [
"bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
"bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
"bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
"bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
"bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300",
];
const DAYS: DayGroup[] = [
{
id: "d-0716",
weekdayShort: "Thu",
weekdayLong: "Thursday",
dayNum: "16",
monthShort: "Jul",
relative: "Today",
isToday: true,
events: [
{
id: "e1",
title: "Platform standup",
start: "8:30 AM",
end: "9:00 AM",
category: "meeting",
location: "Zoom · #platform",
detail:
"Quick sync on the migration blockers. Bring the failing CI logs from last night and the rollback plan for the auth service.",
people: ["Marcus Vidal", "Lena Cho", "Dana Okafor"],
},
{
id: "e2",
title: "Deep work — Q3 pricing model",
start: "9:30 AM",
end: "11:00 AM",
category: "focus",
location: "Do not disturb",
detail:
"Rebuild the tiered pricing sheet with the new seat-based assumptions and sanity-check margins against the 2025 cohort.",
people: [],
},
{
id: "e3",
title: "Lunch with Priya Nair",
start: "12:30 PM",
end: "1:15 PM",
category: "personal",
location: "Café Lomi, 4th St",
detail:
"Catching up before she heads to Lisbon. She's offered to intro us to two design leads she rates.",
people: ["Priya Nair"],
},
{
id: "e4",
title: "Design review — onboarding flow",
start: "2:00 PM",
end: "2:45 PM",
category: "meeting",
location: "Room 4B",
detail:
"Walk through the 3-step onboarding redesign and the empty-state illustrations. Decision needed on the progress indicator.",
people: ["Marcus Vidal", "Lena Cho"],
},
{
id: "e5",
title: "Send investor update draft",
start: "5:00 PM",
category: "deadline",
location: "Notion → Board doc",
detail:
"Final July update: ARR, runway, the two new hires, and the enterprise pilot with Northwind Labs.",
people: [],
},
],
},
{
id: "d-0717",
weekdayShort: "Fri",
weekdayLong: "Friday",
dayNum: "17",
monthShort: "Jul",
relative: "Tomorrow",
events: [
{
id: "e6",
title: "Morning run — riverside loop",
start: "7:00 AM",
end: "7:45 AM",
category: "health",
location: "Willow Creek trail",
detail: "Easy 6 km — third run this week. Keep the pace under 6:00 / km and don't chase it.",
people: [],
},
{
id: "e7",
title: "Customer call — Northwind Labs",
start: "10:00 AM",
end: "11:30 AM",
category: "meeting",
location: "Google Meet",
detail:
"Pilot kickoff. Confirm the data-residency requirements and lock the timeline for their security questionnaire.",
people: ["Aisha Rahman", "Tom Beckett"],
},
{
id: "e8",
title: "Focus — finalize API docs",
start: "1:00 PM",
end: "3:00 PM",
category: "focus",
location: "Do not disturb",
detail: "Fill the gaps in the webhooks reference and add the rate-limit examples reviewers asked for.",
people: [],
},
{
id: "e9",
title: "Ship v2.4 release notes",
start: "4:30 PM",
category: "deadline",
location: "GitHub → Releases",
detail: "Publish notes for v2.4: the agenda widget, dark-mode fixes, and the CSV export.",
people: [],
},
],
},
{
id: "d-0718",
weekdayShort: "Sat",
weekdayLong: "Saturday",
dayNum: "18",
monthShort: "Jul",
events: [
{
id: "e10",
title: "Farmers market run",
start: "9:00 AM",
end: "10:00 AM",
category: "personal",
location: "Grand Ave market",
detail: "Peaches, sourdough, and flowers for the table. Cash only at the cheese stall.",
people: [],
},
{
id: "e11",
title: "Vinyasa yoga",
start: "11:00 AM",
end: "12:00 PM",
category: "health",
location: "Still Point Studio",
detail: "Bring the good mat — last week's rental was falling apart at the seams.",
people: [],
},
],
},
{
id: "d-0719",
weekdayShort: "Sun",
weekdayLong: "Sunday",
dayNum: "19",
monthShort: "Jul",
events: [
{
id: "e12",
title: "Dinner with the Alvarez family",
start: "6:00 PM",
end: "8:00 PM",
category: "personal",
location: "Their place, Maple Rd",
detail:
"Bringing dessert. Sofia's back from her semester in Kyoto — ask her about the ceramics course.",
people: ["Sofia Alvarez", "Ramón Alvarez"],
},
],
},
{
id: "d-0720",
weekdayShort: "Mon",
weekdayLong: "Monday",
dayNum: "20",
monthShort: "Jul",
events: [
{
id: "e13",
title: "Weekly planning",
start: "9:00 AM",
end: "9:30 AM",
category: "focus",
location: "Do not disturb",
detail: "Set the three priorities for the week and clear the inbox to zero before standup.",
people: [],
},
{
id: "e14",
title: "1:1 with Dana Okafor",
start: "11:00 AM",
end: "12:00 PM",
category: "meeting",
location: "Room 2A",
detail: "Career check-in. She wants more backend ownership — line up the payments refactor.",
people: ["Dana Okafor"],
},
{
id: "e15",
title: "Tax documents to accountant",
start: "3:00 PM",
category: "deadline",
location: "Email → Reyes & Co.",
detail: "Send the Q2 receipts and the mileage log. Reyes needs everything before the 25th.",
people: [],
},
],
},
{
id: "d-0721",
weekdayShort: "Tue",
weekdayLong: "Tuesday",
dayNum: "21",
monthShort: "Jul",
events: [
{
id: "e16",
title: "Dentist — cleaning",
start: "8:00 AM",
end: "8:30 AM",
category: "health",
location: "Bright Smile Dental",
detail: "Routine cleaning. Mention the cold sensitivity on the lower left this time.",
people: [],
},
{
id: "e17",
title: "Roadmap sync",
start: "10:00 AM",
end: "11:00 AM",
category: "meeting",
location: "Room 4B",
detail: "Align eng and design on Q3. Cut scope or move the reporting module to Q4 — pick one.",
people: ["Marcus Vidal", "Aisha Rahman"],
},
{
id: "e18",
title: "Focus — prototype agenda widget",
start: "2:00 PM",
end: "4:00 PM",
category: "focus",
location: "Do not disturb",
detail:
"Build the interactive agenda list — expandable events, category filters, and the completed state.",
people: [],
},
],
},
{
id: "d-0722",
weekdayShort: "Wed",
weekdayLong: "Wednesday",
dayNum: "22",
monthShort: "Jul",
events: [
{
id: "e19",
title: "All-hands",
start: "9:30 AM",
end: "10:30 AM",
category: "meeting",
location: "Main hall + stream",
detail: "Q2 results, the new hires, and the roadmap preview. Our demo slot is 4 minutes — keep it tight.",
people: [],
},
{
id: "e20",
title: "Conference talk proposal",
start: "1:00 PM",
category: "deadline",
location: "Sessionize",
detail: "Submit the 'Designing calm software' talk — 300-word abstract plus a two-line bio.",
people: [],
},
],
},
];
const TOTAL_EVENTS = DAYS.reduce((sum, day) => sum + day.events.length, 0);
function initials(name: string): string {
return name
.split(" ")
.map((part) => part.charAt(0))
.join("")
.slice(0, 2)
.toUpperCase();
}
function IconCalendar({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<rect x="3" y="4.5" width="18" height="16" rx="2.5" />
<path d="M3 9h18M8 3v3M16 3v3" />
<path d="M7.5 13h3v3h-3z" fill="currentColor" stroke="none" />
</svg>
);
}
function IconChevron({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function IconCheck({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m5 12.5 4 4 10-10" />
</svg>
);
}
function IconPin({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M12 21s-6.5-5.2-6.5-10a6.5 6.5 0 0 1 13 0c0 4.8-6.5 10-6.5 10Z" />
<circle cx="12" cy="11" r="2.2" />
</svg>
);
}
function IconUsers({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="9" cy="8" r="3" />
<path d="M3.5 20a5.5 5.5 0 0 1 11 0M16 5.2a3 3 0 0 1 0 5.6M17 20a5.5 5.5 0 0 0-2-4.3" />
</svg>
);
}
function IconBell({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z" />
<path d="M10 19a2 2 0 0 0 4 0" />
</svg>
);
}
function IconFlag({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M5 21V4M5 4h11l-2 3.5L16 11H5" />
</svg>
);
}
export default function CalendarAgenda() {
const uid = useId();
const reduce = useReducedMotion();
const [activeCats, setActiveCats] = useState<Set<CategoryKey>>(() => new Set());
const [completed, setCompleted] = useState<Set<string>>(() => new Set(["e1"]));
const [reminders, setReminders] = useState<Set<string>>(() => new Set());
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(["e2"]));
const [hideCompleted, setHideCompleted] = useState(false);
const toggleCat = (key: CategoryKey) =>
setActiveCats((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
const toggleSet =
(setter: React.Dispatch<React.SetStateAction<Set<string>>>) => (id: string) =>
setter((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
const toggleDone = toggleSet(setCompleted);
const toggleReminder = toggleSet(setReminders);
const toggleExpand = toggleSet(setExpanded);
const filteredDays = useMemo(() => {
return DAYS.map((day) => ({
...day,
events: day.events.filter(
(ev) =>
(activeCats.size === 0 || activeCats.has(ev.category)) &&
!(hideCompleted && completed.has(ev.id)),
),
})).filter((day) => day.events.length > 0);
}, [activeCats, hideCompleted, completed]);
const visibleCount = filteredDays.reduce((sum, day) => sum + day.events.length, 0);
const isFiltered = activeCats.size > 0 || hideCompleted;
const headingId = `${uid}-heading`;
return (
<section
aria-labelledby={headingId}
className="relative w-full overflow-hidden bg-gradient-to-b from-white to-slate-50 px-4 py-14 text-neutral-900 dark:from-neutral-950 dark:to-neutral-900 dark:text-neutral-100 sm:py-20"
>
<style>{`
@keyframes calag-rise {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes calag-ping {
0% { transform: scale(1); opacity: 0.55; }
70% { transform: scale(2.4); opacity: 0; }
100% { transform: scale(2.4); opacity: 0; }
}
.calag-rise { animation: calag-rise 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; }
.calag-ping { animation: calag-ping 2.4s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
.calag-rise, .calag-ping { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-64 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-500/10"
/>
<div className="relative mx-auto max-w-2xl">
{/* Header */}
<header className="mb-7">
<div className="flex items-start gap-4">
<span className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/25">
<IconCalendar className="h-6 w-6" />
</span>
<div className="min-w-0">
<h2 id={headingId} className="text-2xl font-semibold tracking-tight sm:text-3xl">
Your agenda
</h2>
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
The week ahead — Thursday, July 16 to Wednesday, July 22
</p>
</div>
</div>
<p
aria-live="polite"
className="mt-4 text-sm text-neutral-600 dark:text-neutral-300"
>
<span className="font-semibold text-neutral-900 dark:text-neutral-100">
{visibleCount}
</span>{" "}
{visibleCount === 1 ? "event" : "events"}
{isFiltered ? (
<span className="text-neutral-400 dark:text-neutral-500"> of {TOTAL_EVENTS}</span>
) : null}{" "}
across{" "}
<span className="font-semibold text-neutral-900 dark:text-neutral-100">
{filteredDays.length}
</span>{" "}
{filteredDays.length === 1 ? "day" : "days"}
</p>
</header>
{/* Controls */}
<div className="mb-8 flex flex-col gap-4 border-y border-neutral-200/80 py-4 dark:border-neutral-800 sm:flex-row sm:items-center sm:justify-between">
<div
role="group"
aria-label="Filter agenda by category"
className="flex flex-wrap items-center gap-2"
>
<button
type="button"
aria-pressed={activeCats.size === 0}
onClick={() => setActiveCats(new Set())}
className={`inline-flex items-center rounded-full px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-950 ${
activeCats.size === 0
? "bg-neutral-900 text-white ring-neutral-900 dark:bg-white dark:text-neutral-900 dark:ring-white"
: "bg-white text-neutral-600 ring-neutral-300 hover:bg-neutral-50 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800"
}`}
>
All
</button>
{CATEGORY_ORDER.map((key) => {
const cfg = CATEGORIES[key];
const on = activeCats.has(key);
return (
<button
key={key}
type="button"
aria-pressed={on}
onClick={() => toggleCat(key)}
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-950 ${
on
? cfg.chipOn
: "bg-white text-neutral-600 ring-neutral-300 hover:bg-neutral-50 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800"
}`}
>
<span
className={`h-2 w-2 rounded-full ${on ? "bg-current opacity-90" : cfg.dot}`}
/>
{cfg.label}
</button>
);
})}
</div>
<button
type="button"
role="switch"
aria-checked={hideCompleted}
onClick={() => setHideCompleted((v) => !v)}
className="group inline-flex shrink-0 items-center gap-2.5 rounded-full text-xs font-medium text-neutral-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-neutral-300 dark:focus-visible:ring-offset-neutral-950"
>
<span
className={`relative h-5 w-9 rounded-full transition-colors ${
hideCompleted
? "bg-indigo-600 dark:bg-indigo-500"
: "bg-neutral-300 dark:bg-neutral-700"
}`}
>
<span
className={`absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
hideCompleted ? "translate-x-4" : "translate-x-0"
}`}
/>
</span>
Hide completed
</button>
</div>
{/* Agenda list */}
{filteredDays.length === 0 ? (
<div className="rounded-2xl border border-dashed border-neutral-300 py-16 text-center dark:border-neutral-700">
<p className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
Nothing scheduled for this filter.
</p>
<button
type="button"
onClick={() => {
setActiveCats(new Set());
setHideCompleted(false);
}}
className="mt-3 text-sm font-semibold text-indigo-600 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-neutral-950"
>
Reset filters
</button>
</div>
) : (
<div className="space-y-8">
{filteredDays.map((day, dayIndex) => (
<section
key={day.id}
aria-label={`${day.weekdayLong}, ${day.monthShort} ${day.dayNum}`}
className="calag-rise"
style={reduce ? undefined : { animationDelay: `${dayIndex * 60}ms` }}
>
{/* Day header */}
<div className="mb-3 flex items-center gap-3">
<div
className={`flex h-12 w-12 shrink-0 flex-col items-center justify-center rounded-xl border ${
day.isToday
? "border-indigo-400 bg-indigo-50 dark:border-indigo-500/60 dark:bg-indigo-500/10"
: "border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900"
}`}
>
<span
className={`text-[10px] font-semibold uppercase tracking-wider ${
day.isToday
? "text-indigo-600 dark:text-indigo-300"
: "text-neutral-400 dark:text-neutral-500"
}`}
>
{day.weekdayShort}
</span>
<span className="text-lg font-semibold leading-none tabular-nums">
{day.dayNum}
</span>
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{day.relative ?? day.weekdayLong}
</h3>
{day.isToday ? (
<span className="inline-flex items-center gap-1.5 rounded-full bg-indigo-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-indigo-600 ring-1 ring-inset ring-indigo-600/20 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/25">
<span className="relative flex h-1.5 w-1.5">
<span className="calag-ping absolute inline-flex h-full w-full rounded-full bg-indigo-500" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-indigo-500" />
</span>
Now
</span>
) : null}
</div>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{day.weekdayLong}, {day.monthShort} {day.dayNum} ·{" "}
{day.events.length} {day.events.length === 1 ? "item" : "items"}
</p>
</div>
</div>
{/* Events */}
<ul className="space-y-2">
{day.events.map((ev) => {
const cfg = CATEGORIES[ev.category];
const isOpen = expanded.has(ev.id);
const isDone = completed.has(ev.id);
const hasReminder = reminders.has(ev.id);
const isDeadline = ev.category === "deadline";
const panelId = `${uid}-panel-${ev.id}`;
const triggerId = `${uid}-trigger-${ev.id}`;
return (
<li key={ev.id}>
<div className="flex gap-3">
{/* Time column */}
<div className="w-16 shrink-0 pt-3 text-right sm:w-20">
{isDeadline ? (
<span className="block text-[10px] font-semibold uppercase tracking-wide text-rose-500 dark:text-rose-400">
Due
</span>
) : null}
<span className="block text-xs font-semibold tabular-nums text-neutral-700 dark:text-neutral-200">
{ev.start}
</span>
{ev.end ? (
<span className="block text-[11px] tabular-nums text-neutral-400 dark:text-neutral-500">
{ev.end}
</span>
) : null}
</div>
{/* Card */}
<div
className={`relative flex-1 overflow-hidden rounded-xl border bg-white transition-colors dark:bg-neutral-900 ${
isDone
? "border-neutral-200/70 dark:border-neutral-800/70"
: "border-neutral-200 shadow-sm shadow-neutral-900/5 hover:border-neutral-300 dark:border-neutral-800 dark:hover:border-neutral-700"
}`}
>
<span
className={`absolute inset-y-2.5 left-0 w-1 rounded-r-full ${cfg.spine} ${
isDone ? "opacity-40" : ""
}`}
/>
<div className="flex items-start gap-1.5 pl-3.5 pr-2 py-2.5">
<button
id={triggerId}
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => toggleExpand(ev.id)}
className="flex flex-1 items-start gap-2 rounded-lg text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900"
>
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5">
{isDeadline ? (
<IconFlag className="h-3.5 w-3.5 shrink-0 text-rose-500 dark:text-rose-400" />
) : null}
<span
className={`text-sm font-semibold ${
isDone
? "text-neutral-400 line-through decoration-neutral-300 dark:text-neutral-500 dark:decoration-neutral-600"
: "text-neutral-900 dark:text-neutral-100"
}`}
>
{ev.title}
</span>
</span>
<span className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">
<span
className={`inline-flex items-center rounded-md px-1.5 py-0.5 text-[11px] font-medium ring-1 ring-inset ${cfg.chip}`}
>
{CATEGORIES[ev.category].label}
</span>
<span className="inline-flex items-center gap-1 text-xs text-neutral-500 dark:text-neutral-400">
<IconPin className="h-3.5 w-3.5" />
{ev.location}
</span>
</span>
</span>
<IconChevron
className={`mt-0.5 h-4 w-4 shrink-0 text-neutral-400 transition-transform duration-200 ${
isOpen ? "rotate-180" : ""
}`}
/>
</button>
<button
type="button"
aria-pressed={isDone}
onClick={() => toggleDone(ev.id)}
aria-label={
isDone
? `Mark "${ev.title}" as not done`
: `Mark "${ev.title}" as done`
}
className={`mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900 ${
isDone
? "border-emerald-500 bg-emerald-500 text-white dark:border-emerald-500 dark:bg-emerald-500"
: "border-neutral-300 text-transparent hover:border-emerald-400 hover:text-emerald-300 dark:border-neutral-600 dark:hover:border-emerald-500"
}`}
>
<IconCheck className="h-3.5 w-3.5" />
</button>
</div>
{/* Expandable detail */}
<AnimatePresence initial={false}>
{isOpen ? (
<motion.div
key="detail"
id={panelId}
role="region"
aria-labelledby={triggerId}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.28, ease: [0.22, 1, 0.36, 1] }
}
className="overflow-hidden"
>
<div className="border-t border-neutral-100 px-3.5 pb-3.5 pt-3 dark:border-neutral-800">
<p className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-300">
{ev.detail}
</p>
{ev.people.length > 0 ? (
<div className="mt-3 flex items-center gap-2.5">
<IconUsers className="h-4 w-4 shrink-0 text-neutral-400 dark:text-neutral-500" />
<ul className="flex flex-wrap items-center gap-2">
{ev.people.map((person, i) => (
<li
key={person}
className="inline-flex items-center gap-1.5 rounded-full bg-neutral-100 py-0.5 pl-0.5 pr-2.5 dark:bg-neutral-800"
>
<span
className={`grid h-6 w-6 place-items-center rounded-full text-[10px] font-bold ${
AVATAR_TONES[i % AVATAR_TONES.length]
}`}
>
{initials(person)}
</span>
<span className="text-xs font-medium text-neutral-700 dark:text-neutral-200">
{person}
</span>
</li>
))}
</ul>
</div>
) : null}
<div className="mt-3.5 flex flex-wrap items-center gap-2">
<button
type="button"
aria-pressed={hasReminder}
onClick={() => toggleReminder(ev.id)}
className={`inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-semibold ring-1 ring-inset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900 ${
hasReminder
? "bg-amber-50 text-amber-700 ring-amber-500/30 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/25"
: "bg-white text-neutral-600 ring-neutral-300 hover:bg-neutral-50 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800"
}`}
>
<IconBell className="h-3.5 w-3.5" />
{hasReminder ? "Reminder on" : "Set reminder"}
</button>
<button
type="button"
onClick={() => toggleDone(ev.id)}
className="inline-flex items-center gap-1.5 rounded-lg bg-white px-2.5 py-1.5 text-xs font-semibold text-neutral-600 ring-1 ring-inset ring-neutral-300 transition-colors hover:bg-neutral-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800 dark:focus-visible:ring-offset-neutral-900"
>
<IconCheck className="h-3.5 w-3.5" />
{isDone ? "Mark as not done" : "Mark done"}
</button>
</div>
</div>
</motion.div>
) : null}
</AnimatePresence>
</div>
</div>
</li>
);
})}
</ul>
</section>
))}
</div>
)}
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Calendar Date Picker
Originaldate picker with a dropdown calendar

Calendar Range Picker
Originaldate range picker with two months

Calendar Inline
Originalalways-visible inline calendar

Calendar Month Year
Originalmonth and year quick selector

Calendar Event
Originalmonth calendar with event dots

Calendar Mini
Originalcompact mini calendar widget

Calendar Time Picker
Originaltime picker with hour/minute

Calendar Datetime
Originalcombined date and time picker

Calendar Week
Originalweek view with time slots

Calendar Availability
Originalavailability slot picker

Calendar Booking
Originalbooking calendar with time slots

Calendar Multi Month
Originaltwo-month scrolling calendar

