Fazekit

Code

"use client";
import * as React from "react";
import { AnimatePresence, LayoutGroup, motion, MotionConfig, useReducedMotion } from "motion/react";
import { CalendarDays, CheckCircle2, ChevronLeft, ChevronRight, Menu, Plus, X } from "lucide-react";
import { cn } from "@/lib/utils";
import {
  addDays,
  addMinutes,
  addMonths,
  atMinutes,
  dateKey,
  expandEvents,
  minutesOfDay,
  monthMatrix,
  parseLocal,
  rangeTitle,
  startOfDay,
  startOfWeek,
  toLocalISO,
} from "./cal-utils";
import { IconButton, focusRing, useMinWidth } from "./calendar-ui";
import { DEFAULT_CALENDARS, SEED_EVENTS, SEED_NOW } from "./data";
import { EventDialog, EventPopover, type EditTarget } from "./event-dialogs";
import { MonthView } from "./month-view";
import { CalendarSidebar } from "./sidebar";
import { TimeGrid, type GridDraft } from "./time-grid";
import type { CalendarEvent, CalendarSource, CalendarView, Occurrence } from "./types";

export type { CalendarEvent, CalendarSource, CalendarView, RepeatRule, CalendarColor } from "./types";

export interface CalendarAppProps {
  /** Events to show. Defaults to a seeded, deterministic demo schedule. */
  initialEvents?: CalendarEvent[];
  calendars?: CalendarSource[];
  /** Calendar ids hidden at start. */
  initialHidden?: string[];
  initialView?: CalendarView;
  /** "YYYY-MM-DD" or Date to open on. Defaults to today. */
  initialDate?: string | Date;
  /** Reference "now" (current-time line, today highlight). Defaults to the seed time (demo) or the client clock (your data). */
  now?: string | Date;
  weekStartsOn?: 0 | 1;
  /** Pixel height of one hour on the time grid. */
  hourHeight?: number;
  appName?: string;
  onEventCreate?: (event: CalendarEvent) => void;
  onEventUpdate?: (event: CalendarEvent) => void;
  onEventDelete?: (event: CalendarEvent) => void;
  /** Fires with the full list after every change. */
  onChange?: (events: CalendarEvent[]) => void;
  className?: string;
}

type Toast = { id: number; text: string; undo?: () => void };
type PopoverState = { occ: Occurrence; anchor: { top: number; left: number; right: number; bottom: number } } | null;

let seq = 0;
const newId = () => `evt-${Date.now().toString(36)}-${(++seq).toString(36)}`;
const VIEWS: { id: CalendarView; label: string; key: string }[] = [
  { id: "month", label: "Month", key: "M" },
  { id: "week", label: "Week", key: "W" },
  { id: "day", label: "Day", key: "D" },
];

export function CalendarApp({
  initialEvents,
  calendars = DEFAULT_CALENDARS,
  initialHidden = [],
  initialView,
  initialDate,
  now: nowProp,
  weekStartsOn = 0,
  hourHeight = 48,
  appName = "Lumen Calendar",
  onEventCreate,
  onEventUpdate,
  onEventDelete,
  onChange,
  className,
}: CalendarAppProps) {
  const reduced = useReducedMotion();
  const [events, setEvents] = React.useState<CalendarEvent[]>(initialEvents ?? SEED_EVENTS);
  const [hidden, setHidden] = React.useState<Set<string>>(() => new Set(initialHidden));
  const [base, setBase] = React.useState(() => parseLocal(typeof nowProp === "string" ? nowProp : nowProp ? toLocalISO(nowProp) : SEED_NOW));
  const [elapsed, setElapsed] = React.useState(0);
  const now = React.useMemo(() => new Date(base.getTime() + elapsed), [base, elapsed]);
  const [view, setView] = React.useState<CalendarView>(initialView ?? "week");
  const [date, setDate] = React.useState<Date>(() =>
    startOfDay(initialDate ? (typeof initialDate === "string" ? parseLocal(initialDate) : initialDate) : base),
  );
  const [dir, setDir] = React.useState(0);
  const [popover, setPopover] = React.useState<PopoverState>(null);
  const [edit, setEdit] = React.useState<EditTarget | null>(null);
  const [draft, setDraft] = React.useState<GridDraft>(null);
  const [drawer, setDrawer] = React.useState(false);
  const [toasts, setToasts] = React.useState<Toast[]>([]);
  const rootRef = React.useRef<HTMLDivElement>(null);
  const [bounds, setBounds] = React.useState({ width: 1280, height: 760 });
  const scrollMemory = React.useRef<number | null>(null);
  const wide = useMinWidth(640);
  const lg = useMinWidth(1024);
  const compact = !wide;

  // Clock: anchored to the seed for the demo, real time for your own data.
  React.useEffect(() => {
    const t0 = Date.now();
    if (!nowProp && initialEvents) {
      const real = new Date();
      setBase(real);
      if (!initialDate) setDate(startOfDay(real));
    }
    const id = window.setInterval(() => setElapsed(Date.now() - t0), 30_000);
    return () => window.clearInterval(id);
  }, [nowProp, initialEvents, initialDate]);

  // Phones open on the day view unless a view was requested.
  React.useEffect(() => {
    if (!initialView && !window.matchMedia("(min-width: 640px)").matches) setView("day");
  }, [initialView]);

  React.useLayoutEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const ro = new ResizeObserver(() => setBounds({ width: el.clientWidth, height: el.clientHeight }));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const onChangeRef = React.useRef(onChange);
  React.useLayoutEffect(() => {
    onChangeRef.current = onChange;
  });
  const first = React.useRef(true);
  React.useEffect(() => {
    if (first.current) {
      first.current = false;
      return;
    }
    onChangeRef.current?.(events);
  }, [events]);

  const calById = React.useMemo(() => Object.fromEntries(calendars.map((c) => [c.id, c])), [calendars]);
  const visibleEvents = React.useMemo(() => events.filter((e) => !hidden.has(e.calendarId) && calById[e.calendarId]), [events, hidden, calById]);

  /* ------------------------------ range ------------------------------ */

  const days = React.useMemo(() => {
    if (view === "day") return [date];
    if (view === "week") {
      const s = startOfWeek(date, weekStartsOn);
      return Array.from({ length: 7 }, (_, i) => addDays(s, i));
    }
    return monthMatrix(date, weekStartsOn);
  }, [view, date, weekStartsOn]);
  const from = days[0];
  const to = addDays(days[days.length - 1], 1);
  const occurrences = React.useMemo(() => expandEvents(visibleEvents, from, to), [visibleEvents, from, to]);

  const busy = React.useMemo(() => {
    const m = monthMatrix(date, weekStartsOn);
    const occ = expandEvents(visibleEvents, addDays(m[0], -42), addDays(m[41], 43));
    const s = new Set<string>();
    for (const o of occ) s.add(dateKey(o.start));
    return s;
  }, [visibleEvents, date, weekStartsOn]);

  const title = rangeTitle(view, date, weekStartsOn);
  const shortTitle =
    view === "day"
      ? date.toLocaleDateString("en-US", { month: "short", day: "numeric", weekday: "short" })
      : title.replace(/([A-Z][a-z]{2})[a-z]+/g, "$1");
  const rangeKey = `${view}-${dateKey(view === "month" ? new Date(date.getFullYear(), date.getMonth(), 1) : days[0])}`;

  /* ------------------------------ toasts ------------------------------ */

  const toast = React.useCallback((text: string, undo?: () => void) => {
    const id = ++seq;
    setToasts((ts) => [...ts.slice(-1), { id, text, undo }]);
    window.setTimeout(() => setToasts((ts) => ts.filter((t) => t.id !== id)), 5000);
  }, []);
  const dismiss = (id: number) => setToasts((ts) => ts.filter((t) => t.id !== id));

  /* ------------------------------ navigation ------------------------------ */

  const navigate = (n: number) => {
    setPopover(null);
    setDir(n);
    setDate((d) => (view === "month" ? addMonths(new Date(d.getFullYear(), d.getMonth(), 1), n) : addDays(d, n * (view === "week" ? 7 : 1))));
  };
  const goTo = (d: Date, v: CalendarView = view) => {
    setPopover(null);
    const target = startOfDay(d);
    setDir(target > date ? 1 : target < date ? -1 : 0);
    setDate(target);
    setView(v);
    setDrawer(false);
  };
  const goToday = () => goTo(now);
  const switchView = (v: CalendarView) => {
    if (v === view) return;
    setPopover(null);
    setDir(0);
    setView(v);
  };

  /* ------------------------------ mutations ------------------------------ */

  const patchEvent = (id: string, fn: (e: CalendarEvent) => CalendarEvent) => {
    const cur = events.find((e) => e.id === id);
    if (!cur) return;
    const next = fn(cur);
    setEvents((es) => es.map((e) => (e.id === id ? next : e)));
    onEventUpdate?.(next);
  };

  const withUndo = (text: string, change: () => void) => {
    const snapshot = events;
    change();
    toast(text, () => setEvents(snapshot));
  };

  const shift = (e: CalendarEvent, deltaMs: number, which: "both" | "end") => ({
    ...e,
    start: which === "both" ? toLocalISO(new Date(parseLocal(e.start).getTime() + deltaMs)) : e.start,
    end: toLocalISO(new Date(parseLocal(e.end).getTime() + deltaMs)),
  });

  const moveOcc = (occ: Occurrence, start: Date) => {
    const delta = start.getTime() - occ.start.getTime();
    const recurring = occ.event.repeat && occ.event.repeat !== "none";
    setPopover(null);
    withUndo(recurring ? "Moved all occurrences" : `Moved to ${start.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" })}${occ.event.allDay ? "" : `, ${start.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}`}`, () => {
      patchEvent(occ.event.id, (e) => shift(e, delta, "both"));
    });
  };

  const resizeOcc = (occ: Occurrence, end: Date) => {
    const delta = end.getTime() - occ.end.getTime();
    setPopover(null);
    withUndo(`Ends at ${end.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}`, () => {
      patchEvent(occ.event.id, (e) => shift(e, delta, "end"));
    });
  };

  const defaultCalendar = () => calendars.find((c) => c.group !== "other" && !hidden.has(c.id))?.id ?? calendars[0]?.id ?? "default";

  const startCreate = (start: Date, end: Date, allDay?: boolean) => {
    setPopover(null);
    const ev: CalendarEvent = { id: newId(), title: "", start: toLocalISO(start), end: toLocalISO(end), calendarId: defaultCalendar(), allDay: allDay || undefined, repeat: "none" };
    setDraft({ start, end, allDay });
    setEdit({ mode: "create", event: ev });
  };

  /** New 1-hour event: next full hour when looking at today, otherwise 9am. */
  const createAtNextSlot = () => {
    const isToday = dateKey(date) === dateKey(now);
    const m = isToday ? Math.min(Math.ceil((minutesOfDay(now) + 1) / 60) * 60, 23 * 60) : 9 * 60;
    const s = atMinutes(date, m);
    startCreate(s, addMinutes(s, 60));
  };

  const save = (ev: CalendarEvent) => {
    const isNew = edit?.mode === "create";
    setEdit(null);
    setDraft(null);
    if (isNew) {
      setEvents((es) => [...es, ev]);
      onEventCreate?.(ev);
      if (hidden.has(ev.calendarId)) setHidden((h) => new Set([...h].filter((x) => x !== ev.calendarId)));
      toast(`Created “${ev.title || "(No title)"}”`);
      const s = parseLocal(ev.start);
      if (s < from || s >= to) goTo(s);
    } else {
      withUndo("Event saved", () => {
        setEvents((es) => es.map((e) => (e.id === ev.id ? ev : e)));
      });
      onEventUpdate?.(ev);
    }
  };

  const remove = (ev: CalendarEvent) => {
    setEdit(null);
    setPopover(null);
    setDraft(null);
    withUndo(`Deleted “${ev.title || "(No title)"}”`, () => setEvents((es) => es.filter((e) => e.id !== ev.id)));
    onEventDelete?.(ev);
  };

  const duplicate = (occ: Occurrence) => {
    setPopover(null);
    const dur = occ.end.getTime() - occ.start.getTime();
    const ev: CalendarEvent = { ...occ.event, id: newId(), title: occ.event.title ? `${occ.event.title} (copy)` : "", start: toLocalISO(occ.start), end: toLocalISO(new Date(occ.start.getTime() + dur)) };
    setDraft({ start: occ.start, end: new Date(occ.start.getTime() + dur), allDay: occ.event.allDay });
    setEdit({ mode: "create", event: ev });
  };

  const openOcc = (occ: Occurrence, el: HTMLElement) => {
    const root = rootRef.current?.getBoundingClientRect();
    const r = el.getBoundingClientRect();
    if (!root) return;
    if (popover?.occ.key === occ.key) return setPopover(null);
    setPopover({ occ, anchor: { top: r.top - root.top, bottom: r.bottom - root.top, left: r.left - root.left, right: r.right - root.left } });
  };

  const editOcc = (occ: Occurrence) => {
    setPopover(null);
    setEdit({ mode: "edit", event: occ.event });
  };

  /* ------------------------------ keyboard ------------------------------ */

  const keyRef = React.useRef<(e: KeyboardEvent) => void>(() => {});
  React.useLayoutEffect(() => {
    keyRef.current = (e) => {
      if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
      const t = e.target as HTMLElement;
      if (e.key === "Escape") {
        if (popover) return setPopover(null);
        if (drawer) return setDrawer(false);
        return;
      }
      if (edit || t.closest("input,textarea,select,[contenteditable]")) return;
      const act = (fn: () => void) => {
        e.preventDefault();
        fn();
      };
      const k = e.key.toLowerCase();
      if (e.key === "ArrowLeft") act(() => navigate(-1));
      else if (e.key === "ArrowRight") act(() => navigate(1));
      else if (k === "t") act(goToday);
      else if (k === "n" || k === "c") act(createAtNextSlot);
      else if (k === "m") act(() => switchView("month"));
      else if (k === "w") act(() => switchView("week"));
      else if (k === "d") act(() => switchView("day"));
    };
  });
  React.useEffect(() => {
    const on = (e: KeyboardEvent) => keyRef.current(e);
    window.addEventListener("keydown", on);
    return () => window.removeEventListener("keydown", on);
  }, []);

  /* -------------------------------- render ------------------------------- */

  const sidebar = (
    <CalendarSidebar
      selected={date}
      today={now}
      view={view}
      weekStartsOn={weekStartsOn}
      busy={busy}
      onPick={(d) => goTo(d)}
      calendars={calendars}
      hidden={hidden}
      onToggle={(id) =>
        setHidden((h) => {
          const n = new Set(h);
          if (n.has(id)) n.delete(id);
          else n.add(id);
          return n;
        })
      }
      onOnly={(id) => setHidden(new Set(calendars.filter((c) => c.id !== id).map((c) => c.id)))}
      onCreate={() => {
        setDrawer(false);
        createAtNextSlot();
      }}
    />
  );

  const shared = {
    occurrences,
    calendars: calById,
    now,
    activeKey: popover?.occ.key ?? null,
    compact,
    onCreate: startCreate,
    onOpen: openOcc,
    onMove: moveOcc,
    onPickDay: (d: Date) => goTo(d, "day"),
  };

  const variants = {
    enter: (d: number) => (reduced ? { opacity: 0 } : d === 0 ? { opacity: 0, scale: 0.985 } : { opacity: 0, x: d * 48 }),
    center: { opacity: 1, x: 0, scale: 1 },
    exit: (d: number) => (reduced ? { opacity: 0 } : d === 0 ? { opacity: 0, scale: 1.01 } : { opacity: 0, x: d * -48 }),
  };

  return (
    <MotionConfig reducedMotion="user">
      <div ref={rootRef} className={cn("relative isolate flex h-[760px] w-full overflow-hidden bg-background text-foreground antialiased", className)}>
        <div inert={edit || drawer ? true : undefined} className="flex min-w-0 flex-1">
          <aside aria-label="Calendars" className="hidden w-64 shrink-0 border-r bg-muted/25 lg:block dark:bg-muted/15">
            <div className="flex h-14 items-center gap-2.5 px-5">
              <Logo />
              <span className="text-[15px] font-semibold tracking-tight">{appName}</span>
            </div>
            <div className="h-[calc(100%-3.5rem)]">{sidebar}</div>
          </aside>

          <div className="flex min-w-0 flex-1 flex-col">
            {/* Toolbar */}
            <header className="flex h-14 shrink-0 items-center gap-1.5 border-b px-2 sm:gap-2 sm:px-4">
              <IconButton label="Open calendars" onClick={() => setDrawer(true)} className="lg:hidden">
                <Menu className="size-[18px]" />
              </IconButton>
              <button
                type="button"
                onClick={goToday}
                title="Today (T)"
                className={cn("h-8 shrink-0 rounded-lg border px-2.5 text-[13px] font-medium transition hover:bg-accent sm:px-3.5", focusRing)}
              >
                Today
              </button>
              <div className="flex shrink-0">
                <IconButton label={`Previous ${view}`} onClick={() => navigate(-1)}>
                  <ChevronLeft className="size-[18px]" />
                </IconButton>
                <IconButton label={`Next ${view}`} onClick={() => navigate(1)}>
                  <ChevronRight className="size-[18px]" />
                </IconButton>
              </div>
              <div className="relative min-w-0 flex-1 overflow-hidden">
                <AnimatePresence mode="popLayout" initial={false}>
                  <motion.h1
                    key={title}
                    initial={{ opacity: 0, y: dir >= 0 ? 8 : -8 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: dir >= 0 ? -8 : 8 }}
                    transition={{ duration: 0.18 }}
                    aria-live="polite"
                    className="truncate text-[15px] font-semibold tracking-tight sm:text-lg"
                  >
                    {compact ? shortTitle : title}
                  </motion.h1>
                </AnimatePresence>
              </div>

              <LayoutGroup id="cal-views">
                <div role="radiogroup" aria-label="View" className="flex shrink-0 rounded-lg bg-muted p-0.5 text-[12px] font-medium">
                  {VIEWS.map((v) => (
                    <button
                      key={v.id}
                      type="button"
                      role="radio"
                      aria-checked={view === v.id}
                      aria-label={v.label}
                      title={`${v.label} (${v.key})`}
                      onClick={() => switchView(v.id)}
                      className={cn(
                        "relative h-7 rounded-md px-2.5 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring sm:px-3",
                        view === v.id ? "text-foreground" : "text-muted-foreground hover:text-foreground",
                      )}
                    >
                      {view === v.id && <motion.span layoutId="cal-view-pill" className="absolute inset-0 rounded-md bg-background shadow-sm dark:bg-accent" transition={{ type: "spring", stiffness: 500, damping: 38 }} />}
                      <span className="relative">{compact ? v.key : v.label}</span>
                    </button>
                  ))}
                </div>
              </LayoutGroup>
              {!lg && (
                <button
                  type="button"
                  onClick={createAtNextSlot}
                  className={cn("hidden h-8 shrink-0 items-center gap-1.5 rounded-lg bg-primary px-3 text-[13px] font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 sm:inline-flex", focusRing)}
                >
                  <Plus className="size-4" aria-hidden />
                  New
                </button>
              )}
            </header>

            <main aria-label={`${title}, ${view} view`} className="relative min-h-0 flex-1 overflow-hidden">
              <AnimatePresence mode="popLayout" initial={false} custom={dir}>
                <motion.div
                  key={rangeKey}
                  custom={dir}
                  variants={variants}
                  initial="enter"
                  animate="center"
                  exit="exit"
                  transition={{ duration: 0.24, ease: [0.2, 0, 0, 1] }}
                  className="absolute inset-0"
                >
                  {view === "month" ? (
                    <MonthView month={date} selected={date} weekStartsOn={weekStartsOn} {...shared} />
                  ) : (
                    <TimeGrid
                      days={days}
                      hourHeight={hourHeight}
                      draft={draft}
                      getSavedScroll={() => scrollMemory.current}
                      onScrollTop={(top) => (scrollMemory.current = top)}
                      onResize={resizeOcc}
                      {...shared}
                    />
                  )}
                </motion.div>
              </AnimatePresence>
            </main>
          </div>
        </div>

        {/* Mobile create */}
        <motion.button
          type="button"
          onClick={createAtNextSlot}
          aria-label="New event"
          whileTap={{ scale: 0.92 }}
          className={cn(
            "absolute bottom-5 right-5 z-30 grid size-14 place-items-center rounded-2xl bg-primary text-primary-foreground shadow-xl shadow-primary/30 sm:hidden",
            focusRing,
            (edit || popover) && "hidden",
          )}
        >
          <Plus className="size-6" />
        </motion.button>

        {/* Drawer */}
        <AnimatePresence>
          {drawer && (
            <>
              <motion.div
                aria-hidden
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                onClick={() => setDrawer(false)}
                className="absolute inset-0 z-30 bg-black/25 lg:hidden dark:bg-black/50"
              />
              <motion.aside
                aria-label="Calendars"
                initial={{ x: "-100%" }}
                animate={{ x: 0 }}
                exit={{ x: "-100%" }}
                transition={{ type: "spring", stiffness: 420, damping: 40 }}
                className="absolute inset-y-0 left-0 z-40 flex w-72 max-w-[85%] flex-col border-r bg-background shadow-2xl lg:hidden"
              >
                <div className="flex h-14 shrink-0 items-center gap-2.5 px-5">
                  <Logo />
                  <span className="flex-1 text-[15px] font-semibold tracking-tight">{appName}</span>
                  <IconButton label="Close calendars" onClick={() => setDrawer(false)}>
                    <X className="size-4" />
                  </IconButton>
                </div>
                <div className="min-h-0 flex-1">{sidebar}</div>
              </motion.aside>
            </>
          )}
        </AnimatePresence>

        <EventPopover
          occ={popover?.occ ?? null}
          anchor={popover?.anchor ?? null}
          bounds={bounds}
          sheet={compact}
          calendar={popover ? calById[popover.occ.event.calendarId] : undefined}
          onClose={() => setPopover(null)}
          onEdit={() => popover && editOcc(popover.occ)}
          onDelete={() => popover && remove(popover.occ.event)}
          onDuplicate={() => popover && duplicate(popover.occ)}
        />

        <EventDialog
          target={edit}
          calendars={calendars}
          onCancel={() => {
            setEdit(null);
            setDraft(null);
          }}
          onSave={save}
          onDelete={remove}
          onDraftChange={setDraft}
        />

        {/* Toasts */}
        <div aria-live="polite" className="pointer-events-none absolute inset-x-0 bottom-4 z-[60] flex flex-col items-center gap-2 px-4">
          <AnimatePresence initial={false}>
            {toasts.map((t) => (
              <motion.div
                key={t.id}
                layout
                initial={{ opacity: 0, y: 24, scale: 0.9 }}
                animate={{ opacity: 1, y: 0, scale: 1 }}
                exit={{ opacity: 0, y: 12, scale: 0.95, transition: { duration: 0.15 } }}
                transition={{ type: "spring", stiffness: 500, damping: 32 }}
                role="status"
                className="pointer-events-auto relative flex min-w-64 max-w-full items-center gap-2.5 overflow-hidden rounded-xl bg-foreground py-2.5 pl-3 pr-2 text-[13px] text-background shadow-xl shadow-black/20"
              >
                <CheckCircle2 className="size-4 shrink-0 text-emerald-400 dark:text-emerald-600" aria-hidden />
                <span className="min-w-0 flex-1 truncate font-medium">{t.text}</span>
                {t.undo && (
                  <button
                    type="button"
                    onClick={() => {
                      t.undo?.();
                      dismiss(t.id);
                    }}
                    className="h-7 rounded-md px-2 text-xs font-semibold text-background/90 outline-none hover:bg-background/10 focus-visible:ring-2 focus-visible:ring-background/60"
                  >
                    Undo
                  </button>
                )}
                <button
                  type="button"
                  aria-label="Dismiss"
                  onClick={() => dismiss(t.id)}
                  className="grid size-6 place-items-center rounded-md text-background/60 outline-none hover:bg-background/10 hover:text-background focus-visible:ring-2 focus-visible:ring-background/60"
                >
                  <X className="size-3.5" />
                </button>
                <motion.span aria-hidden className="absolute bottom-0 left-0 h-0.5 bg-background/30" initial={{ width: "100%" }} animate={{ width: "0%" }} transition={{ duration: 5, ease: "linear" }} />
              </motion.div>
            ))}
          </AnimatePresence>
        </div>
      </div>
    </MotionConfig>
  );
}

function Logo() {
  return (
    <span aria-hidden className="relative grid size-8 place-items-center overflow-hidden rounded-xl bg-gradient-to-br from-violet-500 via-indigo-500 to-sky-500 text-white shadow-md shadow-indigo-500/25">
      <CalendarDays className="size-4" />
    </span>
  );
}

export default CalendarApp;

More in Productivity

View all →