{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "event-calendar-dashboard",
  "title": "Event Calendar Dashboard",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/event-calendar-dashboard/event-calendar-dashboard.component.ts",
      "content": "// EventCalendar -- a full month-view event calendar with stats strip, drag/shift range\n// select, side rail (selected day or range summary), upcoming list, and per-event/per-cell\n// context menus. Port of the Vue/React block 1:1 -- same month-grid + range-select behavior\n// (inlined here; Angular has no useMonthGrid composable), same stats math, same copy.\n//\n// Configurable surface:\n//   - [events] / (eventsChange)  CalendarEvent[]   the source of truth\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// Emits: eventClick(event), eventCreate(dateKey), eventEdit / eventDuplicate /\n// eventDelete(event), rangeChange({ lo, hi }).\nimport {\n  ChangeDetectionStrategy,\n  Component,\n  ContentChild,\n  DestroyRef,\n  EventEmitter,\n  Input,\n  OnInit,\n  Output,\n  TemplateRef,\n  booleanAttribute,\n  computed,\n  effect,\n  inject,\n  signal,\n} from '@angular/core'\nimport { NgTemplateOutlet } from '@angular/common'\nimport {\n  ArrowRight,\n  CalendarDays,\n  CalendarPlus,\n  Clock,\n  Copy,\n  CopyPlus,\n  Eye,\n  ListFilter,\n  LucideAngularModule,\n  MapPin,\n  MousePointer2,\n  Pencil,\n  Plus,\n  Search,\n  Trash2,\n  Users,\n  X,\n  ChevronLeft,\n  ChevronRight,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiContextMenuComponent,\n  UiContextMenuContentComponent,\n  UiContextMenuItemComponent,\n  UiContextMenuLabelComponent,\n  UiContextMenuSeparatorComponent,\n  UiContextMenuShortcutComponent,\n  UiContextMenuTriggerComponent,\n} from '@/ui/context-menu/context-menu.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSkeletonComponent } from '@/ui/skeleton/skeleton.component'\nimport type { CalendarEvent, EventTypeMeta } from './types'\nimport { defaultEventTypes } from './defaults'\n\nexport type { CalendarEvent, EventTypeMeta }\n\nexport type DateKey = string // YYYY-MM-DD\n\nexport function isoDate(d: Date): DateKey {\n  const y = d.getFullYear()\n  const m = String(d.getMonth() + 1).padStart(2, '0')\n  const day = String(d.getDate()).padStart(2, '0')\n  return `${y}-${m}-${day}`\n}\n\nexport function dateFromKey(k: DateKey): Date {\n  return new Date(k + 'T00:00:00')\n}\n\nexport function dayDiff(a: DateKey, b: DateKey): number {\n  return Math.round((dateFromKey(b).getTime() - dateFromKey(a).getTime()) / 86400000)\n}\n\ninterface GridDay {\n  date: Date\n  key: DateKey\n  inMonth: boolean\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-event-calendar-dashboard, [ui-event-calendar-dashboard]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    NgTemplateOutlet,\n    LucideAngularModule,\n    UiAvatarComponent,\n    UiAvatarFallbackComponent,\n    UiButtonComponent,\n    UiContextMenuComponent,\n    UiContextMenuContentComponent,\n    UiContextMenuItemComponent,\n    UiContextMenuLabelComponent,\n    UiContextMenuSeparatorComponent,\n    UiContextMenuShortcutComponent,\n    UiContextMenuTriggerComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiInputComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n    UiSkeletonComponent,\n  ],\n  template: `\n    <div data-slot=\"event-calendar\" 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          @if (description) {\n            <p class=\"text-muted-foreground mt-1 text-sm\">{{ description }}</p>\n          }\n        </div>\n        <div class=\"flex items-center gap-2\">\n          @if (showSearch) {\n            <div class=\"relative\">\n              <lucide-icon\n                [img]=\"Search\"\n                class=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\"\n              />\n              <ui-input\n                [value]=\"search()\"\n                (valueChange)=\"search.set($event)\"\n                placeholder=\"Search events\"\n                class=\"h-8 w-56 pl-8 text-xs\"\n              />\n            </div>\n          }\n          @if (showViewSelect) {\n            <ui-select [value]=\"view()\" (valueChange)=\"setView($event)\">\n              <button ui-select-trigger class=\"h-8 w-24 text-xs\">\n                <ui-select-value />\n              </button>\n              <ui-select-content>\n                <ui-select-item value=\"month\">Month</ui-select-item>\n                <ui-select-item value=\"week\">Week</ui-select-item>\n                <ui-select-item value=\"day\">Day</ui-select-item>\n              </ui-select-content>\n            </ui-select>\n          }\n          @if (headerActionsTpl) {\n            <ng-container [ngTemplateOutlet]=\"headerActionsTpl\" />\n          } @else {\n            <button ui-button size=\"sm\" class=\"gap-1.5\" (click)=\"eventCreate.emit(rangeStart())\">\n              <lucide-icon [img]=\"Plus\" class=\"size-3.5\" />\n              New event\n            </button>\n          }\n        </div>\n      </header>\n\n      <!-- Stats strip -->\n      @if (showStats) {\n        <div class=\"grid grid-cols-1 gap-3\" [class]=\"statsGridClass()\">\n          @for (key of typeKeys(); track key) {\n            <button type=\"button\" [class]=\"statTileClass(key)\" (click)=\"selectCategoryFilter(key)\">\n              <lucide-icon\n                [img]=\"meta(key).icon\"\n                [class]=\"\n                  cn(\n                    'pointer-events-none absolute -top-4 -right-4 size-24 opacity-[0.06] transition-opacity group-hover:opacity-[0.12]',\n                    meta(key).text\n                  )\n                \"\n              />\n\n              <span class=\"flex items-center justify-between gap-2\">\n                <span class=\"flex items-center gap-1.5\">\n                  <span [class]=\"cn('size-1.5 rounded-full', meta(key).dot)\"></span>\n                  <span class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">\n                    {{ meta(key).label }}\n                  </span>\n                </span>\n                <span\n                  [class]=\"cn('flex size-7 items-center justify-center rounded-md ring-1 ring-inset', meta(key).chip)\"\n                >\n                  <lucide-icon [img]=\"meta(key).icon\" class=\"size-3.5\" />\n                </span>\n              </span>\n\n              <span class=\"mt-2 flex items-baseline gap-1.5\">\n                <span class=\"text-3xl leading-none font-semibold tabular-nums\">{{ monthCounts()[key] ?? 0 }}</span>\n                <span class=\"text-muted-foreground text-xs tracking-wider uppercase\">this month</span>\n              </span>\n\n              <span class=\"mt-3 flex h-6 items-end gap-1\">\n                @for (n of typeStats()[key]!.weeks.slice(0, 6); track $index) {\n                  <span\n                    [class]=\"weekBarClass(key, n)\"\n                    [style.height]=\"n > 0 ? Math.min(100, 30 + n * 35) + '%' : '20%'\"\n                    [title]=\"\n                      'Week ' + ($index + 1) + ': ' + n + ' ' + meta(key).label.toLowerCase() + (n === 1 ? '' : 's')\n                    \"\n                  ></span>\n                }\n              </span>\n\n              <span class=\"border-border/50 mt-3 min-h-[2.25rem] border-t pt-2\">\n                @if (typeStats()[key]!.next; as next) {\n                  <span class=\"text-muted-foreground block text-xs tracking-wider uppercase\">\n                    Next <span [class]=\"meta(key).text\">{{ fmtNextDate(next.date) }}</span>\n                  </span>\n                  <span class=\"mt-0.5 block truncate text-xs font-medium\">{{ next.title }}</span>\n                } @else {\n                  <span class=\"text-muted-foreground block text-xs tracking-wider uppercase\">No upcoming</span>\n                  <span class=\"text-muted-foreground/70 mt-0.5 block text-xs\">All clear</span>\n                }\n              </span>\n            </button>\n          }\n        </div>\n      }\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\n                ui-button\n                aria-label=\"Previous month\"\n                variant=\"outline\"\n                size=\"icon\"\n                class=\"size-7\"\n                (click)=\"prevMonth()\"\n              >\n                <lucide-icon [img]=\"ChevronLeft\" class=\"size-4\" />\n              </button>\n              <button\n                ui-button\n                aria-label=\"Next month\"\n                variant=\"outline\"\n                size=\"icon\"\n                class=\"size-7\"\n                (click)=\"nextMonth()\"\n              >\n                <lucide-icon [img]=\"ChevronRight\" class=\"size-4\" />\n              </button>\n              <button ui-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-xs\">\n              @if (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                  <lucide-icon [img]=\"MousePointer2\" class=\"size-3\" />\n                  <span>{{ rangeDayCount() }} days, {{ rangeEvents().length }} events</span>\n                  <button class=\"hover:text-foreground ml-0.5\" aria-label=\"Clear date range\" (click)=\"clearRange()\">\n                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </div>\n              }\n              <div class=\"flex items-center gap-1.5\">\n                <lucide-icon [img]=\"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            @for (w of weekdays(); track w) {\n              <div class=\"px-2 py-2 font-medium\">{{ w }}</div>\n            }\n          </div>\n\n          <div class=\"grid grid-cols-7 select-none\">\n            @if (loading) {\n              @for (i of skeletonCells; track i) {\n                <div class=\"h-24 border-r border-b p-1.5 last:border-r-0\">\n                  <ui-skeleton class=\"h-3 w-6\" />\n                  <ui-skeleton class=\"mt-2 h-3 w-full\" />\n                </div>\n              }\n            } @else {\n              @for (d of gridDays(); track d.key; let i = $index) {\n                <ui-context-menu>\n                  <button\n                    ui-context-menu-trigger\n                    type=\"button\"\n                    [class]=\"cellClass(d, i)\"\n                    (mousedown)=\"onCellMouseDown(d.key, $event)\"\n                    (mouseenter)=\"onCellMouseEnter(d.key)\"\n                  >\n                    <span class=\"flex items-center justify-between\">\n                      <span [class]=\"dayNumberClass(d)\">{{ d.date.getDate() }}</span>\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                      }\n                    </span>\n                    <span class=\"flex flex-col gap-0.5\">\n                      @for (e of (eventsByDate().get(d.key) ?? []).slice(0, 2); track e.id) {\n                        <ui-context-menu>\n                          <span\n                            ui-context-menu-trigger\n                            [class]=\"\n                              cn(\n                                'flex cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-xs ring-1 ring-inset',\n                                meta(e.type).chip\n                              )\n                            \"\n                            (mousedown)=\"$event.stopPropagation()\"\n                            (click)=\"openEvent(e); $event.stopPropagation()\"\n                            (contextmenu)=\"$event.stopPropagation()\"\n                          >\n                            <span class=\"tabular-nums opacity-70\">{{ e.start }}</span>\n                            <span class=\"truncate\">{{ e.title }}</span>\n                          </span>\n                          <ui-context-menu-content class=\"w-52\">\n                            <ui-context-menu-label class=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n                              <span [class]=\"cn('size-1.5 rounded-full', meta(e.type).dot)\"></span>\n                              <span class=\"truncate\">{{ e.title }}</span>\n                            </ui-context-menu-label>\n                            <ui-context-menu-separator />\n                            <ui-context-menu-item class=\"gap-2\" (select)=\"openEvent(e)\">\n                              <lucide-icon [img]=\"Eye\" class=\"size-3.5\" /> View details\n                              <ui-context-menu-shortcut>Enter</ui-context-menu-shortcut>\n                            </ui-context-menu-item>\n                            <ui-context-menu-item class=\"gap-2\" (select)=\"eventEdit.emit(e)\">\n                              <lucide-icon [img]=\"Pencil\" class=\"size-3.5\" /> Edit\n                            </ui-context-menu-item>\n                            <ui-context-menu-item class=\"gap-2\" (select)=\"eventDuplicate.emit(e)\">\n                              <lucide-icon [img]=\"CopyPlus\" class=\"size-3.5\" /> Duplicate\n                            </ui-context-menu-item>\n                            <ui-context-menu-item class=\"gap-2\" (select)=\"copyDate(e.date)\">\n                              <lucide-icon [img]=\"Copy\" class=\"size-3.5\" /> Copy date\n                            </ui-context-menu-item>\n                            <ui-context-menu-separator />\n                            <ui-context-menu-item\n                              class=\"text-destructive focus:text-destructive gap-2\"\n                              (select)=\"eventDelete.emit(e)\"\n                            >\n                              <lucide-icon [img]=\"Trash2\" class=\"size-3.5\" /> Cancel event\n                            </ui-context-menu-item>\n                          </ui-context-menu-content>\n                        </ui-context-menu>\n                      }\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                      }\n                    </span>\n                  </button>\n                  <ui-context-menu-content class=\"w-56\">\n                    <ui-context-menu-label class=\"text-muted-foreground text-xs\">{{\n                      fmtDayLong(d.key)\n                    }}</ui-context-menu-label>\n                    <ui-context-menu-separator />\n                    <ui-context-menu-item class=\"gap-2\" (select)=\"eventCreate.emit(d.key)\">\n                      <lucide-icon [img]=\"CalendarPlus\" class=\"size-3.5\" /> New event\n                      <ui-context-menu-shortcut>N</ui-context-menu-shortcut>\n                    </ui-context-menu-item>\n                    <ui-context-menu-item class=\"gap-2\" (select)=\"selectWeekOf(d.key)\">\n                      <lucide-icon [img]=\"CalendarDays\" class=\"size-3.5\" /> Select this week\n                    </ui-context-menu-item>\n                    <ui-context-menu-separator />\n                    <ui-context-menu-item class=\"gap-2\" (select)=\"copyDate(d.key)\">\n                      <lucide-icon [img]=\"Copy\" class=\"size-3.5\" /> Copy date\n                      <ui-context-menu-shortcut class=\"tabular-nums\">{{ d.key }}</ui-context-menu-shortcut>\n                    </ui-context-menu-item>\n                    <ui-context-menu-item class=\"gap-2\" (select)=\"goToToday()\">\n                      <lucide-icon [img]=\"ArrowRight\" class=\"size-3.5\" /> Go to today\n                    </ui-context-menu-item>\n                    @if (isRange()) {\n                      <ui-context-menu-item\n                        class=\"text-destructive focus:text-destructive gap-2\"\n                        (select)=\"clearRange()\"\n                      >\n                        <lucide-icon [img]=\"X\" class=\"size-3.5\" /> Clear range\n                      </ui-context-menu-item>\n                    }\n                  </ui-context-menu-content>\n                </ui-context-menu>\n              }\n            }\n          </div>\n\n          @if (showLegend) {\n            <div class=\"bg-muted/20 text-muted-foreground flex flex-wrap items-center gap-3 border-t px-4 py-2 text-xs\">\n              <lucide-icon [img]=\"ListFilter\" class=\"size-3\" />\n              @for (key of typeKeys(); track key) {\n                <div class=\"flex items-center gap-1.5\">\n                  <span [class]=\"cn('size-2 rounded-full', meta(key).dot)\"></span>\n                  {{ meta(key).label }}\n                </div>\n              }\n              <span class=\"ml-auto text-xs\">Tip: drag or shift-click to range. Right-click for actions.</span>\n            </div>\n          }\n        </div>\n\n        <!-- Side rail -->\n        @if (showSideRail) {\n          <aside class=\"flex flex-col gap-4\">\n            @if (!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                        {{ 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-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                    @for (i of [0, 1, 2]; track i) {\n                      <div class=\"mb-2 space-y-2 rounded-lg border p-3\">\n                        <ui-skeleton class=\"h-3 w-32\" />\n                        <ui-skeleton class=\"h-2 w-20\" />\n                      </div>\n                    }\n                  } @else if (selectedDayEvents().length === 0) {\n                    @if (emptyDayTpl) {\n                      <ng-container [ngTemplateOutlet]=\"emptyDayTpl\" />\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                          <lucide-icon [img]=\"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                          ui-button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          class=\"mt-1 h-7 gap-1.5 text-xs\"\n                          (click)=\"eventCreate.emit(rangeStart())\"\n                        >\n                          <lucide-icon [img]=\"Plus\" class=\"size-3\" /> New event\n                        </button>\n                      </div>\n                    }\n                  } @else {\n                    @for (e of selectedDayEvents(); track e.id) {\n                      <ui-context-menu>\n                        <div\n                          ui-context-menu-trigger\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]=\"cn('w-0.5 shrink-0 rounded-full', meta(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]=\"\n                                  cn(\n                                    'shrink-0 rounded px-1.5 py-0.5 text-xs tracking-wider uppercase ring-1 ring-inset',\n                                    meta(e.type).chip\n                                  )\n                                \"\n                              >\n                                {{ meta(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                                <lucide-icon [img]=\"Clock\" class=\"size-3\" />{{ e.start }}-{{ e.end }}\n                              </span>\n                              @if (e.location) {\n                                <span class=\"inline-flex items-center gap-1\">\n                                  <lucide-icon [img]=\"MapPin\" class=\"size-3\" />{{ e.location }}\n                                </span>\n                              }\n                            </div>\n                            @if (e.attendees?.length) {\n                              <div class=\"flex items-center -space-x-1.5 pt-0.5\">\n                                @for (a of e.attendees!.slice(0, 4); track a) {\n                                  <ui-avatar class=\"border-background size-5 border-2\">\n                                    <ui-avatar-fallback class=\"bg-primary/10 text-primary text-xs\">\n                                      {{ initials(a) }}\n                                    </ui-avatar-fallback>\n                                  </ui-avatar>\n                                }\n                                @if (e.attendees!.length > 4) {\n                                  <span class=\"text-muted-foreground pl-2 text-xs\">+{{ e.attendees!.length - 4 }}</span>\n                                }\n                              </div>\n                            }\n                          </div>\n                        </div>\n                        <ui-context-menu-content class=\"w-48\">\n                          <ui-context-menu-label class=\"text-muted-foreground truncate text-xs\">\n                            {{ e.title }}\n                          </ui-context-menu-label>\n                          <ui-context-menu-separator />\n                          <ui-context-menu-item class=\"gap-2\" (select)=\"openEvent(e)\">\n                            <lucide-icon [img]=\"Eye\" class=\"size-3.5\" /> View details\n                          </ui-context-menu-item>\n                          <ui-context-menu-item class=\"gap-2\" (select)=\"eventEdit.emit(e)\">\n                            <lucide-icon [img]=\"Pencil\" class=\"size-3.5\" /> Edit\n                          </ui-context-menu-item>\n                          <ui-context-menu-item class=\"gap-2\" (select)=\"eventDuplicate.emit(e)\">\n                            <lucide-icon [img]=\"CopyPlus\" class=\"size-3.5\" /> Duplicate\n                          </ui-context-menu-item>\n                          <ui-context-menu-separator />\n                          <ui-context-menu-item\n                            class=\"text-destructive focus:text-destructive gap-2\"\n                            (select)=\"eventDelete.emit(e)\"\n                          >\n                            <lucide-icon [img]=\"Trash2\" class=\"size-3.5\" /> Cancel event\n                          </ui-context-menu-item>\n                        </ui-context-menu-content>\n                      </ui-context-menu>\n                    }\n                  }\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, {{ rangeDayCount() }} days\n                      </p>\n                      <p class=\"mt-0.5 truncate text-sm font-semibold\">\n                        {{ fmtDayShort(rangeBounds().lo) }} -&gt; {{ fmtDayShort(rangeBounds().hi) }}\n                      </p>\n                    </div>\n                    <button\n                      ui-button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      class=\"size-7\"\n                      aria-label=\"Clear date range\"\n                      (click)=\"clearRange()\"\n                    >\n                      <lucide-icon [img]=\"X\" class=\"size-3.5\" />\n                    </button>\n                  </div>\n                  <div class=\"mt-3 grid gap-2\" [class]=\"rangeCountsClass()\">\n                    @for (key of typeKeys(); track 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', meta(key).dot)\"></span>\n                          {{ meta(key).label }}\n                        </div>\n                        <p class=\"mt-0.5 text-sm font-semibold tabular-nums\">{{ rangeTypeCounts()[key] ?? 0 }}</p>\n                      </div>\n                    }\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                    @for (e of rangeEvents(); track 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                        (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-xs uppercase\">{{ monthShort(e.date) }}</span>\n                          <span class=\"text-sm leading-none font-semibold tabular-nums\">{{ dayNum(e.date) }}</span>\n                        </div>\n                        <div [class]=\"cn('w-0.5 shrink-0 self-stretch rounded-full', meta(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 }}\n                            @if (e.location) {\n                              {{ ' ' }}{{ e.location }}\n                            }\n                          </p>\n                        </div>\n                        <span\n                          [class]=\"\n                            cn(\n                              'shrink-0 rounded px-1.5 py-0.5 text-xs tracking-wider uppercase ring-1 ring-inset',\n                              meta(e.type).chip\n                            )\n                          \"\n                        >\n                          {{ meta(e.type).label }}\n                        </span>\n                      </div>\n                    }\n                  }\n                </div>\n              </div>\n            }\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                    @for (i of [0, 1, 2]; track i) {\n                      <div class=\"flex items-center gap-3 p-3\">\n                        <ui-skeleton class=\"size-9 rounded-md\" />\n                        <div class=\"flex-1 space-y-1\">\n                          <ui-skeleton class=\"h-3 w-32\" />\n                          <ui-skeleton class=\"h-2 w-20\" />\n                        </div>\n                      </div>\n                    }\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                    @for (e of upcoming(); track 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                        (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-xs uppercase\">{{ monthShort(e.date) }}</span>\n                          <span class=\"text-sm leading-none font-semibold tabular-nums\">{{ dayNum(e.date) }}</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', meta(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 }}\n                            @if (e.location) {\n                              {{ ' ' }}{{ e.location }}\n                            }\n                          </p>\n                        </div>\n                      </button>\n                    }\n                  }\n                </div>\n              </div>\n            }\n          </aside>\n        }\n      </div>\n\n      <!-- Event Detail Dialog -->\n      <ui-dialog [open]=\"eventOpen()\" (openChange)=\"eventOpen.set($event)\">\n        @if (selectedEvent(); as ev) {\n          <ui-dialog-content class=\"sm:max-w-md\">\n            @if (eventDialogTpl) {\n              <ng-container [ngTemplateOutlet]=\"eventDialogTpl\" [ngTemplateOutletContext]=\"{ $implicit: ev }\" />\n            } @else {\n            <div ui-dialog-header>\n              <div class=\"flex items-center gap-2\">\n                <div\n                  [class]=\"\n                    cn('flex size-8 items-center justify-center rounded-md ring-1 ring-inset', meta(ev.type).chip)\n                  \"\n                >\n                  <lucide-icon [img]=\"meta(ev.type).icon\" class=\"size-4\" />\n                </div>\n                <div>\n                  <h2 ui-dialog-title class=\"text-base\">{{ ev.title }}</h2>\n                  <p ui-dialog-description class=\"text-xs\">\n                    {{ meta(ev.type).label }}\n                    @if (ev.status) {\n                      {{ ' ' }}{{ ev.status }}\n                    }\n                  </p>\n                </div>\n              </div>\n            </div>\n            <div class=\"space-y-3 py-2\">\n              @if (ev.description) {\n                <p class=\"text-muted-foreground text-sm\">{{ ev.description }}</p>\n              }\n              <ui-separator />\n              <div class=\"grid gap-2 text-sm\">\n                <div class=\"flex items-center gap-2\">\n                  <lucide-icon [img]=\"Clock\" class=\"text-muted-foreground size-4\" />\n                  <span>{{ ev.date }} {{ ' ' }} {{ ev.start }} - {{ ev.end }}</span>\n                </div>\n                @if (ev.location) {\n                  <div class=\"flex items-center gap-2\">\n                    <lucide-icon [img]=\"MapPin\" class=\"text-muted-foreground size-4\" />\n                    <span>{{ ev.location }}</span>\n                  </div>\n                }\n                @if (ev.attendees?.length) {\n                  <div class=\"flex items-start gap-2\">\n                    <lucide-icon [img]=\"Users\" class=\"text-muted-foreground mt-0.5 size-4\" />\n                    <div class=\"flex flex-wrap items-center gap-1.5\">\n                      @for (a of ev.attendees; track a) {\n                        <div class=\"bg-muted flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs\">\n                          <ui-avatar class=\"size-4\">\n                            <ui-avatar-fallback class=\"bg-primary/10 text-primary text-xs\">\n                              {{ initials(a) }}\n                            </ui-avatar-fallback>\n                          </ui-avatar>\n                          <span>{{ a }}</span>\n                        </div>\n                      }\n                    </div>\n                  </div>\n                }\n              </div>\n            </div>\n            <div ui-dialog-footer>\n              <button ui-button variant=\"outline\" size=\"sm\" (click)=\"handleEditSelected()\">Edit</button>\n              <button ui-button size=\"sm\" variant=\"destructive\" (click)=\"handleDeleteSelected()\">Delete</button>\n            </div>\n            }\n          </ui-dialog-content>\n        }\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiEventCalendarDashboardComponent implements OnInit {\n  protected readonly ArrowRight = ArrowRight\n  protected readonly CalendarDays = CalendarDays\n  protected readonly CalendarPlus = CalendarPlus\n  protected readonly ChevronLeft = ChevronLeft\n  protected readonly ChevronRight = ChevronRight\n  protected readonly Clock = Clock\n  protected readonly Copy = Copy\n  protected readonly CopyPlus = CopyPlus\n  protected readonly Eye = Eye\n  protected readonly ListFilter = ListFilter\n  protected readonly MapPin = MapPin\n  protected readonly MousePointer2 = MousePointer2\n  protected readonly Pencil = Pencil\n  protected readonly Plus = Plus\n  protected readonly Search = Search\n  protected readonly Trash2 = Trash2\n  protected readonly Users = Users\n  protected readonly X = X\n  protected readonly cn = cn\n  protected readonly Math = Math\n  protected readonly skeletonCells = Array.from({ length: 42 }, (_, i) => i)\n\n  /** Source of truth for the rendered events. */\n  @Input() set events(v: CalendarEvent[] | undefined) {\n    this.eventsState.set([...(v ?? [])])\n  }\n  /** Visual + semantic theme per event type. Falls back to defaultEventTypes. */\n  @Input() set eventTypes(v: Record<string, EventTypeMeta> | undefined) {\n    this.eventTypesState.set(v ?? defaultEventTypes)\n  }\n  /** YYYY-MM-DD -- month to open first. Defaults to today. */\n  @Input() initialDate?: string\n  /** 0 = Sunday (default), 1 = Monday. */\n  @Input() weekStartsOn: 0 | 1 = 0\n  /** Show the skeleton placeholders instead of cells / rail rows. */\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input() title = 'Calendar'\n  @Input() description = 'Schedule, meetings, and deadlines. Drag or shift-click to select a range.'\n  /** Default = true. Stats strip across the top. */\n  @Input({ transform: booleanAttribute }) showStats = true\n  /** Default = true. Side rail to the right of the grid. */\n  @Input({ transform: booleanAttribute }) showSideRail = true\n  /** Default = true. Search input in the header. */\n  @Input({ transform: booleanAttribute }) showSearch = true\n  /** Default = true. Month/Week/Day select. Week/Day not implemented yet -- visual only. */\n  @Input({ transform: booleanAttribute }) showViewSelect = true\n  /** Default = true. Legend strip under the grid. */\n  @Input({ transform: booleanAttribute }) showLegend = true\n  /** Default = true. \"Up next\" pane under the side rail. */\n  @Input({ transform: booleanAttribute }) showUpcoming = true\n  @Input('class') className?: string\n\n  @Output() eventsChange = new EventEmitter<CalendarEvent[]>()\n  @Output() eventClick = new EventEmitter<CalendarEvent>()\n  @Output() eventCreate = new EventEmitter<string>()\n  @Output() eventEdit = new EventEmitter<CalendarEvent>()\n  @Output() eventDuplicate = new EventEmitter<CalendarEvent>()\n  @Output() eventDelete = new EventEmitter<CalendarEvent>()\n  @Output() rangeChange = new EventEmitter<{ lo: string; hi: string }>()\n\n  /** Override for the header \"New event\" button. Project `<ng-template #headerActions>…</ng-template>`. */\n  @ContentChild('headerActions') headerActionsTpl?: TemplateRef<unknown>\n  /** Full override of the detail dialog body. Context: `{ $implicit: CalendarEvent }`. */\n  @ContentChild('eventDialog') eventDialogTpl?: TemplateRef<{ $implicit: CalendarEvent }>\n  /** Override for the empty selected-day state. Project `<ng-template #emptyDay>…</ng-template>`. */\n  @ContentChild('emptyDay') emptyDayTpl?: TemplateRef<unknown>\n\n  readonly eventsState = signal<CalendarEvent[]>([])\n  readonly eventTypesState = signal<Record<string, EventTypeMeta>>(defaultEventTypes)\n\n  readonly view = signal<'month' | 'week' | 'day'>('month')\n  readonly eventOpen = signal(false)\n  readonly selectedEvent = signal<CalendarEvent | null>(null)\n  readonly search = signal('')\n\n  // Month-grid state (port of the useMonthGrid headless composable).\n  readonly todayKey = signal<DateKey>(isoDate(new Date()))\n  private todayDate = new Date()\n  readonly cursor = signal<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 1))\n  readonly rangeAnchor = signal<DateKey>(this.todayKey())\n  readonly rangeStart = signal<DateKey>(this.todayKey())\n  readonly rangeEnd = signal<DateKey>(this.todayKey())\n  readonly isDragging = signal(false)\n\n  constructor() {\n    const destroyRef = inject(DestroyRef)\n    // Mirror the Vue block: the drag also ends when the mouse is released outside the grid.\n    if (typeof window !== 'undefined') {\n      window.addEventListener('mouseup', this.endDragBound)\n      window.addEventListener('mouseleave', this.endDragBound)\n      destroyRef.onDestroy(() => {\n        window.removeEventListener('mouseup', this.endDragBound)\n        window.removeEventListener('mouseleave', this.endDragBound)\n      })\n    }\n    // Vue watches rangeBounds and emits range-change (no initial fire).\n    let firstRange = true\n    effect(() => {\n      const bounds = this.rangeBounds()\n      if (firstRange) {\n        firstRange = false\n        return\n      }\n      this.rangeChange.emit(bounds)\n    })\n  }\n\n  private readonly endDragBound = (): void => this.endDrag()\n\n  ngOnInit(): void {\n    // initialDate is initial-only, like the Vue composable: seed cursor + selection once.\n    if (this.initialDate) {\n      const init = dateFromKey(this.initialDate)\n      this.todayDate = new Date(init.getFullYear(), init.getMonth(), init.getDate())\n      const key = isoDate(this.todayDate)\n      this.todayKey.set(key)\n      this.cursor.set(new Date(init.getFullYear(), init.getMonth(), 1))\n      this.rangeAnchor.set(key)\n      this.rangeStart.set(key)\n      this.rangeEnd.set(key)\n    }\n  }\n\n  readonly monthLabel = computed(() => this.cursor().toLocaleDateString('en-US', { month: 'long', year: 'numeric' }))\n\n  readonly gridDays = computed<GridDay[]>(() => {\n    const cursor = this.cursor()\n    const weekStartsOn = this.weekStartsOn\n    const first = new Date(cursor.getFullYear(), cursor.getMonth(), 1)\n    const offset = (first.getDay() - weekStartsOn + 7) % 7\n    const start = new Date(first)\n    start.setDate(first.getDate() - offset)\n    const days: GridDay[] = []\n    for (let i = 0; i < 42; i++) {\n      const d = new Date(start)\n      d.setDate(start.getDate() + i)\n      days.push({ date: d, key: isoDate(d), inMonth: d.getMonth() === cursor.getMonth() })\n    }\n    return days\n  })\n\n  readonly weekdays = computed(() => {\n    const base = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n    return [...base.slice(this.weekStartsOn), ...base.slice(0, this.weekStartsOn)]\n  })\n\n  readonly rangeBounds = computed(() => {\n    const a = this.rangeStart()\n    const b = this.rangeEnd()\n    return a <= b ? { lo: a, hi: b } : { lo: b, hi: a }\n  })\n\n  readonly rangeDayCount = computed(() => dayDiff(this.rangeBounds().lo, this.rangeBounds().hi) + 1)\n  readonly isRange = computed(() => this.rangeBounds().lo !== this.rangeBounds().hi)\n\n  /** Fall through to a neutral style for unknown event types. */\n  private readonly 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\n  meta(type: string): EventTypeMeta {\n    return this.eventTypesState()[type] ?? this.neutralMeta\n  }\n\n  readonly typeKeys = computed(() => Object.keys(this.eventTypesState()))\n\n  readonly filtered = computed(() => {\n    const q = this.search().trim().toLowerCase()\n    const events = this.eventsState()\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  readonly eventsByDate = computed(() => {\n    const map = new Map<string, CalendarEvent[]>()\n    for (const e of this.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  readonly monthCounts = computed(() => {\n    const cursor = this.cursor()\n    const y = cursor.getFullYear()\n    const m = cursor.getMonth()\n    const out: Record<string, number> & { total: number } = { total: 0 }\n    for (const k of this.typeKeys()) out[k] = 0\n    for (const e of this.eventsState()) {\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  readonly typeStats = computed(() => {\n    const cursor = this.cursor()\n    const y = cursor.getFullYear()\n    const m = cursor.getMonth()\n    const keys = this.typeKeys()\n    const out: Record<string, { weeks: number[]; next: CalendarEvent | null }> = {}\n    for (const k of keys) out[k] = { weeks: [0, 0, 0, 0, 0, 0], next: null }\n    const firstOffset = new Date(y, m, 1).getDay()\n    for (const e of this.eventsState()) {\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 keys) {\n      const next = this.eventsState()\n        .filter((e) => e.type === t && e.date >= this.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\n  readonly rangeEvents = computed(() => {\n    const { lo, hi } = this.rangeBounds()\n    return this.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  readonly rangeTypeCounts = computed(() => {\n    const init: Record<string, number> = {}\n    for (const k of this.typeKeys()) init[k] = 0\n    for (const e of this.rangeEvents()) {\n      if (init[e.type] !== undefined) init[e.type]!++\n    }\n    return init\n  })\n\n  readonly selectedDayEvents = computed(() => this.eventsByDate().get(this.rangeStart()) ?? [])\n\n  readonly upcoming = computed(() =>\n    this.filtered()\n      .filter((e) => e.date > this.todayKey())\n      .sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start))\n      .slice(0, 4),\n  )\n\n  setView(v: string): void {\n    this.view.set(v as 'month' | 'week' | 'day')\n  }\n\n  openEvent(e: CalendarEvent): void {\n    this.selectedEvent.set(e)\n    this.eventOpen.set(true)\n    this.eventClick.emit(e)\n  }\n\n  copyDate(key: string): void {\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  fmtNextDate(key: string): string {\n    if (key === this.todayKey()) return 'Today'\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  fmtDayLong(key: string): string {\n    return dateFromKey(key).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })\n  }\n\n  fmtDayShort(key: string): string {\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  monthShort(key: string): string {\n    return new Date(key).toLocaleDateString('en-US', { month: 'short' })\n  }\n\n  dayNum(key: string): number {\n    return new Date(key).getDate()\n  }\n\n  initials(name: string): string {\n    return name\n      .split(' ')\n      .map((n) => n[0])\n      .join('')\n      .slice(0, 2)\n  }\n\n  inRange(key: DateKey): boolean {\n    const { lo, hi } = this.rangeBounds()\n    return key >= lo && key <= hi\n  }\n\n  cellRangeClass(key: string, inMonth: boolean): string {\n    const { lo, hi } = this.rangeBounds()\n    if (!this.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  cellClass(d: GridDay, i: number): string {\n    return 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      this.cellRangeClass(d.key, d.inMonth),\n      !d.inMonth && !this.inRange(d.key) && 'text-muted-foreground/60',\n    )\n  }\n\n  dayNumberClass(d: GridDay): string {\n    return cn(\n      'inline-flex size-5 items-center justify-center rounded-full text-xs tabular-nums',\n      d.key === this.todayKey() && 'bg-primary text-primary-foreground font-semibold',\n      d.key !== this.todayKey() && d.inMonth && 'text-foreground',\n      !d.inMonth && 'text-muted-foreground/60',\n    )\n  }\n\n  statTileClass(key: string): string {\n    return 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      this.meta(key).ring,\n    )\n  }\n\n  weekBarClass(key: string, n: number): string {\n    return cn(\n      'flex-1 rounded-sm transition-colors',\n      n > 0 ? this.meta(key).bar : 'bg-muted/40',\n      n > 0 && 'opacity-70 group-hover:opacity-100',\n    )\n  }\n\n  /** Static Tailwind-safe grid columns for the stats strip (Vue builds this string dynamically). */\n  statsGridClass(): string {\n    const n = Math.min(this.typeKeys().length, 4)\n    if (n <= 1) return ''\n    if (n === 2) return 'sm:grid-cols-2'\n    if (n === 3) return 'sm:grid-cols-2 lg:grid-cols-3'\n    return 'sm:grid-cols-2 lg:grid-cols-4'\n  }\n\n  rangeCountsClass(): string {\n    const n = Math.min(this.typeKeys().length, 4)\n    if (n <= 1) return 'grid-cols-1'\n    if (n === 2) return 'grid-cols-2'\n    if (n === 3) return 'grid-cols-3'\n    return 'grid-cols-4'\n  }\n\n  selectCategoryFilter(key: string): void {\n    const label = this.eventTypesState()[key]?.label.toLowerCase() || ''\n    this.search.set(label === this.search().toLowerCase() ? '' : label)\n  }\n\n  handleEditSelected(): void {\n    if (this.selectedEvent()) this.eventEdit.emit(this.selectedEvent()!)\n    this.eventOpen.set(false)\n  }\n\n  handleDeleteSelected(): void {\n    if (this.selectedEvent()) this.eventDelete.emit(this.selectedEvent()!)\n    this.eventOpen.set(false)\n  }\n\n  prevMonth(): void {\n    const c = this.cursor()\n    this.cursor.set(new Date(c.getFullYear(), c.getMonth() - 1, 1))\n  }\n\n  nextMonth(): void {\n    const c = this.cursor()\n    this.cursor.set(new Date(c.getFullYear(), c.getMonth() + 1, 1))\n  }\n\n  goToToday(): void {\n    const t = this.todayDate\n    this.cursor.set(new Date(t.getFullYear(), t.getMonth(), 1))\n    const key = this.todayKey()\n    this.rangeAnchor.set(key)\n    this.rangeStart.set(key)\n    this.rangeEnd.set(key)\n  }\n\n  selectDay(key: DateKey): void {\n    this.rangeAnchor.set(key)\n    this.rangeStart.set(key)\n    this.rangeEnd.set(key)\n  }\n\n  selectWeekOf(key: DateKey): void {\n    const weekStartsOn = this.weekStartsOn\n    const d = dateFromKey(key)\n    const dow = (d.getDay() - weekStartsOn + 7) % 7\n    const wkStart = new Date(d)\n    wkStart.setDate(d.getDate() - dow)\n    const wkEnd = new Date(wkStart)\n    wkEnd.setDate(wkStart.getDate() + 6)\n    this.rangeAnchor.set(isoDate(wkStart))\n    this.rangeStart.set(isoDate(wkStart))\n    this.rangeEnd.set(isoDate(wkEnd))\n  }\n\n  clearRange(): void {\n    const key = this.todayKey()\n    this.rangeAnchor.set(key)\n    this.rangeStart.set(key)\n    this.rangeEnd.set(key)\n  }\n\n  onCellMouseDown(key: DateKey, ev: MouseEvent): void {\n    if (ev.button !== 0) return // ignore right-click -- context menus handle it\n    if (ev.shiftKey) {\n      this.rangeEnd.set(key)\n      return\n    }\n    this.rangeAnchor.set(key)\n    this.rangeStart.set(key)\n    this.rangeEnd.set(key)\n    this.isDragging.set(true)\n  }\n\n  onCellMouseEnter(key: DateKey): void {\n    if (!this.isDragging()) return\n    this.rangeEnd.set(key)\n    this.rangeStart.set(this.rangeAnchor())\n  }\n\n  endDrag(): void {\n    if (this.isDragging()) this.isDragging.set(false)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/event-calendar-dashboard/event-calendar-dashboard.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/event-calendar-dashboard/defaults.ts",
      "content": "import { AlertCircle, CheckCircle2, Plane, Video } from 'lucide-angular'\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 `[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/app/components/blocks/event-calendar-dashboard/defaults.ts"
    },
    {
      "path": "packages/registry-angular/blocks/event-calendar-dashboard/types.ts",
      "content": "import type { LucideIconData } from 'lucide-angular'\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 <ui-event-calendar-dashboard>. 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 for the stat tile + chip + dialog header. */\n  icon: LucideIconData\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/app/components/blocks/event-calendar-dashboard/types.ts"
    },
    {
      "path": "packages/registry-angular/blocks/event-calendar-dashboard/index.ts",
      "content": "export { UiEventCalendarDashboardComponent, type DateKey } from './event-calendar-dashboard.component'\nexport { defaultEventTypes } from './defaults'\nexport type { CalendarEvent, EventTypeMeta } from './types'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/event-calendar-dashboard/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/dialog.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/avatar.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/skeleton.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/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. [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.",
  "categories": [
    "productivity",
    "dashboard"
  ]
}