{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "event-calendar-dashboard",
  "title": "Event Calendar Dashboard",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-svelte/blocks/event-calendar-dashboard/EventCalendar.svelte",
      "content": "<!--\n  EventCalendar -- a full month-view event calendar with stats strip,\n  drag/shift range select, side rail (selected day or range summary),\n  upcoming list, and per-event/per-cell context menus.\n\n  Configurable surface:\n    - events         CalendarEvent[]   the source of truth (bindable)\n    - eventTypes     Record<typeKey, EventTypeMeta>  visual config per type\n    - initialDate    YYYY-MM-DD        which month opens first\n    - weekStartsOn   0 | 1             Sunday vs Monday\n    - loading                          skeleton state during fetch\n    - title / description / show-* toggles per pane\n\n  Snippets:\n    - headerActions     extra controls to the right of search/view/new\n    - eventDialog       full override of the detail dialog body ({ event })\n    - empty             text shown when a selected day has no events\n\n  Callbacks:\n    - onEventClick(event)\n    - onEventCreate(dateKey)            requested from \"New event\" or\n                                        the per-cell context menu\n    - onEventEdit / onEventDuplicate / onEventDelete(event)\n    - onRangeChange({ lo, hi })         fires when the selection range moves\n-->\n<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { CalendarEvent, EventTypeMeta } from './types'\n\n  export interface EventCalendarProps {\n    /** Source of truth. Pair with bind:events. */\n    events?: CalendarEvent[]\n    /** Visual + semantic theme per event type. Falls back to defaultEventTypes. */\n    eventTypes?: Record<string, EventTypeMeta>\n    /** YYYY-MM-DD -- month to open first. Defaults to today. */\n    initialDate?: string\n    /** 0 = Sunday (default), 1 = Monday. */\n    weekStartsOn?: 0 | 1\n    /** Show the skeleton placeholders instead of cells / rail rows. */\n    loading?: boolean\n    title?: string\n    description?: string\n    /** Default = true. Stats strip across the top. */\n    showStats?: boolean\n    /** Default = true. Side rail to the right of the grid. */\n    showSideRail?: boolean\n    /** Default = true. Search input in the header. */\n    showSearch?: boolean\n    /** Default = true. Month/Week/Day select. Week/Day not implemented yet\n     *  -- the select is purely visual until you wire up the modes. */\n    showViewSelect?: boolean\n    /** Default = true. Legend strip under the grid. */\n    showLegend?: boolean\n    /** Default = true. \"Up next\" pane under the side rail. */\n    showUpcoming?: boolean\n    headerActions?: Snippet\n    eventDialog?: Snippet<[{ event: CalendarEvent }]>\n    empty?: Snippet\n    onEventClick?: (event: CalendarEvent) => void\n    onEventCreate?: (dateKey: string) => void\n    onEventEdit?: (event: CalendarEvent) => void\n    onEventDuplicate?: (event: CalendarEvent) => void\n    onEventDelete?: (event: CalendarEvent) => void\n    onRangeChange?: (bounds: { lo: string; hi: string }) => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { onDestroy, onMount } from 'svelte'\n  import {\n    ChevronLeft,\n    ChevronRight,\n    Plus,\n    Clock,\n    MapPin,\n    Users,\n    Search,\n    CalendarDays,\n    ListFilter,\n    Copy,\n    X,\n    CalendarPlus,\n    ArrowRight,\n    MousePointer2,\n    Pencil,\n    Trash2,\n    Eye,\n    CopyPlus,\n  } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { useMonthGrid, dateFromKey } from '$lib/composables/useMonthGrid.svelte'\n  import { Button } from '$lib/components/ui/button'\n  import {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n  } from '$lib/components/ui/dialog'\n  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'\n  import { Avatar, AvatarFallback } from '$lib/components/ui/avatar'\n  import { Separator } from '$lib/components/ui/separator'\n  import { Skeleton } from '$lib/components/ui/skeleton'\n  import { Input } from '$lib/components/ui/input'\n  import {\n    ContextMenu,\n    ContextMenuContent,\n    ContextMenuItem,\n    ContextMenuSeparator,\n    ContextMenuShortcut,\n    ContextMenuTrigger,\n    ContextMenuLabel,\n  } from '$lib/components/ui/context-menu'\n  import { defaultEventTypes } from './defaults'\n\n  let {\n    events = $bindable([]),\n    eventTypes = defaultEventTypes,\n    initialDate,\n    weekStartsOn = 0,\n    loading = false,\n    title = 'Calendar',\n    description = 'Schedule, meetings, and deadlines. Drag or shift-click to select a range.',\n    showStats = true,\n    showSideRail = true,\n    showSearch = true,\n    showViewSelect = true,\n    showLegend = true,\n    showUpcoming = true,\n    headerActions,\n    eventDialog,\n    empty,\n    onEventClick,\n    onEventCreate,\n    onEventEdit,\n    onEventDuplicate,\n    onEventDelete,\n    onRangeChange,\n  }: EventCalendarProps = $props()\n\n  let view = $state<'month' | 'week' | 'day'>('month')\n  let eventOpen = $state(false)\n  let selectedEvent = $state<CalendarEvent | null>(null)\n  let search = $state('')\n\n  // svelte-ignore state_referenced_locally -- intentionally initial-only; the grid owns the cursor after mount.\n  const grid = useMonthGrid({ initialDate, weekStartsOn })\n  const todayKey = grid.todayKey\n\n  // Matches Vue `watch` (no immediate): skip the first run so consumers only\n  // hear about actual range moves.\n  let rangeInit = false\n  $effect(() => {\n    const b = grid.rangeBounds\n    if (!rangeInit) {\n      rangeInit = true\n      return\n    }\n    onRangeChange?.(b)\n  })\n\n  // Fall through to a neutral style for unknown event types so the calendar\n  // doesn't crash when a consumer passes a type that's missing from the map.\n  const neutralMeta: EventTypeMeta = {\n    label: 'Event',\n    icon: CalendarDays,\n    chip: 'bg-muted text-muted-foreground ring-muted-foreground/20',\n    bar: 'bg-muted-foreground',\n    dot: 'bg-muted-foreground',\n    glow: 'bg-muted-foreground/10',\n    text: 'text-muted-foreground',\n    ring: 'hover:ring-muted-foreground/30',\n  }\n  function metaFor(type: string): EventTypeMeta {\n    return eventTypes[type] ?? neutralMeta\n  }\n\n  const typeKeys = $derived(Object.keys(eventTypes))\n\n  const filtered = $derived.by(() => {\n    const q = search.trim().toLowerCase()\n    if (!q) return events\n    return events.filter(\n      (e) =>\n        e.title.toLowerCase().includes(q) ||\n        (e.description ?? '').toLowerCase().includes(q) ||\n        (e.location ?? '').toLowerCase().includes(q),\n    )\n  })\n\n  const eventsByDate = $derived.by(() => {\n    const map = new Map<string, CalendarEvent[]>()\n    for (const e of filtered) {\n      if (!map.has(e.date)) map.set(e.date, [])\n      map.get(e.date)!.push(e)\n    }\n    for (const arr of map.values()) arr.sort((a, b) => a.start.localeCompare(b.start))\n    return map\n  })\n\n  const monthCounts = $derived.by(() => {\n    const y = grid.cursor.getFullYear()\n    const m = grid.cursor.getMonth()\n    const out: Record<string, number> & { total: number } = { total: 0 }\n    for (const k of typeKeys) out[k] = 0\n    for (const e of events) {\n      const d = new Date(e.date)\n      if (d.getFullYear() === y && d.getMonth() === m) {\n        if (out[e.type] !== undefined) out[e.type]!++\n        out.total++\n      }\n    }\n    return out\n  })\n\n  const typeStats = $derived.by(() => {\n    const y = grid.cursor.getFullYear()\n    const m = grid.cursor.getMonth()\n    const out: Record<string, { weeks: number[]; next: CalendarEvent | null }> = {}\n    for (const k of typeKeys) out[k] = { weeks: [0, 0, 0, 0, 0, 0], next: null }\n    const firstOffset = new Date(y, m, 1).getDay()\n\n    for (const e of events) {\n      const d = new Date(e.date)\n      if (d.getFullYear() === y && d.getMonth() === m && out[e.type]) {\n        const week = Math.floor((d.getDate() - 1 + firstOffset) / 7)\n        out[e.type]!.weeks[week]!++\n      }\n    }\n    for (const t of typeKeys) {\n      const next =\n        events\n          .filter((e) => e.type === t && e.date >= todayKey)\n          .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))[0] ?? null\n      out[t]!.next = next\n    }\n    return out\n  })\n\n  function fmtNextDate(key: string) {\n    if (key === todayKey) return 'Today'\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  const rangeEvents = $derived.by(() => {\n    const { lo, hi } = grid.rangeBounds\n    return filtered\n      .filter((e) => e.date >= lo && e.date <= hi)\n      .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))\n  })\n\n  const rangeTypeCounts = $derived.by(() => {\n    const init: Record<string, number> = {}\n    for (const k of typeKeys) init[k] = 0\n    for (const e of rangeEvents) {\n      if (init[e.type] !== undefined) init[e.type]!++\n    }\n    return init\n  })\n\n  const selectedDayEvents = $derived(eventsByDate.get(grid.rangeStart) ?? [])\n\n  const upcoming = $derived(\n    filtered\n      .filter((e) => e.date > todayKey)\n      .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))\n      .slice(0, 4),\n  )\n\n  function openEvent(e: CalendarEvent) {\n    selectedEvent = e\n    eventOpen = true\n    onEventClick?.(e)\n  }\n\n  function copyDate(key: string) {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) {\n      navigator.clipboard.writeText(key).catch(() => {\n        /* clipboard may be denied; nothing to do */\n      })\n    }\n  }\n\n  function fmtDayLong(key: string) {\n    return dateFromKey(key).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })\n  }\n  function fmtDayShort(key: string) {\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  function initials(name: string) {\n    return name\n      .split(' ')\n      .map((n) => n[0])\n      .join('')\n      .slice(0, 2)\n  }\n\n  function cellRangeClass(key: string, inMonth: boolean) {\n    const { lo, hi } = grid.rangeBounds\n    if (!grid.inRange(key)) return inMonth ? 'bg-background hover:bg-accent/40' : 'bg-muted/20 hover:bg-muted/30'\n    if (key === lo && key === hi) return 'bg-accent/30 ring-1 ring-inset ring-primary/60'\n    let cls = 'bg-primary/10 hover:bg-primary/15'\n    if (key === lo) cls += ' ring-1 ring-inset ring-primary/60'\n    if (key === hi) cls += ' ring-1 ring-inset ring-primary/60'\n    return cls\n  }\n\n  function selectCategoryFilter(key: string) {\n    const label = eventTypes[key]?.label.toLowerCase() || ''\n    search = label === search.toLowerCase() ? '' : label\n  }\n\n  function handleEditSelected() {\n    if (selectedEvent) onEventEdit?.(selectedEvent)\n    eventOpen = false\n  }\n\n  function handleDeleteSelected() {\n    if (selectedEvent) onEventDelete?.(selectedEvent)\n    eventOpen = false\n  }\n\n  // Window listeners so the drag also ends when the user releases the mouse\n  // outside the grid surface.\n  onMount(() => {\n    if (typeof window !== 'undefined') {\n      window.addEventListener('mouseup', grid.endDrag)\n      window.addEventListener('mouseleave', grid.endDrag)\n    }\n  })\n  onDestroy(() => {\n    if (typeof window !== 'undefined') {\n      window.removeEventListener('mouseup', grid.endDrag)\n      window.removeEventListener('mouseleave', grid.endDrag)\n    }\n  })\n</script>\n\n<div data-slot=\"event-calendar\" class=\"flex flex-col gap-5\" onmouseup={grid.endDrag}>\n  <!-- Header -->\n  <header class=\"flex flex-wrap items-end justify-between gap-4\">\n    <div>\n      <h1 class=\"text-2xl font-semibold tracking-tight\">{title}</h1>\n      {#if description}\n        <p class=\"text-muted-foreground mt-1 text-sm\">{description}</p>\n      {/if}\n    </div>\n    <div class=\"flex items-center gap-2\">\n      {#if showSearch}\n        <div class=\"relative\">\n          <Search class=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\" />\n          <Input bind:value={search} placeholder=\"Search events\" class=\"h-8 w-56 pl-8 text-xs\" />\n        </div>\n      {/if}\n      {#if showViewSelect}\n        <Select value={view} onValueChange={(v) => (view = v as typeof view)}>\n          <SelectTrigger class=\"h-8 w-24 text-xs\">\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectItem value=\"month\">Month</SelectItem>\n            <SelectItem value=\"week\">Week</SelectItem>\n            <SelectItem value=\"day\">Day</SelectItem>\n          </SelectContent>\n        </Select>\n      {/if}\n      {#if headerActions}\n        {@render headerActions()}\n      {:else}\n        <Button size=\"sm\" class=\"gap-1.5\" onclick={() => onEventCreate?.(grid.rangeStart)}>\n          <Plus class=\"size-3.5\" />\n          New event\n        </Button>\n      {/if}\n    </div>\n  </header>\n\n  <!-- Stats strip -->\n  {#if showStats}\n    <div class={`grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-${Math.min(typeKeys.length, 4)}`}>\n      {#each typeKeys as key (key)}\n        {@const meta = eventTypes[key]!}\n        {@const Icon = meta.icon}\n        <button\n          type=\"button\"\n          class={cn(\n            'group bg-card hover:bg-muted/40 relative isolate flex flex-col overflow-hidden rounded-xl border p-4 text-left ring-1 ring-transparent transition-[colors,box-shadow,transform] duration-150 ring-inset hover:shadow-xs active:scale-[0.98]',\n            meta.ring,\n          )}\n          onclick={() => selectCategoryFilter(key)}\n        >\n          <Icon\n            class={cn(\n              'pointer-events-none absolute -top-4 -right-4 size-24 opacity-[0.06] transition-opacity group-hover:opacity-[0.12]',\n              meta.text,\n            )}\n          />\n\n          <div class=\"flex items-center justify-between gap-2\">\n            <div class=\"flex items-center gap-1.5\">\n              <span class={cn('size-1.5 rounded-full', meta.dot)}></span>\n              <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">\n                {meta.label}\n              </p>\n            </div>\n            <div class={cn('flex size-7 items-center justify-center rounded-md ring-1 ring-inset', meta.chip)}>\n              <Icon class=\"size-3.5\" />\n            </div>\n          </div>\n\n          <div class=\"mt-2 flex items-baseline gap-1.5\">\n            <p class=\"text-3xl leading-none font-semibold tabular-nums\">{monthCounts[key] ?? 0}</p>\n            <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">this month</p>\n          </div>\n\n          <div class=\"mt-3 flex h-6 items-end gap-1\">\n            {#each typeStats[key]!.weeks.slice(0, 6) as n, i (i)}\n              <span\n                class={cn(\n                  'flex-1 rounded-sm transition-colors',\n                  n > 0 ? meta.bar : 'bg-muted/40',\n                  n > 0 && 'opacity-70 group-hover:opacity-100',\n                )}\n                style:height={n > 0 ? `${Math.min(100, 30 + n * 35)}%` : '20%'}\n                title={`Week ${i + 1}: ${n} ${meta.label.toLowerCase()}${n === 1 ? '' : 's'}`}\n              ></span>\n            {/each}\n          </div>\n\n          <div class=\"border-border/50 mt-3 min-h-[2.25rem] border-t pt-2\">\n            {#if typeStats[key]!.next}\n              <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">\n                Next <span class={meta.text}>{fmtNextDate(typeStats[key]!.next!.date)}</span>\n              </p>\n              <p class=\"mt-0.5 truncate text-xs font-medium\">{typeStats[key]!.next!.title}</p>\n            {:else}\n              <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">No upcoming</p>\n              <p class=\"text-muted-foreground/70 mt-0.5 text-xs\">All clear</p>\n            {/if}\n          </div>\n        </button>\n      {/each}\n    </div>\n  {/if}\n\n  <!-- Main: calendar + side rail -->\n  <div class={cn('grid gap-4', showSideRail && 'lg:grid-cols-[minmax(0,1fr)_320px]')}>\n    <!-- Month grid -->\n    <div class=\"bg-card/40 overflow-hidden rounded-xl border\">\n      <div class=\"bg-muted/30 flex flex-wrap items-center justify-between gap-3 border-b px-4 py-2.5\">\n        <div class=\"flex items-center gap-2\">\n          <Button aria-label=\"Previous month\" variant=\"outline\" size=\"icon\" class=\"size-7\" onclick={() => grid.prevMonth()}\n            ><ChevronLeft class=\"size-4\" /></Button\n          >\n          <Button aria-label=\"Next month\" variant=\"outline\" size=\"icon\" class=\"size-7\" onclick={() => grid.nextMonth()}\n            ><ChevronRight class=\"size-4\" /></Button\n          >\n          <Button variant=\"ghost\" size=\"sm\" class=\"h-7 text-xs\" onclick={() => grid.goToToday()}>Today</Button>\n          <h2 class=\"ml-2 text-sm font-semibold\">{grid.monthLabel}</h2>\n        </div>\n        <div class=\"text-muted-foreground flex items-center gap-3 text-xs\">\n          {#if grid.isRange}\n            <div\n              class=\"bg-primary/10 text-primary ring-primary/20 flex items-center gap-1.5 rounded-full px-2 py-0.5 ring-1 ring-inset\"\n            >\n              <MousePointer2 class=\"size-3\" />\n              <span>{grid.rangeDayCount} days, {rangeEvents.length} events</span>\n              <button class=\"hover:text-foreground ml-0.5\" aria-label=\"Clear date range\" onclick={() => grid.clearRange()}>\n                <X class=\"size-3\" />\n              </button>\n            </div>\n          {/if}\n          <div class=\"flex items-center gap-1.5\">\n            <CalendarDays class=\"size-3\" />\n            <span>{monthCounts.total} event{monthCounts.total === 1 ? '' : 's'} this month</span>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"bg-muted/10 text-muted-foreground grid grid-cols-7 border-b text-xs tracking-wider uppercase\">\n        {#each grid.weekdays as w (w)}\n          <div class=\"px-2 py-2 font-medium\">{w}</div>\n        {/each}\n      </div>\n\n      <div class=\"grid grid-cols-7 select-none\">\n        {#if loading}\n          {#each Array.from({ length: 42 }, (_, i) => i) as i (i)}\n            <div class=\"h-24 border-r border-b p-1.5 last:border-r-0\">\n              <Skeleton class=\"h-3 w-6\" />\n              <Skeleton class=\"mt-2 h-3 w-full\" />\n            </div>\n          {/each}\n        {:else}\n          {#each grid.gridDays as d, i (d.key)}\n            <ContextMenu>\n              <!-- display:contents keeps the button as the effective grid item. -->\n              <ContextMenuTrigger class=\"contents\">\n                <button\n                  type=\"button\"\n                  class={cn(\n                    'group focus-visible:ring-ring relative flex h-24 flex-col gap-1 border-r border-b p-1.5 text-left transition-colors focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none',\n                    (i + 1) % 7 === 0 && 'border-r-0',\n                    i >= 35 && 'border-b-0',\n                    cellRangeClass(d.key, d.inMonth),\n                    !d.inMonth && !grid.inRange(d.key) && 'text-muted-foreground/60',\n                  )}\n                  onmousedown={(e) => grid.onCellMouseDown(d.key, e)}\n                  onmouseenter={() => grid.onCellMouseEnter(d.key)}\n                >\n                  <div class=\"flex items-center justify-between\">\n                    <span\n                      class={cn(\n                        'inline-flex size-5 items-center justify-center rounded-full text-xs tabular-nums',\n                        d.key === todayKey && 'bg-primary text-primary-foreground font-semibold',\n                        d.key !== todayKey && d.inMonth && 'text-foreground',\n                        !d.inMonth && 'text-muted-foreground/60',\n                      )}>{d.date.getDate()}</span\n                    >\n                    {#if (eventsByDate.get(d.key)?.length ?? 0) > 0}\n                      <span class=\"text-muted-foreground text-xs tabular-nums\">\n                        {eventsByDate.get(d.key)!.length}\n                      </span>\n                    {/if}\n                  </div>\n                  <div class=\"flex flex-col gap-0.5\">\n                    {#each (eventsByDate.get(d.key) ?? []).slice(0, 2) as e (e.id)}\n                      <ContextMenu>\n                        <ContextMenuTrigger class=\"contents\">\n                          <span\n                            class={cn(\n                              'flex cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-xs ring-1 ring-inset',\n                              metaFor(e.type).chip,\n                            )}\n                            onmousedown={(ev) => ev.stopPropagation()}\n                            onclick={(ev) => {\n                              ev.stopPropagation()\n                              openEvent(e)\n                            }}\n                          >\n                            <!-- No contextmenu stopPropagation here: the inner\n                                 trigger must receive the bubbled event to open.\n                                 It calls preventDefault, which tells the outer\n                                 cell trigger to stand down. -->\n                            <span class=\"tabular-nums opacity-70\">{e.start}</span>\n                            <span class=\"truncate\">{e.title}</span>\n                          </span>\n                        </ContextMenuTrigger>\n                        <ContextMenuContent class=\"w-52\">\n                          <ContextMenuLabel class=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n                            <span class={cn('size-1.5 rounded-full', metaFor(e.type).dot)}></span>\n                            <span class=\"truncate\">{e.title}</span>\n                          </ContextMenuLabel>\n                          <ContextMenuSeparator />\n                          <ContextMenuItem class=\"gap-2\" onSelect={() => openEvent(e)}>\n                            <Eye class=\"size-3.5\" /> View details\n                            <ContextMenuShortcut>Enter</ContextMenuShortcut>\n                          </ContextMenuItem>\n                          <ContextMenuItem class=\"gap-2\" onSelect={() => onEventEdit?.(e)}\n                            ><Pencil class=\"size-3.5\" /> Edit</ContextMenuItem\n                          >\n                          <ContextMenuItem class=\"gap-2\" onSelect={() => onEventDuplicate?.(e)}\n                            ><CopyPlus class=\"size-3.5\" /> Duplicate</ContextMenuItem\n                          >\n                          <ContextMenuItem class=\"gap-2\" onSelect={() => copyDate(e.date)}>\n                            <Copy class=\"size-3.5\" /> Copy date\n                          </ContextMenuItem>\n                          <ContextMenuSeparator />\n                          <ContextMenuItem\n                            class=\"text-destructive focus:text-destructive gap-2\"\n                            onSelect={() => onEventDelete?.(e)}\n                          >\n                            <Trash2 class=\"size-3.5\" /> Cancel event\n                          </ContextMenuItem>\n                        </ContextMenuContent>\n                      </ContextMenu>\n                    {/each}\n                    {#if (eventsByDate.get(d.key)?.length ?? 0) > 2}\n                      <span class=\"text-muted-foreground px-1 text-xs\">\n                        +{eventsByDate.get(d.key)!.length - 2} more\n                      </span>\n                    {/if}\n                  </div>\n                </button>\n              </ContextMenuTrigger>\n              <ContextMenuContent class=\"w-56\">\n                <ContextMenuLabel class=\"text-muted-foreground text-xs\">{fmtDayLong(d.key)}</ContextMenuLabel>\n                <ContextMenuSeparator />\n                <ContextMenuItem class=\"gap-2\" onSelect={() => onEventCreate?.(d.key)}>\n                  <CalendarPlus class=\"size-3.5\" /> New event\n                  <ContextMenuShortcut>N</ContextMenuShortcut>\n                </ContextMenuItem>\n                <ContextMenuItem class=\"gap-2\" onSelect={() => grid.selectWeekOf(d.key)}>\n                  <CalendarDays class=\"size-3.5\" /> Select this week\n                </ContextMenuItem>\n                <ContextMenuSeparator />\n                <ContextMenuItem class=\"gap-2\" onSelect={() => copyDate(d.key)}>\n                  <Copy class=\"size-3.5\" /> Copy date\n                  <ContextMenuShortcut class=\"tabular-nums\">{d.key}</ContextMenuShortcut>\n                </ContextMenuItem>\n                <ContextMenuItem class=\"gap-2\" onSelect={() => grid.goToToday()}>\n                  <ArrowRight class=\"size-3.5\" /> Go to today\n                </ContextMenuItem>\n                {#if grid.isRange}\n                  <ContextMenuItem\n                    class=\"text-destructive focus:text-destructive gap-2\"\n                    onSelect={() => grid.clearRange()}\n                  >\n                    <X class=\"size-3.5\" /> Clear range\n                  </ContextMenuItem>\n                {/if}\n              </ContextMenuContent>\n            </ContextMenu>\n          {/each}\n        {/if}\n      </div>\n\n      {#if showLegend}\n        <div\n          class=\"bg-muted/20 text-muted-foreground flex flex-wrap items-center gap-3 border-t px-4 py-2 text-xs\"\n        >\n          <ListFilter class=\"size-3\" />\n          {#each typeKeys as key (key)}\n            <div class=\"flex items-center gap-1.5\">\n              <span class={cn('size-2 rounded-full', eventTypes[key]!.dot)}></span>\n              {eventTypes[key]!.label}\n            </div>\n          {/each}\n          <span class=\"ml-auto text-xs\">Tip: drag or shift-click to range. Right-click for actions.</span>\n        </div>\n      {/if}\n    </div>\n\n    <!-- Side rail -->\n    {#if showSideRail}\n      <aside class=\"flex flex-col gap-4\">\n        {#if !grid.isRange}\n          <div class=\"bg-card/40 overflow-hidden rounded-xl border\">\n            <div class=\"bg-muted/30 border-b px-4 py-3\">\n              <div class=\"flex items-center justify-between\">\n                <div>\n                  <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">\n                    {grid.rangeStart === todayKey ? 'Today' : 'Selected'}\n                  </p>\n                  <p class=\"mt-0.5 text-sm font-semibold\">{fmtDayLong(grid.rangeStart)}</p>\n                </div>\n                <div class=\"text-right\">\n                  <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">Events</p>\n                  <p class=\"text-sm font-semibold tabular-nums\">{selectedDayEvents.length}</p>\n                </div>\n              </div>\n            </div>\n            <div class=\"max-h-[26.25rem] overflow-y-auto p-3\">\n              {#if loading}\n                {#each [0, 1, 2] as i (i)}\n                  <div class=\"mb-2 space-y-2 rounded-lg border p-3\">\n                    <Skeleton class=\"h-3 w-32\" />\n                    <Skeleton class=\"h-2 w-20\" />\n                  </div>\n                {/each}\n              {:else if selectedDayEvents.length === 0}\n                {#if empty}\n                  {@render empty()}\n                {:else}\n                  <div class=\"flex flex-col items-center justify-center gap-2 py-8 text-center\">\n                    <div class=\"bg-muted flex size-10 items-center justify-center rounded-full\">\n                      <CalendarDays class=\"text-muted-foreground size-5\" />\n                    </div>\n                    <p class=\"text-sm font-medium\">Nothing scheduled</p>\n                    <p class=\"text-muted-foreground text-xs\">Click a date or add a new event.</p>\n                    <Button\n                      size=\"sm\"\n                      variant=\"outline\"\n                      class=\"mt-1 h-7 gap-1.5 text-xs\"\n                      onclick={() => onEventCreate?.(grid.rangeStart)}\n                    >\n                      <Plus class=\"size-3\" /> New event\n                    </Button>\n                  </div>\n                {/if}\n              {:else}\n                {#each selectedDayEvents as e (e.id)}\n                  <ContextMenu>\n                    <ContextMenuTrigger class=\"contents\">\n                      <div\n                        class=\"group hover:bg-accent/50 relative flex cursor-pointer gap-3 rounded-lg p-2.5 transition-colors\"\n                        onclick={() => openEvent(e)}\n                      >\n                        <div class={cn('w-0.5 shrink-0 rounded-full', metaFor(e.type).bar)}></div>\n                        <div class=\"min-w-0 flex-1 space-y-1\">\n                          <div class=\"flex items-start justify-between gap-2\">\n                            <p class=\"text-sm leading-tight font-medium\">{e.title}</p>\n                            <span\n                              class={cn(\n                                'shrink-0 rounded px-1.5 py-0.5 text-xs tracking-wider uppercase ring-1 ring-inset',\n                                metaFor(e.type).chip,\n                              )}\n                            >\n                              {metaFor(e.type).label}\n                            </span>\n                          </div>\n                          <div class=\"text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs\">\n                            <span class=\"inline-flex items-center gap-1\"\n                              ><Clock class=\"size-3\" />{e.start}-{e.end}</span\n                            >\n                            {#if e.location}\n                              <span class=\"inline-flex items-center gap-1\"\n                                ><MapPin class=\"size-3\" />{e.location}</span\n                              >\n                            {/if}\n                          </div>\n                          {#if e.attendees?.length}\n                            <div class=\"flex items-center -space-x-1.5 pt-0.5\">\n                              {#each e.attendees.slice(0, 4) as a, i (i)}\n                                <Avatar class=\"border-background size-5 border-2\">\n                                  <AvatarFallback class=\"bg-primary/10 text-primary text-xs\"\n                                    >{initials(a)}</AvatarFallback\n                                  >\n                                </Avatar>\n                              {/each}\n                              {#if e.attendees.length > 4}\n                                <span class=\"text-muted-foreground pl-2 text-xs\">+{e.attendees.length - 4}</span>\n                              {/if}\n                            </div>\n                          {/if}\n                        </div>\n                      </div>\n                    </ContextMenuTrigger>\n                    <ContextMenuContent class=\"w-48\">\n                      <ContextMenuLabel class=\"text-muted-foreground truncate text-xs\">{e.title}</ContextMenuLabel>\n                      <ContextMenuSeparator />\n                      <ContextMenuItem class=\"gap-2\" onSelect={() => openEvent(e)}\n                        ><Eye class=\"size-3.5\" /> View details</ContextMenuItem\n                      >\n                      <ContextMenuItem class=\"gap-2\" onSelect={() => onEventEdit?.(e)}\n                        ><Pencil class=\"size-3.5\" /> Edit</ContextMenuItem\n                      >\n                      <ContextMenuItem class=\"gap-2\" onSelect={() => onEventDuplicate?.(e)}\n                        ><CopyPlus class=\"size-3.5\" /> Duplicate</ContextMenuItem\n                      >\n                      <ContextMenuSeparator />\n                      <ContextMenuItem\n                        class=\"text-destructive focus:text-destructive gap-2\"\n                        onSelect={() => onEventDelete?.(e)}><Trash2 class=\"size-3.5\" /> Cancel event</ContextMenuItem\n                      >\n                    </ContextMenuContent>\n                  </ContextMenu>\n                {/each}\n              {/if}\n            </div>\n          </div>\n        {:else}\n          <div class=\"bg-card/40 overflow-hidden rounded-xl border\">\n            <div class=\"from-primary/10 border-b bg-gradient-to-r to-transparent px-4 py-3\">\n              <div class=\"flex items-center justify-between gap-3\">\n                <div class=\"min-w-0\">\n                  <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">\n                    Range, {grid.rangeDayCount} days\n                  </p>\n                  <p class=\"mt-0.5 truncate text-sm font-semibold\">\n                    {fmtDayShort(grid.rangeBounds.lo)} -> {fmtDayShort(grid.rangeBounds.hi)}\n                  </p>\n                </div>\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  class=\"size-7\"\n                  aria-label=\"Clear date range\"\n                  onclick={() => grid.clearRange()}><X class=\"size-3.5\" /></Button\n                >\n              </div>\n              <div class={`mt-3 grid gap-2 grid-cols-${Math.min(typeKeys.length, 4)}`}>\n                {#each typeKeys as key (key)}\n                  <div class=\"bg-background/40 rounded-md border px-2 py-1.5\">\n                    <div class=\"text-muted-foreground flex items-center gap-1 text-xs tracking-wider uppercase\">\n                      <span class={cn('size-1.5 rounded-full', eventTypes[key]!.dot)}></span>\n                      {eventTypes[key]!.label}\n                    </div>\n                    <p class=\"mt-0.5 text-sm font-semibold tabular-nums\">{rangeTypeCounts[key] ?? 0}</p>\n                  </div>\n                {/each}\n              </div>\n            </div>\n            <div class=\"max-h-[26.25rem] overflow-y-auto\">\n              {#if rangeEvents.length === 0}\n                <p class=\"text-muted-foreground px-4 py-8 text-center text-xs\">No events in range.</p>\n              {:else}\n                {#each rangeEvents as e (e.id)}\n                  <div\n                    class=\"hover:bg-accent/40 flex cursor-pointer items-start gap-3 border-b px-3 py-2.5 transition-colors last:border-b-0\"\n                    onclick={() => openEvent(e)}\n                  >\n                    <div\n                      class=\"bg-background/60 flex w-10 shrink-0 flex-col items-center rounded-md border px-1 py-1 text-center\"\n                    >\n                      <span class=\"text-muted-foreground text-xs uppercase\">\n                        {new Date(e.date).toLocaleDateString('en-US', { month: 'short' })}\n                      </span>\n                      <span class=\"text-sm leading-none font-semibold tabular-nums\">{new Date(e.date).getDate()}</span>\n                    </div>\n                    <div class={cn('w-0.5 shrink-0 self-stretch rounded-full', metaFor(e.type).bar)}></div>\n                    <div class=\"min-w-0 flex-1\">\n                      <p class=\"truncate text-xs font-medium\">{e.title}</p>\n                      <p class=\"text-muted-foreground mt-0.5 text-xs tabular-nums\">\n                        {e.start}-{e.end}{#if e.location} {e.location}{/if}\n                      </p>\n                    </div>\n                    <span\n                      class={cn(\n                        'shrink-0 rounded px-1.5 py-0.5 text-xs tracking-wider uppercase ring-1 ring-inset',\n                        metaFor(e.type).chip,\n                      )}\n                    >\n                      {metaFor(e.type).label}\n                    </span>\n                  </div>\n                {/each}\n              {/if}\n            </div>\n          </div>\n        {/if}\n\n        {#if showUpcoming}\n          <div class=\"bg-card/40 overflow-hidden rounded-xl border\">\n            <div class=\"bg-muted/30 border-b px-4 py-2.5\">\n              <p class=\"text-sm font-semibold\">Up next</p>\n              <p class=\"text-muted-foreground text-xs\">After today</p>\n            </div>\n            <div class=\"divide-y\">\n              {#if loading}\n                {#each [0, 1, 2] as i (i)}\n                  <div class=\"flex items-center gap-3 p-3\">\n                    <Skeleton class=\"size-9 rounded-md\" />\n                    <div class=\"flex-1 space-y-1\">\n                      <Skeleton class=\"h-3 w-32\" />\n                      <Skeleton class=\"h-2 w-20\" />\n                    </div>\n                  </div>\n                {/each}\n              {:else if upcoming.length === 0}\n                <p class=\"text-muted-foreground px-4 py-6 text-center text-xs\">Nothing on the horizon.</p>\n              {:else}\n                {#each upcoming as e (e.id)}\n                  <button\n                    type=\"button\"\n                    class=\"group hover:bg-accent/40 flex w-full items-center gap-3 p-3 text-left transition-colors\"\n                    onclick={() => openEvent(e)}\n                  >\n                    <div\n                      class=\"bg-background flex size-10 shrink-0 flex-col items-center justify-center rounded-md border text-center\"\n                    >\n                      <span class=\"text-muted-foreground text-xs uppercase\">\n                        {new Date(e.date).toLocaleDateString('en-US', { month: 'short' })}\n                      </span>\n                      <span class=\"text-sm leading-none font-semibold tabular-nums\">{new Date(e.date).getDate()}</span>\n                    </div>\n                    <div class=\"min-w-0 flex-1\">\n                      <div class=\"flex items-center gap-1.5\">\n                        <span class={cn('size-1.5 rounded-full', metaFor(e.type).dot)}></span>\n                        <p class=\"truncate text-xs font-medium\">{e.title}</p>\n                      </div>\n                      <p class=\"text-muted-foreground mt-0.5 text-xs tabular-nums\">\n                        {e.start}-{e.end}{#if e.location} {e.location}{/if}\n                      </p>\n                    </div>\n                  </button>\n                {/each}\n              {/if}\n            </div>\n          </div>\n        {/if}\n      </aside>\n    {/if}\n  </div>\n\n  <!-- Event Detail Dialog -->\n  <Dialog bind:open={eventOpen}>\n    {#if selectedEvent}\n      {@const event = selectedEvent}\n      {@const dialogMeta = metaFor(event.type)}\n      {@const DialogIcon = dialogMeta.icon}\n      <DialogContent class=\"sm:max-w-md\">\n        {#if eventDialog}\n          {@render eventDialog({ event })}\n        {:else}\n          <DialogHeader>\n            <div class=\"flex items-center gap-2\">\n              <div class={cn('flex size-8 items-center justify-center rounded-md ring-1 ring-inset', dialogMeta.chip)}>\n                <DialogIcon class=\"size-4\" />\n              </div>\n              <div>\n                <DialogTitle class=\"text-base\">{event.title}</DialogTitle>\n                <DialogDescription class=\"text-xs\">\n                  {dialogMeta.label}{#if event.status} {event.status}{/if}\n                </DialogDescription>\n              </div>\n            </div>\n          </DialogHeader>\n          <div class=\"space-y-3 py-2\">\n            {#if event.description}\n              <p class=\"text-muted-foreground text-sm\">\n                {event.description}\n              </p>\n            {/if}\n            <Separator />\n            <div class=\"grid gap-2 text-sm\">\n              <div class=\"flex items-center gap-2\">\n                <Clock class=\"text-muted-foreground size-4\" />\n                <span>{event.date} {event.start} - {event.end}</span>\n              </div>\n              {#if event.location}\n                <div class=\"flex items-center gap-2\">\n                  <MapPin class=\"text-muted-foreground size-4\" />\n                  <span>{event.location}</span>\n                </div>\n              {/if}\n              {#if event.attendees?.length}\n                <div class=\"flex items-start gap-2\">\n                  <Users class=\"text-muted-foreground mt-0.5 size-4\" />\n                  <div class=\"flex flex-wrap items-center gap-1.5\">\n                    {#each event.attendees as a (a)}\n                      <div class=\"bg-muted flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs\">\n                        <Avatar class=\"size-4\">\n                          <AvatarFallback class=\"bg-primary/10 text-primary text-xs\">{initials(a)}</AvatarFallback>\n                        </Avatar>\n                        <span>{a}</span>\n                      </div>\n                    {/each}\n                  </div>\n                </div>\n              {/if}\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" size=\"sm\" onclick={handleEditSelected}>Edit</Button>\n            <Button size=\"sm\" variant=\"destructive\" onclick={handleDeleteSelected}>Delete</Button>\n          </DialogFooter>\n        {/if}\n      </DialogContent>\n    {/if}\n  </Dialog>\n</div>\n",
      "type": "registry:block",
      "target": "~/src/lib/components/blocks/event-calendar-dashboard/EventCalendar.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/event-calendar-dashboard/defaults.ts",
      "content": "import { Video, CheckCircle2, AlertCircle, Plane } from '@lucide/svelte'\nimport type { EventTypeMeta } from './types'\n\n/**\n * Default event-type theme. Mirrors the boilerplate dashboard but routes\n * every color through OKLCH tokens (chart-1..4, success, warning) so it\n * follows light/dark mode and any consumer theme override.\n *\n * Override per-type or replace the whole map via `<EventCalendar eventTypes={...} />`.\n */\nexport const defaultEventTypes: Record<string, EventTypeMeta> = {\n  meeting: {\n    label: 'Meeting',\n    icon: Video,\n    chip: 'bg-chart-1/10 text-chart-1 ring-chart-1/20',\n    bar: 'bg-chart-1',\n    dot: 'bg-chart-1',\n    glow: 'bg-chart-1/10',\n    text: 'text-chart-1',\n    ring: 'hover:ring-chart-1/30',\n  },\n  task: {\n    label: 'Task',\n    icon: CheckCircle2,\n    chip: 'bg-success/10 text-success ring-success/20',\n    bar: 'bg-success',\n    dot: 'bg-success',\n    glow: 'bg-success/10',\n    text: 'text-success',\n    ring: 'hover:ring-success/30',\n  },\n  reminder: {\n    label: 'Reminder',\n    icon: AlertCircle,\n    chip: 'bg-warning/10 text-warning ring-warning/20',\n    bar: 'bg-warning',\n    dot: 'bg-warning',\n    glow: 'bg-warning/10',\n    text: 'text-warning',\n    ring: 'hover:ring-warning/30',\n  },\n  travel: {\n    label: 'Travel',\n    icon: Plane,\n    chip: 'bg-chart-3/10 text-chart-3 ring-chart-3/20',\n    bar: 'bg-chart-3',\n    dot: 'bg-chart-3',\n    glow: 'bg-chart-3/10',\n    text: 'text-chart-3',\n    ring: 'hover:ring-chart-3/30',\n  },\n}\n",
      "type": "registry:block",
      "target": "~/src/lib/components/blocks/event-calendar-dashboard/defaults.ts"
    },
    {
      "path": "packages/registry-svelte/blocks/event-calendar-dashboard/types.ts",
      "content": "import type { Component } from 'svelte'\n\n/**\n * Single event on the calendar. `type` is a free-form string (e.g. `'meeting'`,\n * `'task'`, anything) -- the matching key must exist in the `eventTypes` map\n * passed to <EventCalendar>. Unknown types fall through to a neutral style.\n */\nexport interface CalendarEvent {\n  id: string\n  title: string\n  /** YYYY-MM-DD */\n  date: string\n  /** HH:mm */\n  start: string\n  /** HH:mm */\n  end: string\n  type: string\n  description?: string\n  location?: string\n  attendees?: string[]\n  /** Free-form status tag rendered next to the type label in the dialog. */\n  status?: string\n}\n\n/**\n * Visual + semantic config per event type. The default theme uses OKLCH\n * tokens (chart-1..4, success, warning) so consumers get sensible colors\n * out of the box, but you can override per-type to wire any palette.\n */\nexport interface EventTypeMeta {\n  label: string\n  /** Lucide icon component for the stat tile + chip + dialog header. */\n  icon: Component\n  /** Tailwind classes for the colored chip on event rows + dialog header.\n   *  Shape: `bg-X/10 text-X ring-X/20` is what the default theme uses. */\n  chip: string\n  /** Tailwind class for the vertical bar to the left of side-rail rows. */\n  bar: string\n  /** Tailwind class for the legend / stat-tile dot. */\n  dot: string\n  /** Tailwind class for the soft watermark glow behind stat tiles. */\n  glow: string\n  /** Tailwind class for the small text accent on \"Next: ...\" date labels. */\n  text: string\n  /** Tailwind class for the stat-tile hover ring. */\n  ring: string\n}\n",
      "type": "registry:block",
      "target": "~/src/lib/components/blocks/event-calendar-dashboard/types.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/use-month-grid.json",
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/dialog.json",
    "https://uipkge.dev/r/svelte/select.json",
    "https://uipkge.dev/r/svelte/avatar.json",
    "https://uipkge.dev/r/svelte/separator.json",
    "https://uipkge.dev/r/svelte/skeleton.json",
    "https://uipkge.dev/r/svelte/input.json",
    "https://uipkge.dev/r/svelte/context-menu.json"
  ],
  "description": "Month-view event calendar with stats strip, drag/shift range select, side rail (selected day or range summary), upcoming list, and per-cell/per-event context menus. bind:events holds the source of truth; eventTypes config maps each type key to its label/icon/color theme so swapping the visual palette is one prop. Built on the use-month-grid headless hook.",
  "categories": [
    "productivity",
    "dashboard"
  ]
}