Web InnoventixFreeCode

Dashboard Calendar Widget

Original · free

mini calendar dashboard widget

byWeb InnoventixReact + Tailwind
dashcalendarwidgetdashboards
Open

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/dash-calendar-widget.json
dash-calendar-widget.tsx
"use client";

import { useCallback, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";

type EventKind = "call" | "ship" | "focus" | "review";

type CalEvent = {
  id: string;
  /** ISO date, YYYY-MM-DD */
  date: string;
  time: string;
  title: string;
  detail: string;
  kind: EventKind;
};

const MONTHS = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

const WEEKDAYS = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
const WEEKDAYS_FULL = [
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday",
];

const KIND_META: Record<
  EventKind,
  { label: string; dot: string; chip: string; rail: string }
> = {
  call: {
    label: "Call",
    dot: "bg-indigo-500 dark:bg-indigo-400",
    chip: "bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/25",
    rail: "bg-indigo-500 dark:bg-indigo-400",
  },
  ship: {
    label: "Ship",
    dot: "bg-emerald-500 dark:bg-emerald-400",
    chip: "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-400/25",
    rail: "bg-emerald-500 dark:bg-emerald-400",
  },
  focus: {
    label: "Focus",
    dot: "bg-violet-500 dark:bg-violet-400",
    chip: "bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-500/10 dark:text-violet-300 dark:ring-violet-400/25",
    rail: "bg-violet-500 dark:bg-violet-400",
  },
  review: {
    label: "Review",
    dot: "bg-amber-500 dark:bg-amber-400",
    chip: "bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-400/25",
    rail: "bg-amber-500 dark:bg-amber-400",
  },
};

const EVENTS: CalEvent[] = [
  {
    id: "e1",
    date: "2026-03-02",
    time: "09:30",
    title: "Sprint kickoff — Billing v3",
    detail: "Scope the metered-usage migration with Priya and Tom.",
    kind: "call",
  },
  {
    id: "e2",
    date: "2026-03-02",
    time: "14:00",
    title: "Deep work: invoice reconciler",
    detail: "No meetings block. Finish the retry queue.",
    kind: "focus",
  },
  {
    id: "e3",
    date: "2026-03-05",
    time: "11:15",
    title: "Design review — onboarding flow",
    detail: "Second pass on the three-step signup. Bring the Figma prototype.",
    kind: "review",
  },
  {
    id: "e4",
    date: "2026-03-09",
    time: "08:45",
    title: "Customer call — Northwind",
    detail: "Renewal check-in. They asked about SSO pricing tiers.",
    kind: "call",
  },
  {
    id: "e5",
    date: "2026-03-11",
    time: "16:00",
    title: "Ship 3.4.0 to production",
    detail: "Canary at 10% for two hours, then full rollout.",
    kind: "ship",
  },
  {
    id: "e6",
    date: "2026-03-11",
    time: "17:30",
    title: "Postmortem — March 4 outage",
    detail: "Blameless writeup, action items owned by end of week.",
    kind: "review",
  },
  {
    id: "e7",
    date: "2026-03-17",
    time: "10:00",
    title: "Quarterly roadmap sync",
    detail: "Trim Q2 to three bets. Everything else is a maybe.",
    kind: "call",
  },
  {
    id: "e8",
    date: "2026-03-17",
    time: "13:00",
    title: "Focus: API docs rewrite",
    detail: "Rewrite the auth section around real curl examples.",
    kind: "focus",
  },
  {
    id: "e9",
    date: "2026-03-17",
    time: "15:45",
    title: "Ship docs site refresh",
    detail: "New search index plus the versioned sidebar.",
    kind: "ship",
  },
  {
    id: "e10",
    date: "2026-03-20",
    time: "09:00",
    title: "Interview — senior platform eng",
    detail: "System design round. Prompt: multi-region write path.",
    kind: "call",
  },
  {
    id: "e11",
    date: "2026-03-24",
    time: "12:30",
    title: "Ship webhooks beta",
    detail: "Twelve design partners get the signing-secret rotation.",
    kind: "ship",
  },
  {
    id: "e12",
    date: "2026-03-27",
    time: "15:00",
    title: "Pricing experiment review",
    detail: "Two weeks of data on the annual toggle. Read the cohort split.",
    kind: "review",
  },
  {
    id: "e13",
    date: "2026-04-01",
    time: "10:30",
    title: "Board prep — Q1 numbers",
    detail: "Net revenue retention story needs one clean chart.",
    kind: "review",
  },
  {
    id: "e14",
    date: "2026-04-03",
    time: "09:00",
    title: "Focus: latency budget",
    detail: "p99 is 380ms. Target is 200ms. Start with the fan-out.",
    kind: "focus",
  },
  {
    id: "e15",
    date: "2026-02-25",
    time: "14:30",
    title: "Retro — February",
    detail: "Two shipped bets, one dropped. Talk about the drop.",
    kind: "review",
  },
];

const TODAY_ISO = "2026-03-11";

function pad(n: number): string {
  return n < 10 ? `0${n}` : `${n}`;
}

function toIso(y: number, m: number, d: number): string {
  return `${y}-${pad(m + 1)}-${pad(d)}`;
}

function daysInMonth(y: number, m: number): number {
  return new Date(y, m + 1, 0).getDate();
}

/** Monday-first offset for the first of the month. */
function leadingBlanks(y: number, m: number): number {
  const js = new Date(y, m, 1).getDay();
  return (js + 6) % 7;
}

type Cell = { day: number; iso: string; weekdayIndex: number };

function ChevronIcon({ dir }: { dir: "left" | "right" }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-4 w-4"
      aria-hidden="true"
    >
      {dir === "left" ? (
        <path d="M15 6l-6 6 6 6" />
      ) : (
        <path d="M9 6l6 6-6 6" />
      )}
    </svg>
  );
}

function ClockIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-3.5 w-3.5"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 7v5l3 2" />
    </svg>
  );
}

function SparkIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-4 w-4"
      aria-hidden="true"
    >
      <path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8" />
    </svg>
  );
}

export default function DashCalendarWidget() {
  const reduced = useReducedMotion();

  const [year, setYear] = useState<number>(2026);
  const [month, setMonth] = useState<number>(2);
  const [selected, setSelected] = useState<string>(TODAY_ISO);
  const [dir, setDir] = useState<1 | -1>(1);
  const [filter, setFilter] = useState<EventKind | "all">("all");

  const gridRef = useRef<HTMLDivElement | null>(null);

  const eventsByDate = useMemo(() => {
    const map = new Map<string, CalEvent[]>();
    for (const ev of EVENTS) {
      if (filter !== "all" && ev.kind !== filter) continue;
      const list = map.get(ev.date);
      if (list) list.push(ev);
      else map.set(ev.date, [ev]);
    }
    for (const list of map.values()) list.sort((a, b) => a.time.localeCompare(b.time));
    return map;
  }, [filter]);

  const cells = useMemo<Cell[]>(() => {
    const total = daysInMonth(year, month);
    const out: Cell[] = [];
    for (let d = 1; d <= total; d++) {
      out.push({
        day: d,
        iso: toIso(year, month, d),
        weekdayIndex: (new Date(year, month, d).getDay() + 6) % 7,
      });
    }
    return out;
  }, [year, month]);

  const blanks = useMemo(() => leadingBlanks(year, month), [year, month]);

  const selectedEvents = eventsByDate.get(selected) ?? [];

  const monthCount = useMemo(() => {
    let n = 0;
    for (const [iso, list] of eventsByDate) {
      if (iso.startsWith(`${year}-${pad(month + 1)}`)) n += list.length;
    }
    return n;
  }, [eventsByDate, year, month]);

  const shift = useCallback(
    (delta: 1 | -1) => {
      setDir(delta);
      setMonth((m) => {
        const next = m + delta;
        if (next < 0) {
          setYear((y) => y - 1);
          return 11;
        }
        if (next > 11) {
          setYear((y) => y + 1);
          return 0;
        }
        return next;
      });
    },
    []
  );

  const focusIso = useCallback((iso: string) => {
    const node = gridRef.current?.querySelector<HTMLButtonElement>(
      `[data-iso="${iso}"]`
    );
    node?.focus();
  }, []);

  const moveSelection = useCallback(
    (deltaDays: number) => {
      const [y, m, d] = selected.split("-").map(Number);
      const next = new Date(y, m - 1, d + deltaDays);
      const nIso = toIso(next.getFullYear(), next.getMonth(), next.getDate());
      setSelected(nIso);
      if (next.getMonth() !== month || next.getFullYear() !== year) {
        setDir(deltaDays > 0 ? 1 : -1);
        setMonth(next.getMonth());
        setYear(next.getFullYear());
        window.setTimeout(() => focusIso(nIso), 0);
      } else {
        focusIso(nIso);
      }
    },
    [selected, month, year, focusIso]
  );

  const onGridKeyDown = useCallback(
    (e: ReactKeyboardEvent<HTMLDivElement>) => {
      switch (e.key) {
        case "ArrowLeft":
          e.preventDefault();
          moveSelection(-1);
          break;
        case "ArrowRight":
          e.preventDefault();
          moveSelection(1);
          break;
        case "ArrowUp":
          e.preventDefault();
          moveSelection(-7);
          break;
        case "ArrowDown":
          e.preventDefault();
          moveSelection(7);
          break;
        case "Home": {
          e.preventDefault();
          const [y, m, d] = selected.split("-").map(Number);
          const wd = (new Date(y, m - 1, d).getDay() + 6) % 7;
          moveSelection(-wd);
          break;
        }
        case "End": {
          e.preventDefault();
          const [y, m, d] = selected.split("-").map(Number);
          const wd = (new Date(y, m - 1, d).getDay() + 6) % 7;
          moveSelection(6 - wd);
          break;
        }
        case "PageUp":
          e.preventDefault();
          moveSelection(-daysInMonth(year, month === 0 ? 11 : month - 1));
          break;
        case "PageDown":
          e.preventDefault();
          moveSelection(daysInMonth(year, month));
          break;
        default:
          break;
      }
    },
    [moveSelection, selected, year, month]
  );

  const goToday = useCallback(() => {
    setDir(TODAY_ISO > selected ? 1 : -1);
    setSelected(TODAY_ISO);
    setYear(2026);
    setMonth(2);
    window.setTimeout(() => focusIso(TODAY_ISO), 0);
  }, [selected, focusIso]);

  const selectedLabel = useMemo(() => {
    const [y, m, d] = selected.split("-").map(Number);
    const wd = (new Date(y, m - 1, d).getDay() + 6) % 7;
    return `${WEEKDAYS_FULL[wd]}, ${MONTHS[m - 1]} ${d}`;
  }, [selected]);

  const slideX = reduced ? 0 : dir === 1 ? 18 : -18;

  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
      <style>{`
        @keyframes dcw-pulse-ring {
          0% { transform: scale(0.85); opacity: 0.55; }
          70% { transform: scale(1.6); opacity: 0; }
          100% { transform: scale(1.6); opacity: 0; }
        }
        @keyframes dcw-sheen {
          0% { background-position: -220% 0; }
          100% { background-position: 220% 0; }
        }
        .dcw-ring::after {
          content: "";
          position: absolute;
          inset: 0;
          border-radius: 9999px;
          border: 1px solid currentColor;
          animation: dcw-pulse-ring 2.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
        }
        .dcw-sheen {
          background-image: linear-gradient(
            100deg,
            transparent 38%,
            rgba(99, 102, 241, 0.14) 50%,
            transparent 62%
          );
          background-size: 220% 100%;
          animation: dcw-sheen 7s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .dcw-ring::after,
          .dcw-sheen {
            animation: none !important;
          }
          .dcw-sheen { background-image: none; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-5xl">
        <header className="mb-8 flex flex-col gap-2">
          <p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
            Workspace
          </p>
          <h2 className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-50">
            Schedule at a glance
          </h2>
          <p className="max-w-xl text-sm leading-relaxed text-slate-600 dark:text-slate-400">
            Pick a day to read its agenda. Arrow keys move the selection, Page
            Up and Page Down jump a month.
          </p>
        </header>

        <div className="grid gap-4 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)]">
          {/* Calendar card */}
          <div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
            <div
              className="pointer-events-none absolute inset-x-0 top-0 h-px dcw-sheen"
              aria-hidden="true"
            />

            <div className="flex items-center justify-between gap-3 border-b border-slate-200 px-4 py-3.5 sm:px-5 dark:border-slate-800">
              <div className="min-w-0">
                <div
                  className="truncate text-sm font-semibold text-slate-900 dark:text-slate-100"
                  aria-live="polite"
                >
                  {MONTHS[month]} {year}
                </div>
                <div className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                  {monthCount === 0
                    ? "Nothing scheduled"
                    : `${monthCount} ${monthCount === 1 ? "entry" : "entries"}`}
                </div>
              </div>

              <div className="flex shrink-0 items-center gap-1.5">
                <button
                  type="button"
                  onClick={goToday}
                  className="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
                >
                  Today
                </button>
                <button
                  type="button"
                  onClick={() => shift(-1)}
                  aria-label={`Previous month, ${
                    MONTHS[month === 0 ? 11 : month - 1]
                  }`}
                  className="rounded-lg border border-slate-200 p-1.5 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-offset-slate-900"
                >
                  <ChevronIcon dir="left" />
                </button>
                <button
                  type="button"
                  onClick={() => shift(1)}
                  aria-label={`Next month, ${MONTHS[month === 11 ? 0 : month + 1]}`}
                  className="rounded-lg border border-slate-200 p-1.5 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-offset-slate-900"
                >
                  <ChevronIcon dir="right" />
                </button>
              </div>
            </div>

            <div className="px-3 pb-4 pt-3 sm:px-4">
              <div className="mb-1 grid grid-cols-7 gap-1" aria-hidden="true">
                {WEEKDAYS.map((w, i) => (
                  <div
                    key={w}
                    className={`py-1.5 text-center text-[11px] font-semibold uppercase tracking-wider ${
                      i > 4
                        ? "text-slate-400 dark:text-slate-600"
                        : "text-slate-500 dark:text-slate-400"
                    }`}
                  >
                    {w}
                  </div>
                ))}
              </div>

              <AnimatePresence mode="wait" initial={false}>
                <motion.div
                  key={`${year}-${month}`}
                  initial={{ opacity: 0, x: slideX }}
                  animate={{ opacity: 1, x: 0 }}
                  exit={{ opacity: 0, x: -slideX }}
                  transition={{ duration: reduced ? 0 : 0.22, ease: [0.16, 1, 0.3, 1] }}
                >
                  <div
                    ref={gridRef}
                    role="grid"
                    aria-label={`${MONTHS[month]} ${year}`}
                    onKeyDown={onGridKeyDown}
                    className="grid grid-cols-7 gap-1"
                  >
                    {Array.from({ length: blanks }).map((_, i) => (
                      <div key={`blank-${i}`} role="presentation" />
                    ))}

                    {cells.map((cell) => {
                      const list = eventsByDate.get(cell.iso) ?? [];
                      const isToday = cell.iso === TODAY_ISO;
                      const isSel = cell.iso === selected;
                      const weekend = cell.weekdayIndex > 4;
                      const kinds = Array.from(new Set(list.map((e) => e.kind))).slice(
                        0,
                        3
                      );

                      return (
                        <button
                          key={cell.iso}
                          type="button"
                          role="gridcell"
                          data-iso={cell.iso}
                          tabIndex={isSel ? 0 : -1}
                          aria-selected={isSel}
                          aria-current={isToday ? "date" : undefined}
                          aria-label={`${MONTHS[month]} ${cell.day}, ${year}${
                            list.length
                              ? `, ${list.length} ${
                                  list.length === 1 ? "event" : "events"
                                }`
                              : ", no events"
                          }`}
                          onClick={() => setSelected(cell.iso)}
                          className={[
                            "relative flex aspect-square flex-col items-center justify-center gap-1 rounded-xl text-sm 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-slate-900",
                            isSel
                              ? "bg-slate-900 font-semibold text-white dark:bg-slate-100 dark:text-slate-900"
                              : weekend
                                ? "text-slate-400 hover:bg-slate-100 dark:text-slate-500 dark:hover:bg-slate-800"
                                : "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800",
                            !isSel && isToday
                              ? "font-semibold text-indigo-600 dark:text-indigo-400"
                              : "",
                          ].join(" ")}
                        >
                          {isToday && !isSel ? (
                            <span
                              className="dcw-ring pointer-events-none absolute inset-1.5 rounded-full text-indigo-500/70 dark:text-indigo-400/70"
                              aria-hidden="true"
                            />
                          ) : null}

                          <span className="relative tabular-nums leading-none">
                            {cell.day}
                          </span>

                          <span
                            className="flex h-1.5 items-center gap-[3px]"
                            aria-hidden="true"
                          >
                            {kinds.map((k) => (
                              <span
                                key={k}
                                className={[
                                  "h-1.5 w-1.5 rounded-full",
                                  isSel
                                    ? "bg-white/70 dark:bg-slate-900/60"
                                    : KIND_META[k].dot,
                                ].join(" ")}
                              />
                            ))}
                          </span>
                        </button>
                      );
                    })}
                  </div>
                </motion.div>
              </AnimatePresence>
            </div>

            <div className="flex flex-wrap items-center gap-1.5 border-t border-slate-200 px-3 py-3 sm:px-4 dark:border-slate-800">
              <span className="mr-1 text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
                Filter
              </span>
              {(["all", "call", "ship", "focus", "review"] as const).map((k) => {
                const active = filter === k;
                return (
                  <button
                    key={k}
                    type="button"
                    aria-pressed={active}
                    onClick={() => setFilter(k)}
                    className={[
                      "inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ring-1 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-slate-900",
                      active
                        ? "bg-slate-900 text-white ring-slate-900 dark:bg-slate-100 dark:text-slate-900 dark:ring-slate-100"
                        : "bg-transparent text-slate-600 ring-slate-200 hover:bg-slate-100 dark:text-slate-300 dark:ring-slate-700 dark:hover:bg-slate-800",
                    ].join(" ")}
                  >
                    {k !== "all" ? (
                      <span
                        className={`h-1.5 w-1.5 rounded-full ${
                          active ? "bg-current" : KIND_META[k].dot
                        }`}
                        aria-hidden="true"
                      />
                    ) : null}
                    {k === "all" ? "All" : KIND_META[k].label}
                  </button>
                );
              })}
            </div>
          </div>

          {/* Agenda card */}
          <div className="flex flex-col rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
            <div className="border-b border-slate-200 px-4 py-3.5 sm:px-5 dark:border-slate-800">
              <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">
                {selectedLabel}
              </div>
              <div className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                {selected === TODAY_ISO ? "Today · " : ""}
                {selectedEvents.length === 0
                  ? "Open calendar"
                  : `${selectedEvents.length} ${
                      selectedEvents.length === 1 ? "entry" : "entries"
                    }`}
              </div>
            </div>

            <div
              className="min-h-[19rem] flex-1 px-3 py-3 sm:px-4"
              aria-live="polite"
              aria-atomic="false"
            >
              <AnimatePresence mode="wait" initial={false}>
                <motion.ul
                  key={`${selected}-${filter}`}
                  initial={{ opacity: 0, y: reduced ? 0 : 6 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: reduced ? 0 : -6 }}
                  transition={{ duration: reduced ? 0 : 0.18, ease: [0.16, 1, 0.3, 1] }}
                  className="space-y-2"
                >
                  {selectedEvents.length === 0 ? (
                    <li className="flex h-[17rem] flex-col items-center justify-center gap-2 text-center">
                      <span className="text-slate-400 dark:text-slate-600">
                        <SparkIcon />
                      </span>
                      <p className="text-sm font-medium text-slate-700 dark:text-slate-300">
                        No entries on this day
                      </p>
                      <p className="max-w-[16rem] text-xs leading-relaxed text-slate-500 dark:text-slate-400">
                        {filter === "all"
                          ? "A clear day. Good time to pull work forward."
                          : `Nothing tagged ${KIND_META[filter].label.toLowerCase()} here — try the All filter.`}
                      </p>
                    </li>
                  ) : (
                    selectedEvents.map((ev, i) => (
                      <motion.li
                        key={ev.id}
                        initial={{ opacity: 0, y: reduced ? 0 : 8 }}
                        animate={{ opacity: 1, y: 0 }}
                        transition={{
                          duration: reduced ? 0 : 0.24,
                          delay: reduced ? 0 : i * 0.05,
                          ease: [0.16, 1, 0.3, 1],
                        }}
                        className="relative overflow-hidden rounded-xl border border-slate-200 bg-slate-50/60 p-3 pl-4 dark:border-slate-800 dark:bg-slate-800/40"
                      >
                        <span
                          className={`absolute inset-y-0 left-0 w-1 ${KIND_META[ev.kind].rail}`}
                          aria-hidden="true"
                        />
                        <div className="flex items-start justify-between gap-3">
                          <div className="min-w-0">
                            <p className="text-sm font-medium leading-snug text-slate-900 dark:text-slate-100">
                              {ev.title}
                            </p>
                            <p className="mt-1 text-xs leading-relaxed text-slate-600 dark:text-slate-400">
                              {ev.detail}
                            </p>
                          </div>
                          <span
                            className={`shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium ring-1 ${KIND_META[ev.kind].chip}`}
                          >
                            {KIND_META[ev.kind].label}
                          </span>
                        </div>
                        <div className="mt-2 inline-flex items-center gap-1.5 text-xs tabular-nums text-slate-500 dark:text-slate-400">
                          <ClockIcon />
                          {ev.time}
                        </div>
                      </motion.li>
                    ))
                  )}
                </motion.ul>
              </AnimatePresence>
            </div>
          </div>
        </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 quote

Similar components

Browse all →