{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "event-calendar",
  "title": "Event Calendar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/event-calendar/EventCalendar.vue",
      "content": "<script setup lang=\"ts\">\n/**\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 *   - v-model:events  CalendarEvent[]   the source of truth\n *   - :event-types    Record<typeKey, EventTypeMeta>  visual config per type\n *   - :initial-date   YYYY-MM-DD        which month opens first\n *   - :week-starts-on 0 | 1              Sunday vs Monday\n *   - :loading                          skeleton state during fetch\n *   - :title / :description / :show-* toggles per pane\n *\n * Slots:\n *   - #header-actions     extra controls to the right of search/view/new\n *   - #event-dialog       full override of the detail dialog body\n *   - #empty              text shown when a selected day has no events\n *\n * Emits:\n *   - event-click(event)\n *   - event-create(dateKey)             requested from \"New event\" or\n *                                       the per-cell context menu\n *   - event-edit / event-duplicate / event-delete(event)\n *   - range-change({ lo, hi })          fires when the selection range moves\n */\nimport { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'\nimport {\n  ChevronLeft,\n  ChevronRight,\n  Plus,\n  Clock,\n  MapPin,\n  Users,\n  Search,\n  CalendarDays,\n  ListFilter,\n  Sparkles,\n  Copy,\n  X,\n  CalendarPlus,\n  ArrowRight,\n  MousePointer2,\n  Pencil,\n  Trash2,\n  Eye,\n  CopyPlus,\n} from 'lucide-vue-next'\nimport { useMonthGrid, dateFromKey } from '@/composables/useMonthGrid'\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar'\nimport { Separator } from '@/components/ui/separator'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport { Input } from '@/components/ui/input'\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n  ContextMenuLabel,\n} from '@/components/ui/context-menu'\nimport type { CalendarEvent, EventTypeMeta } from './types'\nimport { defaultEventTypes } from './defaults'\n\nconst props = withDefaults(\n  defineProps<{\n    /** Source of truth. Pair with v-model: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  }>(),\n  {\n    events: () => [],\n    eventTypes: () => defaultEventTypes,\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  },\n)\n\nconst emit = defineEmits<{\n  'update:events': [value: CalendarEvent[]]\n  'event-click': [event: CalendarEvent]\n  'event-create': [dateKey: string]\n  'event-edit': [event: CalendarEvent]\n  'event-duplicate': [event: CalendarEvent]\n  'event-delete': [event: CalendarEvent]\n  'range-change': [bounds: { lo: string; hi: string }]\n}>()\n\nconst view = ref<'month' | 'week' | 'day'>('month')\nconst eventOpen = ref(false)\nconst selectedEvent = ref<CalendarEvent | null>(null)\nconst search = ref('')\n\nconst {\n  todayKey,\n  cursor,\n  monthLabel,\n  gridDays,\n  weekdays,\n  rangeStart,\n  rangeBounds,\n  rangeDayCount,\n  isRange,\n  inRange,\n  prevMonth,\n  nextMonth,\n  goToToday,\n  selectWeekOf,\n  clearRange,\n  onCellMouseDown,\n  onCellMouseEnter,\n  endDrag,\n} = useMonthGrid({ initialDate: props.initialDate, weekStartsOn: props.weekStartsOn })\n\nwatch(rangeBounds, (b) => emit('range-change', b))\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.\nconst 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}\nfunction metaFor(type: string): EventTypeMeta {\n  return props.eventTypes[type] ?? neutralMeta\n}\n\nconst typeKeys = computed(() => Object.keys(props.eventTypes))\n\nconst filtered = computed(() => {\n  const q = search.value.trim().toLowerCase()\n  if (!q) return props.events\n  return props.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\nconst eventsByDate = computed(() => {\n  const map = new Map<string, CalendarEvent[]>()\n  for (const e of filtered.value) {\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\nconst monthCounts = computed(() => {\n  const y = cursor.value.getFullYear()\n  const m = cursor.value.getMonth()\n  const out: Record<string, number> & { total: number } = { total: 0 }\n  for (const k of typeKeys.value) out[k] = 0\n  for (const e of props.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\nconst typeStats = computed(() => {\n  const y = cursor.value.getFullYear()\n  const m = cursor.value.getMonth()\n  const out: Record<string, { weeks: number[]; next: CalendarEvent | null }> = {}\n  for (const k of typeKeys.value) 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 props.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.value) {\n    const next = props.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]\n    out[t]!.next = next ?? null\n  }\n  return out\n})\n\nfunction fmtNextDate(key: string) {\n  if (key === todayKey) return 'Today'\n  return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n}\n\nconst rangeEvents = computed(() => {\n  const { lo, hi } = rangeBounds.value\n  return filtered.value\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\nconst rangeTypeCounts = computed(() => {\n  const init: Record<string, number> = {}\n  for (const k of typeKeys.value) init[k] = 0\n  for (const e of rangeEvents.value) {\n    if (init[e.type] !== undefined) init[e.type]!++\n  }\n  return init\n})\n\nconst selectedDayEvents = computed(() => eventsByDate.value.get(rangeStart.value) ?? [])\n\nconst upcoming = computed(() =>\n  filtered.value\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\nfunction openEvent(e: CalendarEvent) {\n  selectedEvent.value = e\n  eventOpen.value = true\n  emit('event-click', e)\n}\n\nfunction 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\nfunction fmtDayLong(key: string) {\n  return dateFromKey(key).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })\n}\nfunction fmtDayShort(key: string) {\n  return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n}\n\nfunction initials(name: string) {\n  return name\n    .split(' ')\n    .map((n) => n[0])\n    .join('')\n    .slice(0, 2)\n}\n\nfunction cellRangeClass(key: string, inMonth: boolean) {\n  const { lo, hi } = rangeBounds.value\n  if (!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// Window listeners are also used inside useMonthGrid for mouseup; mirror\n// them at the block level so the drag also ends when the user releases\n// the mouse outside the grid surface.\nonMounted(() => {\n  if (typeof window !== 'undefined') {\n    window.addEventListener('mouseup', endDrag)\n    window.addEventListener('mouseleave', endDrag)\n  }\n})\nonBeforeUnmount(() => {\n  if (typeof window !== 'undefined') {\n    window.removeEventListener('mouseup', endDrag)\n    window.removeEventListener('mouseleave', endDrag)\n  }\n})\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-5\" @mouseup=\"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        <p v-if=\"description\" class=\"text-muted-foreground mt-1 text-sm\">{{ description }}</p>\n      </div>\n      <div class=\"flex items-center gap-2\">\n        <div v-if=\"showSearch\" class=\"relative\">\n          <Search class=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\" />\n          <Input v-model=\"search\" placeholder=\"Search events\" class=\"h-8 w-56 pl-8 text-xs\" />\n        </div>\n        <Select v-if=\"showViewSelect\" v-model=\"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        <slot name=\"header-actions\">\n          <Button size=\"sm\" class=\"gap-1.5\" @click=\"emit('event-create', rangeStart)\">\n            <Plus class=\"size-3.5\" />\n            New event\n          </Button>\n        </slot>\n      </div>\n    </header>\n\n    <!-- Stats strip -->\n    <div\n      v-if=\"showStats\"\n      class=\"grid grid-cols-1 gap-3\"\n      :class=\"`sm:grid-cols-2 lg:grid-cols-${Math.min(typeKeys.length, 4)}`\"\n    >\n      <button\n        v-for=\"key in typeKeys\"\n        :key=\"key\"\n        type=\"button\"\n        :class=\"[\n          'group bg-card/40 hover:bg-card relative isolate flex flex-col overflow-hidden rounded-xl border p-4 text-left ring-1 ring-transparent backdrop-blur transition-all ring-inset hover:-translate-y-px hover:shadow-lg',\n          eventTypes[key]!.ring,\n        ]\"\n        @click=\"\n          search =\n            eventTypes[key]!.label.toLowerCase() === search.toLowerCase() ? '' : eventTypes[key]!.label.toLowerCase()\n        \"\n      >\n        <component\n          :is=\"eventTypes[key]!.icon\"\n          :class=\"[\n            'pointer-events-none absolute -top-4 -right-4 size-24 opacity-[0.06] transition-opacity group-hover:opacity-[0.12]',\n            eventTypes[key]!.text,\n          ]\"\n        />\n        <div\n          :class=\"[\n            'pointer-events-none absolute -right-12 -bottom-12 size-32 rounded-full blur-2xl',\n            eventTypes[key]!.glow,\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=\"['size-1.5 rounded-full', eventTypes[key]!.dot]\" />\n            <p class=\"text-muted-foreground text-[10px] font-medium tracking-[0.14em] uppercase\">\n              {{ eventTypes[key]!.label }}\n            </p>\n          </div>\n          <div :class=\"['flex size-7 items-center justify-center rounded-md ring-1 ring-inset', eventTypes[key]!.chip]\">\n            <component :is=\"eventTypes[key]!.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-[10px] tracking-wider uppercase\">this month</p>\n        </div>\n\n        <div class=\"mt-3 flex h-6 items-end gap-1\">\n          <span\n            v-for=\"(n, i) in typeStats[key]!.weeks.slice(0, 6)\"\n            :key=\"i\"\n            :class=\"[\n              'flex-1 rounded-sm transition-colors',\n              n > 0 ? eventTypes[key]!.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} ${eventTypes[key]!.label.toLowerCase()}${n === 1 ? '' : 's'}`\"\n          />\n        </div>\n\n        <div class=\"border-border/50 mt-3 min-h-[2.25rem] border-t pt-2\">\n          <template v-if=\"typeStats[key]!.next\">\n            <p class=\"text-muted-foreground text-[10px] tracking-wider uppercase\">\n              Next <span :class=\"eventTypes[key]!.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          </template>\n          <template v-else>\n            <p class=\"text-muted-foreground text-[10px] tracking-wider uppercase\">No upcoming</p>\n            <p class=\"text-muted-foreground/70 mt-0.5 text-xs\">All clear</p>\n          </template>\n        </div>\n      </button>\n    </div>\n\n    <!-- Main: calendar + side rail -->\n    <div :class=\"['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 variant=\"outline\" size=\"icon\" class=\"size-7\" @click=\"prevMonth\"\n              ><ChevronLeft class=\"size-4\"\n            /></Button>\n            <Button variant=\"outline\" size=\"icon\" class=\"size-7\" @click=\"nextMonth\"\n              ><ChevronRight class=\"size-4\"\n            /></Button>\n            <Button variant=\"ghost\" size=\"sm\" class=\"h-7 text-xs\" @click=\"goToToday\">Today</Button>\n            <h2 class=\"ml-2 text-sm font-semibold\">{{ monthLabel }}</h2>\n          </div>\n          <div class=\"text-muted-foreground flex items-center gap-3 text-[11px]\">\n            <div\n              v-if=\"isRange\"\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>{{ rangeDayCount }} days, {{ rangeEvents.length }} events</span>\n              <button class=\"hover:text-foreground ml-0.5\" @click=\"clearRange\"><X class=\"size-3\" /></button>\n            </div>\n            <div class=\"flex items-center gap-1.5\">\n              <Sparkles 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-[10px] tracking-wider uppercase\">\n          <div v-for=\"w in weekdays\" :key=\"w\" class=\"px-2 py-2 font-medium\">{{ w }}</div>\n        </div>\n\n        <div class=\"grid grid-cols-7 select-none\">\n          <template v-if=\"loading\">\n            <div v-for=\"i in 42\" :key=\"i\" 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          </template>\n          <template v-else>\n            <ContextMenu v-for=\"(d, i) in gridDays\" :key=\"d.key\">\n              <ContextMenuTrigger as-child>\n                <button\n                  type=\"button\"\n                  :class=\"[\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 && !inRange(d.key) && 'text-muted-foreground/60',\n                  ]\"\n                  @mousedown=\"onCellMouseDown(d.key, $event)\"\n                  @mouseenter=\"onCellMouseEnter(d.key)\"\n                >\n                  <div class=\"flex items-center justify-between\">\n                    <span\n                      :class=\"[\n                        'inline-flex size-5 items-center justify-center rounded-full text-[11px] 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                      ]\"\n                      >{{ d.date.getDate() }}</span\n                    >\n                    <span\n                      v-if=\"(eventsByDate.get(d.key)?.length ?? 0) > 0\"\n                      class=\"text-muted-foreground text-[9px] tabular-nums\"\n                    >\n                      {{ eventsByDate.get(d.key)!.length }}\n                    </span>\n                  </div>\n                  <div class=\"flex flex-col gap-0.5\">\n                    <ContextMenu v-for=\"e in (eventsByDate.get(d.key) ?? []).slice(0, 2)\" :key=\"e.id\">\n                      <ContextMenuTrigger as-child>\n                        <span\n                          :class=\"[\n                            'flex cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-[10px] ring-1 ring-inset',\n                            metaFor(e.type).chip,\n                          ]\"\n                          @mousedown.stop\n                          @click.stop=\"openEvent(e)\"\n                          @contextmenu.stop\n                        >\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-[11px]\">\n                          <span :class=\"['size-1.5 rounded-full', metaFor(e.type).dot]\" />\n                          <span class=\"truncate\">{{ e.title }}</span>\n                        </ContextMenuLabel>\n                        <ContextMenuSeparator />\n                        <ContextMenuItem class=\"gap-2\" @select=\"openEvent(e)\">\n                          <Eye class=\"size-3.5\" /> View details\n                          <ContextMenuShortcut>Enter</ContextMenuShortcut>\n                        </ContextMenuItem>\n                        <ContextMenuItem class=\"gap-2\" @select=\"emit('event-edit', e)\"\n                          ><Pencil class=\"size-3.5\" /> Edit</ContextMenuItem\n                        >\n                        <ContextMenuItem class=\"gap-2\" @select=\"emit('event-duplicate', e)\"\n                          ><CopyPlus class=\"size-3.5\" /> Duplicate</ContextMenuItem\n                        >\n                        <ContextMenuItem class=\"gap-2\" @select=\"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                          @select=\"emit('event-delete', e)\"\n                        >\n                          <Trash2 class=\"size-3.5\" /> Cancel event\n                        </ContextMenuItem>\n                      </ContextMenuContent>\n                    </ContextMenu>\n                    <span\n                      v-if=\"(eventsByDate.get(d.key)?.length ?? 0) > 2\"\n                      class=\"text-muted-foreground px-1 text-[10px]\"\n                    >\n                      +{{ eventsByDate.get(d.key)!.length - 2 }} more\n                    </span>\n                  </div>\n                </button>\n              </ContextMenuTrigger>\n              <ContextMenuContent class=\"w-56\">\n                <ContextMenuLabel class=\"text-muted-foreground text-[11px]\">{{ fmtDayLong(d.key) }}</ContextMenuLabel>\n                <ContextMenuSeparator />\n                <ContextMenuItem class=\"gap-2\" @select=\"emit('event-create', d.key)\">\n                  <CalendarPlus class=\"size-3.5\" /> New event\n                  <ContextMenuShortcut>N</ContextMenuShortcut>\n                </ContextMenuItem>\n                <ContextMenuItem class=\"gap-2\" @select=\"selectWeekOf(d.key)\">\n                  <CalendarDays class=\"size-3.5\" /> Select this week\n                </ContextMenuItem>\n                <ContextMenuSeparator />\n                <ContextMenuItem class=\"gap-2\" @select=\"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\" @select=\"goToToday\">\n                  <ArrowRight class=\"size-3.5\" /> Go to today\n                </ContextMenuItem>\n                <ContextMenuItem\n                  v-if=\"isRange\"\n                  class=\"text-destructive focus:text-destructive gap-2\"\n                  @select=\"clearRange\"\n                >\n                  <X class=\"size-3.5\" /> Clear range\n                </ContextMenuItem>\n              </ContextMenuContent>\n            </ContextMenu>\n          </template>\n        </div>\n\n        <div\n          v-if=\"showLegend\"\n          class=\"bg-muted/20 text-muted-foreground flex flex-wrap items-center gap-3 border-t px-4 py-2 text-[10px]\"\n        >\n          <ListFilter class=\"size-3\" />\n          <div v-for=\"key in typeKeys\" :key=\"key\" class=\"flex items-center gap-1.5\">\n            <span :class=\"['size-2 rounded-full', eventTypes[key]!.dot]\" />\n            {{ eventTypes[key]!.label }}\n          </div>\n          <span class=\"ml-auto text-[10px]\">Tip: drag or shift-click to range. Right-click for actions.</span>\n        </div>\n      </div>\n\n      <!-- Side rail -->\n      <aside v-if=\"showSideRail\" class=\"flex flex-col gap-4\">\n        <div v-if=\"!isRange\" 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-[10px] tracking-wider uppercase\">\n                  {{ rangeStart === todayKey ? 'Today' : 'Selected' }}\n                </p>\n                <p class=\"mt-0.5 text-sm font-semibold\">{{ fmtDayLong(rangeStart) }}</p>\n              </div>\n              <div class=\"text-right\">\n                <p class=\"text-muted-foreground text-[10px] 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-[420px] overflow-y-auto p-3\">\n            <template v-if=\"loading\">\n              <div v-for=\"i in 3\" :key=\"i\" 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            </template>\n            <template v-else-if=\"selectedDayEvents.length === 0\">\n              <slot name=\"empty\">\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                    @click=\"emit('event-create', rangeStart)\"\n                  >\n                    <Plus class=\"size-3\" /> New event\n                  </Button>\n                </div>\n              </slot>\n            </template>\n            <template v-else>\n              <ContextMenu v-for=\"e in selectedDayEvents\" :key=\"e.id\">\n                <ContextMenuTrigger as-child>\n                  <div\n                    class=\"group hover:bg-accent/50 relative flex cursor-pointer gap-3 rounded-lg p-2.5 transition-colors\"\n                    @click=\"openEvent(e)\"\n                  >\n                    <div :class=\"['w-0.5 shrink-0 rounded-full', metaFor(e.type).bar]\" />\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=\"[\n                            'shrink-0 rounded px-1.5 py-0.5 text-[9px] 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-[11px]\">\n                        <span class=\"inline-flex items-center gap-1\"\n                          ><Clock class=\"size-3\" />{{ e.start }}-{{ e.end }}</span\n                        >\n                        <span v-if=\"e.location\" class=\"inline-flex items-center gap-1\"\n                          ><MapPin class=\"size-3\" />{{ e.location }}</span\n                        >\n                      </div>\n                      <div v-if=\"e.attendees?.length\" class=\"flex items-center -space-x-1.5 pt-0.5\">\n                        <Avatar\n                          v-for=\"(a, i) in e.attendees.slice(0, 4)\"\n                          :key=\"i\"\n                          class=\"border-background size-5 border-2\"\n                        >\n                          <AvatarFallback class=\"bg-primary/10 text-primary text-[8px]\">{{\n                            initials(a)\n                          }}</AvatarFallback>\n                        </Avatar>\n                        <span v-if=\"e.attendees.length > 4\" class=\"text-muted-foreground pl-2 text-[10px]\"\n                          >+{{ e.attendees.length - 4 }}</span\n                        >\n                      </div>\n                    </div>\n                  </div>\n                </ContextMenuTrigger>\n                <ContextMenuContent class=\"w-48\">\n                  <ContextMenuLabel class=\"text-muted-foreground truncate text-[11px]\">{{ e.title }}</ContextMenuLabel>\n                  <ContextMenuSeparator />\n                  <ContextMenuItem class=\"gap-2\" @select=\"openEvent(e)\"\n                    ><Eye class=\"size-3.5\" /> View details</ContextMenuItem\n                  >\n                  <ContextMenuItem class=\"gap-2\" @select=\"emit('event-edit', e)\"\n                    ><Pencil class=\"size-3.5\" /> Edit</ContextMenuItem\n                  >\n                  <ContextMenuItem class=\"gap-2\" @select=\"emit('event-duplicate', e)\"\n                    ><CopyPlus class=\"size-3.5\" /> Duplicate</ContextMenuItem\n                  >\n                  <ContextMenuSeparator />\n                  <ContextMenuItem\n                    class=\"text-destructive focus:text-destructive gap-2\"\n                    @select=\"emit('event-delete', e)\"\n                    ><Trash2 class=\"size-3.5\" /> Cancel event</ContextMenuItem\n                  >\n                </ContextMenuContent>\n              </ContextMenu>\n            </template>\n          </div>\n        </div>\n\n        <div v-else 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-[10px] tracking-wider uppercase\">\n                  Range, {{ rangeDayCount }} days\n                </p>\n                <p class=\"mt-0.5 truncate text-sm font-semibold\">\n                  {{ fmtDayShort(rangeBounds.lo) }} -> {{ fmtDayShort(rangeBounds.hi) }}\n                </p>\n              </div>\n              <Button variant=\"ghost\" size=\"icon\" class=\"size-7\" @click=\"clearRange\"><X class=\"size-3.5\" /></Button>\n            </div>\n            <div class=\"mt-3 grid gap-2\" :class=\"`grid-cols-${Math.min(typeKeys.length, 4)}`\">\n              <div v-for=\"key in typeKeys\" :key=\"key\" class=\"bg-background/40 rounded-md border px-2 py-1.5\">\n                <div class=\"text-muted-foreground flex items-center gap-1 text-[9px] tracking-wider uppercase\">\n                  <span :class=\"['size-1.5 rounded-full', eventTypes[key]!.dot]\" />\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            </div>\n          </div>\n          <div class=\"max-h-[420px] overflow-y-auto\">\n            <template v-if=\"rangeEvents.length === 0\">\n              <p class=\"text-muted-foreground px-4 py-8 text-center text-xs\">No events in range.</p>\n            </template>\n            <template v-else>\n              <div\n                v-for=\"e in rangeEvents\"\n                :key=\"e.id\"\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                @click=\"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-[8px] 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=\"['w-0.5 shrink-0 self-stretch rounded-full', metaFor(e.type).bar]\" />\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-[11px] tabular-nums\">\n                    {{ e.start }}-{{ e.end }}<span v-if=\"e.location\"> {{ ' ' }} {{ e.location }}</span>\n                  </p>\n                </div>\n                <span\n                  :class=\"[\n                    'shrink-0 rounded px-1.5 py-0.5 text-[9px] tracking-wider uppercase ring-1 ring-inset',\n                    metaFor(e.type).chip,\n                  ]\"\n                >\n                  {{ metaFor(e.type).label }}\n                </span>\n              </div>\n            </template>\n          </div>\n        </div>\n\n        <div v-if=\"showUpcoming\" 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-[11px]\">After today</p>\n          </div>\n          <div class=\"divide-y\">\n            <template v-if=\"loading\">\n              <div v-for=\"i in 3\" :key=\"i\" 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            </template>\n            <template v-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            </template>\n            <template v-else>\n              <button\n                v-for=\"e in upcoming\"\n                :key=\"e.id\"\n                type=\"button\"\n                class=\"group hover:bg-accent/40 flex w-full items-center gap-3 p-3 text-left transition-colors\"\n                @click=\"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-[9px] 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=\"['size-1.5 rounded-full', metaFor(e.type).dot]\" />\n                    <p class=\"truncate text-xs font-medium\">{{ e.title }}</p>\n                  </div>\n                  <p class=\"text-muted-foreground mt-0.5 text-[11px] tabular-nums\">\n                    {{ e.start }}-{{ e.end }}<span v-if=\"e.location\"> {{ ' ' }} {{ e.location }}</span>\n                  </p>\n                </div>\n              </button>\n            </template>\n          </div>\n        </div>\n      </aside>\n    </div>\n\n    <!-- Event Detail Dialog -->\n    <Dialog v-model:open=\"eventOpen\">\n      <DialogContent v-if=\"selectedEvent\" class=\"sm:max-w-md\">\n        <slot name=\"event-dialog\" :event=\"selectedEvent\">\n          <DialogHeader>\n            <div class=\"flex items-center gap-2\">\n              <div\n                :class=\"[\n                  'flex size-8 items-center justify-center rounded-md ring-1 ring-inset',\n                  metaFor(selectedEvent.type).chip,\n                ]\"\n              >\n                <component :is=\"metaFor(selectedEvent.type).icon\" class=\"size-4\" />\n              </div>\n              <div>\n                <DialogTitle class=\"text-base\">{{ selectedEvent.title }}</DialogTitle>\n                <DialogDescription class=\"text-xs\">\n                  {{ metaFor(selectedEvent.type).label\n                  }}<span v-if=\"selectedEvent.status\"> {{ ' ' }} {{ selectedEvent.status }}</span>\n                </DialogDescription>\n              </div>\n            </div>\n          </DialogHeader>\n          <div class=\"space-y-3 py-2\">\n            <p v-if=\"selectedEvent.description\" class=\"text-muted-foreground text-sm\">\n              {{ selectedEvent.description }}\n            </p>\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>{{ selectedEvent.date }} {{ ' ' }} {{ selectedEvent.start }} - {{ selectedEvent.end }}</span>\n              </div>\n              <div v-if=\"selectedEvent.location\" class=\"flex items-center gap-2\">\n                <MapPin class=\"text-muted-foreground size-4\" />\n                <span>{{ selectedEvent.location }}</span>\n              </div>\n              <div v-if=\"selectedEvent.attendees?.length\" 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                  <div\n                    v-for=\"a in selectedEvent.attendees\"\n                    :key=\"a\"\n                    class=\"bg-muted flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs\"\n                  >\n                    <Avatar class=\"size-4\">\n                      <AvatarFallback class=\"bg-primary/10 text-primary text-[8px]\">{{ initials(a) }}</AvatarFallback>\n                    </Avatar>\n                    <span>{{ a }}</span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              @click=\"\n                () => {\n                  emit('event-edit', selectedEvent)\n                  eventOpen = false\n                }\n              \"\n              >Edit</Button\n            >\n            <Button\n              size=\"sm\"\n              variant=\"destructive\"\n              @click=\"\n                () => {\n                  emit('event-delete', selectedEvent)\n                  eventOpen = false\n                }\n              \"\n              >Cancel event</Button\n            >\n          </DialogFooter>\n        </slot>\n      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/event-calendar/EventCalendar.vue"
    },
    {
      "path": "packages/registry-vue/blocks/event-calendar/defaults.ts",
      "content": "import { Video, CheckCircle2, AlertCircle, Plane } from 'lucide-vue-next'\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 :event-types=\"...\" />`.\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": "~/app/components/blocks/event-calendar/defaults.ts"
    },
    {
      "path": "packages/registry-vue/blocks/event-calendar/types.ts",
      "content": "import type { Component } from 'vue'\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": "~/app/components/blocks/event-calendar/types.ts"
    },
    {
      "path": "packages/registry-vue/blocks/event-calendar/index.ts",
      "content": "export { default as EventCalendar } from './EventCalendar.vue'\nexport type { CalendarEvent, EventTypeMeta } from './types'\nexport { defaultEventTypes } from './defaults'\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/event-calendar/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/use-month-grid.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/dialog.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/avatar.json",
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/skeleton.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/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. v-model:events binds 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 composable.",
  "categories": [
    "dashboard"
  ]
}