{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "event-calendar-dashboard",
  "title": "Event Calendar Dashboard",
  "type": "registry:block",
  "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 / onEventsChange 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 hook.",
  "categories": [
    "productivity",
    "dashboard"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/event-calendar-dashboard/event-calendar-dashboard.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport {\n  AlertCircle,\n  CalendarDays,\n  CheckCircle2,\n  ChevronLeft,\n  ChevronRight,\n  Clock,\n  GraduationCap,\n  Headphones,\n  ListFilter,\n  MapPin,\n  MousePointer2,\n  Pencil,\n  Plane,\n  Plus,\n  Search,\n  Trash2,\n  Users,\n  Video,\n  Wrench,\n  X,\n  type IconNode,\n} from 'lucide'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/avatar/avatar'\nimport '../../components/button/button'\nimport '../../components/dialog/dialog'\nimport '../../components/input/input'\nimport '../../components/select/select'\nimport '../../components/separator/separator'\nimport '../../components/skeleton/skeleton'\nimport type { UipInput } from '../../components/input/input'\nimport type { UipSelect } from '../../components/select/select'\n\nexport interface DashboardCalendarEvent {\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  status?: string\n}\n\n/**\n * Visual + semantic config per event type. Mirrors React's EventTypeMeta,\n * except `icon` is a kebab-case icon name (resolved to lucide inside the\n * shadow root) instead of a Lucide component — components can't cross the\n * custom-element boundary from plain HTML.\n */\nexport interface DashboardEventTypeMeta {\n  label: string\n  icon: string\n  chip: string\n  bar: string\n  dot: string\n  glow: string\n  text: string\n  ring: string\n}\n\nexport const defaultDashboardEventTypes: Record<string, DashboardEventTypeMeta> = {\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: 'circle-check',\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: 'circle-alert',\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\nconst NEUTRAL_META: DashboardEventTypeMeta = {\n  label: 'Event',\n  icon: 'calendar-days',\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\nfunction typeIcon(name: string, cls: string) {\n  const map: Record<string, [IconNode, string]> = {\n    video: [Video, 'video'],\n    'circle-check': [CheckCircle2, 'circle-check-big'],\n    'circle-alert': [AlertCircle, 'circle-alert'],\n    plane: [Plane, 'plane'],\n    headphones: [Headphones, 'headphones'],\n    wrench: [Wrench, 'wrench'],\n    'graduation-cap': [GraduationCap, 'graduation-cap'],\n    'calendar-days': [CalendarDays, 'calendar-days'],\n  }\n  const [node, kebab] = map[name] ?? [CalendarDays, 'calendar-days']\n  return icon(node, kebab, cls)\n}\n\nfunction pad(n: number): string {\n  return String(n).padStart(2, '0')\n}\n\nfunction dateKey(d: Date): string {\n  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\n}\n\nfunction dateFromKey(key: string): Date {\n  const [y, m, d] = key.split('-').map(Number)\n  return new Date(y!, m! - 1, d)\n}\n\n/**\n * <uip-event-calendar-dashboard> — the registry EventCalendar block as a web component.\n *\n * A faithful month-view port of React's EventCalendar: stats strip, day\n * click / shift-click range select, side rail (selected day or range\n * summary), upcoming list, search, skeleton loading, show* toggles, and the\n * event detail dialog. `events` / `eventTypes` are settable properties\n * (arrays/objects cross the boundary as properties, not attributes).\n *\n * Deviations from React (documented): no drag-to-range (click /\n * shift-click only — pointer drag across shadow-DOM cells is unreliable),\n * no per-cell/per-event context menus (uip-context-menu trigger-wrapping\n * per cell is left to consumers), no headerActions / eventDialog / emptyDay\n * render-prop slots (use light-DOM composition instead), and\n * `eventTypes[].icon` is a kebab icon-name string rather than a Lucide\n * component. Callbacks become kebab CustomEvents: `event-click`,\n * `event-create` (dateKey), `event-edit` / `event-duplicate` /\n * `event-delete` (event), `range-change` ({ lo, hi }).\n */\nexport class UipEventCalendarDashboard extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    events: { attribute: false },\n    eventTypes: { attribute: false },\n    initialDate: { attribute: 'initial-date' },\n    weekStartsOn: { type: Number, attribute: 'week-starts-on' },\n    loading: { type: Boolean },\n    heading: {},\n    description: {},\n    showStats: { type: Boolean, attribute: 'show-stats' },\n    showSideRail: { type: Boolean, attribute: 'show-side-rail' },\n    showSearch: { type: Boolean, attribute: 'show-search' },\n    showViewSelect: { type: Boolean, attribute: 'show-view-select' },\n    showLegend: { type: Boolean, attribute: 'show-legend' },\n    showUpcoming: { type: Boolean, attribute: 'show-upcoming' },\n    view: { state: true },\n    search: { state: true },\n    cursorYear: { state: true },\n    cursorMonth: { state: true },\n    rangeStart: { state: true },\n    rangeEnd: { state: true },\n    dialogOpen: { state: true },\n    selectedEvent: { state: true },\n  }\n\n  events: DashboardCalendarEvent[] = []\n  eventTypes: Record<string, DashboardEventTypeMeta> = defaultDashboardEventTypes\n  initialDate?: string\n  weekStartsOn: 0 | 1 = 0\n  loading = false\n  /** React's `title` (`title` is taken by HTMLElement). */\n  heading = 'Calendar'\n  description = 'Schedule, meetings, and deadlines. Click 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  private view: 'month' | 'week' | 'day' = 'month'\n  private search = ''\n  private cursorYear = 0\n  private cursorMonth = 0\n  private rangeStart = ''\n  private rangeEnd = ''\n  private dialogOpen = false\n  private selectedEvent: DashboardCalendarEvent | null = null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n    const now = new Date()\n    this.cursorYear = now.getFullYear()\n    this.cursorMonth = now.getMonth()\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'event-calendar-dashboard')\n    const base = this.initialDate ? dateFromKey(this.initialDate) : new Date()\n    this.cursorYear = base.getFullYear()\n    this.cursorMonth = base.getMonth()\n    const today = dateKey(new Date())\n    this.rangeStart = today\n    this.rangeEnd = today\n  }\n\n  private get todayKey(): string {\n    return dateKey(new Date())\n  }\n\n  private get monthLabel(): string {\n    return new Date(this.cursorYear, this.cursorMonth, 1).toLocaleDateString('en-US', {\n      month: 'long',\n      year: 'numeric',\n    })\n  }\n\n  private get weekdays(): string[] {\n    const base = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n    return this.weekStartsOn === 1 ? [...base.slice(1), base[0]!] : base\n  }\n\n  private get gridDays(): Array<{ key: string; date: Date; inMonth: boolean }> {\n    const first = new Date(this.cursorYear, this.cursorMonth, 1)\n    let offset = first.getDay()\n    if (this.weekStartsOn === 1) offset = (offset + 6) % 7\n    const start = new Date(this.cursorYear, this.cursorMonth, 1 - offset)\n    return Array.from({ length: 42 }, (_, i) => {\n      const date = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i)\n      return { key: dateKey(date), date, inMonth: date.getMonth() === this.cursorMonth }\n    })\n  }\n\n  private get rangeBounds(): { lo: string; hi: string } {\n    return this.rangeStart <= this.rangeEnd\n      ? { lo: this.rangeStart, hi: this.rangeEnd }\n      : { lo: this.rangeEnd, hi: this.rangeStart }\n  }\n\n  private get isRange(): boolean {\n    return this.rangeBounds.lo !== this.rangeBounds.hi\n  }\n\n  private get rangeDayCount(): number {\n    const { lo, hi } = this.rangeBounds\n    return Math.round((dateFromKey(hi).getTime() - dateFromKey(lo).getTime()) / 86_400_000) + 1\n  }\n\n  private inRange(key: string): boolean {\n    const { lo, hi } = this.rangeBounds\n    return key >= lo && key <= hi\n  }\n\n  private metaFor(type: string): DashboardEventTypeMeta {\n    return this.eventTypes[type] ?? NEUTRAL_META\n  }\n\n  private get typeKeys(): string[] {\n    return Object.keys(this.eventTypes)\n  }\n\n  private get filtered(): DashboardCalendarEvent[] {\n    const q = this.search.trim().toLowerCase()\n    if (!q) return this.events\n    return this.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  private get eventsByDate(): Map<string, DashboardCalendarEvent[]> {\n    const map = new Map<string, DashboardCalendarEvent[]>()\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  private get monthCounts(): Record<string, number> & { total: number } {\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.events) {\n      const d = new Date(`${e.date}T00:00:00`)\n      if (d.getFullYear() === this.cursorYear && d.getMonth() === this.cursorMonth) {\n        if (out[e.type] !== undefined) out[e.type]!++\n        out.total++\n      }\n    }\n    return out\n  }\n\n  private get typeStats(): Record<string, { weeks: number[]; next: DashboardCalendarEvent | null }> {\n    const out: Record<string, { weeks: number[]; next: DashboardCalendarEvent | null }> = {}\n    for (const k of this.typeKeys) out[k] = { weeks: [0, 0, 0, 0, 0, 0], next: null }\n    const firstOffset = new Date(this.cursorYear, this.cursorMonth, 1).getDay()\n    for (const e of this.events) {\n      const d = new Date(`${e.date}T00:00:00`)\n      if (d.getFullYear() === this.cursorYear && d.getMonth() === this.cursorMonth && 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 this.typeKeys) {\n      out[t]!.next =\n        this.events\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] ?? null\n    }\n    return out\n  }\n\n  private 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  private get rangeEvents(): DashboardCalendarEvent[] {\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  private get selectedDayEvents(): DashboardCalendarEvent[] {\n    return this.eventsByDate.get(this.rangeStart) ?? []\n  }\n\n  private get upcoming(): DashboardCalendarEvent[] {\n    return 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  private openEvent(e: DashboardCalendarEvent) {\n    this.selectedEvent = e\n    this.dialogOpen = true\n    this.dispatchEvent(new CustomEvent('event-click', { detail: e, bubbles: true, composed: true }))\n  }\n\n  private prevMonth() {\n    const d = new Date(this.cursorYear, this.cursorMonth - 1, 1)\n    this.cursorYear = d.getFullYear()\n    this.cursorMonth = d.getMonth()\n  }\n\n  private nextMonth() {\n    const d = new Date(this.cursorYear, this.cursorMonth + 1, 1)\n    this.cursorYear = d.getFullYear()\n    this.cursorMonth = d.getMonth()\n  }\n\n  private goToToday() {\n    const now = new Date()\n    this.cursorYear = now.getFullYear()\n    this.cursorMonth = now.getMonth()\n    const key = dateKey(now)\n    this.rangeStart = key\n    this.rangeEnd = key\n    this.emitRange()\n  }\n\n  private clearRange() {\n    this.rangeEnd = this.rangeStart\n    this.emitRange()\n  }\n\n  private onCellClick(key: string, e: MouseEvent) {\n    if (e.shiftKey) {\n      this.rangeEnd = key\n    } else {\n      this.rangeStart = key\n      this.rangeEnd = key\n    }\n    this.emitRange()\n  }\n\n  private emitRange() {\n    this.dispatchEvent(\n      new CustomEvent('range-change', { detail: this.rangeBounds, bubbles: true, composed: true }),\n    )\n  }\n\n  private fmtDayLong(key: string): string {\n    return dateFromKey(key).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })\n  }\n\n  private fmtDayShort(key: string): string {\n    return dateFromKey(key).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n  }\n\n  private initials(name: string): string {\n    return name\n      .split(' ')\n      .map((n) => n[0])\n      .join('')\n      .slice(0, 2)\n  }\n\n  private 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  render() {\n    const monthCounts = this.monthCounts\n    const typeStats = this.typeStats\n    const gridDays = this.gridDays\n    const eventsByDate = this.eventsByDate\n    const todayKey = this.todayKey\n    const rangeEvents = this.rangeEvents\n    const selectedDayEvents = this.selectedDayEvents\n    const upcoming = this.upcoming\n    const rangeTypeCounts: Record<string, number> = {}\n    for (const k of this.typeKeys) rangeTypeCounts[k] = 0\n    for (const e of rangeEvents) {\n      if (rangeTypeCounts[e.type] !== undefined) rangeTypeCounts[e.type]!++\n    }\n    return html`\n      <div data-slot=\"event-calendar-dashboard\" class=\"flex flex-col gap-5\">\n        <header class=\"flex flex-wrap items-end justify-between gap-4\">\n          <div>\n            <h1 class=\"text-2xl font-semibold tracking-tight\">${this.heading}</h1>\n            ${this.description ? html`<p class=\"text-muted-foreground mt-1 text-sm\">${this.description}</p>` : nothing}\n          </div>\n          <div class=\"flex items-center gap-2\">\n            ${this.showSearch\n              ? html`<div class=\"relative\">\n                  <uip-input\n                    .value=${this.search}\n                    placeholder=\"Search events\"\n                    class=\"h-8 w-56 text-xs\"\n                    @input=${(e: Event) => {\n                      this.search = (e.target as UipInput).value\n                    }}\n                  >\n                    <span slot=\"prefix\">${icon(Search, 'search', 'size-3.5')}</span>\n                  </uip-input>\n                </div>`\n              : nothing}\n            ${this.showViewSelect\n              ? html`<uip-select\n                  value=${this.view}\n                  class=\"h-8 w-24 text-xs\"\n                  @change=${(e: Event) => {\n                    this.view = (e.target as UipSelect).value as 'month' | 'week' | 'day'\n                  }}\n                >\n                  <option value=\"month\">Month</option>\n                  <option value=\"week\">Week</option>\n                  <option value=\"day\">Day</option>\n                </uip-select>`\n              : nothing}\n            <uip-button\n              size=\"sm\"\n              class=\"gap-1.5\"\n              @click=${() =>\n                this.dispatchEvent(new CustomEvent('event-create', { detail: this.rangeStart, bubbles: true, composed: true }))}\n            >\n              ${icon(Plus, 'plus', 'size-3.5')} New event\n            </uip-button>\n          </div>\n        </header>\n\n        ${this.showStats\n          ? html`\n              <div class=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n                ${this.typeKeys.map((key) => {\n                  const meta = this.eventTypes[key]!\n                  const searched = meta.label.toLowerCase() === this.search.toLowerCase()\n                  return html`\n                    <button\n                      type=\"button\"\n                      class=${[\n                        'group bg-card hover:bg-muted/40 relative isolate flex flex-col overflow-hidden rounded-xl border p-4 text-left ring-1 ring-transparent transition-[colors,box-shadow,transform] duration-150 ring-inset hover:shadow-xs active:scale-[0.98]',\n                        meta.ring,\n                      ].join(' ')}\n                      @click=${() => {\n                        this.search = searched ? '' : meta.label.toLowerCase()\n                      }}\n                    >\n                      ${typeIcon(\n                        meta.icon,\n                        [\n                          'pointer-events-none absolute -top-4 -right-4 size-24 opacity-[0.06] transition-opacity group-hover:opacity-[0.12]',\n                          meta.text,\n                        ].join(' '),\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', meta.dot].join(' ')}></span>\n                          <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">${meta.label}</p>\n                        </div>\n                        <div\n                          class=${['flex size-7 items-center justify-center rounded-md ring-1 ring-inset', meta.chip].join(' ')}\n                        >\n                          ${typeIcon(meta.icon, 'size-3.5')}\n                        </div>\n                      </div>\n                      <div class=\"mt-2 flex items-baseline gap-1.5\">\n                        <p class=\"text-3xl leading-none font-semibold tabular-nums\">${monthCounts[key] ?? 0}</p>\n                        <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">this month</p>\n                      </div>\n                      <div class=\"mt-3 flex h-6 items-end gap-1\">\n                        ${typeStats[key]!.weeks.slice(0, 6).map(\n                          (n, i) => html`\n                            <span\n                              class=${[\n                                'flex-1 rounded-sm transition-colors',\n                                n > 0 ? meta.bar : 'bg-muted/40',\n                                n > 0 ? 'opacity-70 group-hover:opacity-100' : '',\n                              ].join(' ')}\n                              style=\"height: ${n > 0 ? Math.min(100, 30 + n * 35) : 20}%\"\n                              title=\"Week ${i + 1}: ${n} ${meta.label.toLowerCase()}${n === 1 ? '' : 's'}\"\n                            ></span>\n                          `,\n                        )}\n                      </div>\n                      <div class=\"border-border/50 mt-3 min-h-[2.25rem] border-t pt-2\">\n                        ${typeStats[key]!.next\n                          ? html`\n                              <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">\n                                Next <span class=${meta.text}>${this.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                            `\n                          : html`\n                              <p class=\"text-muted-foreground text-xs tracking-wider uppercase\">No upcoming</p>\n                              <p class=\"text-muted-foreground/70 mt-0.5 text-xs\">All clear</p>\n                            `}\n                      </div>\n                    </button>\n                  `\n                })}\n              </div>\n            `\n          : nothing}\n\n        <div class=${['grid gap-4', this.showSideRail ? 'lg:grid-cols-[minmax(0,1fr)_320px]' : ''].join(' ')}>\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                <uip-button variant=\"outline\" size=\"icon\" class=\"size-7\" aria-label=\"Previous month\" @click=${this.prevMonth}>\n                  ${icon(ChevronLeft, 'chevron-left', 'size-4')}\n                </uip-button>\n                <uip-button variant=\"outline\" size=\"icon\" class=\"size-7\" aria-label=\"Next month\" @click=${this.nextMonth}>\n                  ${icon(ChevronRight, 'chevron-right', 'size-4')}\n                </uip-button>\n                <uip-button variant=\"ghost\" size=\"sm\" class=\"h-7 text-xs\" @click=${this.goToToday}>Today</uip-button>\n                <h2 class=\"ml-2 text-sm font-semibold\">${this.monthLabel}</h2>\n              </div>\n              <div class=\"text-muted-foreground flex items-center gap-3 text-xs\">\n                ${this.isRange\n                  ? html`\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                        ${icon(MousePointer2, 'mouse-pointer-2', 'size-3')}\n                        <span> ${this.rangeDayCount} days, ${rangeEvents.length} events </span>\n                        <button class=\"hover:text-foreground ml-0.5\" aria-label=\"Clear date range\" @click=${this.clearRange}>\n                          ${icon(X, 'x', 'size-3')}\n                        </button>\n                      </div>\n                    `\n                  : nothing}\n                <div class=\"flex items-center gap-1.5\">\n                  ${icon(CalendarDays, 'calendar-days', '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              ${this.weekdays.map((w) => html`<div class=\"px-2 py-2 font-medium\">${w}</div>`)}\n            </div>\n\n            <div class=\"grid grid-cols-7 select-none\">\n              ${this.loading\n                ? Array.from({ length: 42 }).map(\n                    () => html`\n                      <div class=\"h-24 border-r border-b p-1.5 last:border-r-0\">\n                        <uip-skeleton class=\"h-3 w-6\"></uip-skeleton>\n                        <uip-skeleton class=\"mt-2 h-3 w-full\"></uip-skeleton>\n                      </div>\n                    `,\n                  )\n                : gridDays.map(\n                    (d, i) => html`\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                          this.cellRangeClass(d.key, d.inMonth),\n                          !d.inMonth && !this.inRange(d.key) ? 'text-muted-foreground/60' : '',\n                        ].join(' ')}\n                        @click=${(e: MouseEvent) => this.onCellClick(d.key, e)}\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-xs tabular-nums',\n                              d.key === todayKey ? 'bg-primary text-primary-foreground font-semibold' : '',\n                              d.key !== todayKey && d.inMonth ? 'text-foreground' : '',\n                              !d.inMonth ? 'text-muted-foreground/60' : '',\n                            ].join(' ')}\n                          >\n                            ${d.date.getDate()}\n                          </span>\n                          ${(eventsByDate.get(d.key)?.length ?? 0) > 0\n                            ? html`<span class=\"text-muted-foreground text-xs tabular-nums\"\n                                >${eventsByDate.get(d.key)!.length}</span\n                              >`\n                            : nothing}\n                        </div>\n                        <div class=\"flex flex-col gap-0.5\">\n                          ${(eventsByDate.get(d.key) ?? []).slice(0, 2).map(\n                            (e) => html`\n                              <span\n                                class=${[\n                                  'flex cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-xs ring-1 ring-inset',\n                                  this.metaFor(e.type).chip,\n                                ].join(' ')}\n                                @click=${(ev: Event) => {\n                                  ev.stopPropagation()\n                                  this.openEvent(e)\n                                }}\n                              >\n                                <span class=\"tabular-nums opacity-70\">${e.start}</span>\n                                <span class=\"truncate\">${e.title}</span>\n                              </span>\n                            `,\n                          )}\n                          ${(eventsByDate.get(d.key)?.length ?? 0) > 2\n                            ? html`<span class=\"text-muted-foreground px-1 text-xs\"\n                                >+${eventsByDate.get(d.key)!.length - 2} more</span\n                              >`\n                            : nothing}\n                        </div>\n                      </button>\n                    `,\n                  )}\n            </div>\n\n            ${this.showLegend\n              ? html`\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                    ${icon(ListFilter, 'list-filter', 'size-3')}\n                    ${this.typeKeys.map(\n                      (key) => html`\n                        <div class=\"flex items-center gap-1.5\">\n                          <span class=${['size-2 rounded-full', this.eventTypes[key]!.dot].join(' ')}></span>\n                          ${this.eventTypes[key]!.label}\n                        </div>\n                      `,\n                    )}\n                    <span class=\"ml-auto text-xs\">Tip: click or shift-click to range. Click an event for details.</span>\n                  </div>\n                `\n              : nothing}\n          </div>\n\n          ${this.showSideRail\n            ? html`\n                <aside class=\"flex flex-col gap-4\">\n                  ${!this.isRange\n                    ? html`\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                                  ${this.rangeStart === todayKey ? 'Today' : 'Selected'}\n                                </p>\n                                <p class=\"mt-0.5 text-sm font-semibold\">${this.fmtDayLong(this.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-[420px] overflow-y-auto p-3\">\n                            ${this.loading\n                              ? Array.from({ length: 3 }).map(\n                                  () => html`\n                                    <div class=\"mb-2 space-y-2 rounded-lg border p-3\">\n                                      <uip-skeleton class=\"h-3 w-32\"></uip-skeleton>\n                                      <uip-skeleton class=\"h-2 w-20\"></uip-skeleton>\n                                    </div>\n                                  `,\n                                )\n                              : selectedDayEvents.length === 0\n                                ? html`\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                                        ${icon(CalendarDays, 'calendar-days', '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                                      <uip-button\n                                        size=\"sm\"\n                                        variant=\"outline\"\n                                        class=\"mt-1 h-7 gap-1.5 text-xs\"\n                                        @click=${() =>\n                                          this.dispatchEvent(\n                                            new CustomEvent('event-create', { detail: this.rangeStart, bubbles: true, composed: true }),\n                                          )}\n                                      >\n                                        ${icon(Plus, 'plus', 'size-3')} New event\n                                      </uip-button>\n                                    </div>\n                                  `\n                                : selectedDayEvents.map(\n                                    (e) => html`\n                                      <div\n                                        class=\"group hover:bg-accent/50 relative flex cursor-pointer gap-3 rounded-lg p-2.5 transition-colors\"\n                                        @click=${() => this.openEvent(e)}\n                                      >\n                                        <div class=${['w-0.5 shrink-0 rounded-full', this.metaFor(e.type).bar].join(' ')}></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                                                'shrink-0 rounded px-1.5 py-0.5 text-xs tracking-wider uppercase ring-1 ring-inset',\n                                                this.metaFor(e.type).chip,\n                                              ].join(' ')}\n                                            >\n                                              ${this.metaFor(e.type).label}\n                                            </span>\n                                          </div>\n                                          <div class=\"text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs\">\n                                            <span class=\"inline-flex items-center gap-1\">\n                                              ${icon(Clock, 'clock', 'size-3')} ${e.start}-${e.end}\n                                            </span>\n                                            ${e.location\n                                              ? html`<span class=\"inline-flex items-center gap-1\">\n                                                  ${icon(MapPin, 'map-pin', 'size-3')} ${e.location}\n                                                </span>`\n                                              : nothing}\n                                          </div>\n                                          ${e.attendees?.length\n                                            ? html`<div class=\"flex items-center -space-x-1.5 pt-0.5\">\n                                                ${e.attendees.slice(0, 4).map(\n                                                  (a) => html`\n                                                    <uip-avatar class=\"border-background size-5 border-2\">\n                                                      <uip-avatar-fallback class=\"bg-primary/10 text-primary text-xs\">\n                                                        ${this.initials(a)}\n                                                      </uip-avatar-fallback>\n                                                    </uip-avatar>\n                                                  `,\n                                                )}\n                                                ${e.attendees.length > 4\n                                                  ? html`<span class=\"text-muted-foreground pl-2 text-xs\">\n                                                      +${e.attendees.length - 4}\n                                                    </span>`\n                                                  : nothing}\n                                              </div>`\n                                            : nothing}\n                                        </div>\n                                      </div>\n                                    `,\n                                  )}\n                          </div>\n                        </div>\n                      `\n                    : html`\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, ${this.rangeDayCount} days\n                                </p>\n                                <p class=\"mt-0.5 truncate text-sm font-semibold\">\n                                  ${this.fmtDayShort(this.rangeBounds.lo)} -&gt; ${this.fmtDayShort(this.rangeBounds.hi)}\n                                </p>\n                              </div>\n                              <uip-button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                class=\"size-7\"\n                                aria-label=\"Clear date range\"\n                                @click=${this.clearRange}\n                              >\n                                ${icon(X, 'x', 'size-3.5')}\n                              </uip-button>\n                            </div>\n                            <div class=\"mt-3 grid gap-2 grid-cols-4\">\n                              ${this.typeKeys.map(\n                                (key) => html`\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=${['size-1.5 rounded-full', this.eventTypes[key]!.dot].join(' ')}></span>\n                                      ${this.eventTypes[key]!.label}\n                                    </div>\n                                    <p class=\"mt-0.5 text-sm font-semibold tabular-nums\">${rangeTypeCounts[key] ?? 0}</p>\n                                  </div>\n                                `,\n                              )}\n                            </div>\n                          </div>\n                          <div class=\"max-h-[420px] overflow-y-auto\">\n                            ${rangeEvents.length === 0\n                              ? html`<p class=\"text-muted-foreground px-4 py-8 text-center text-xs\">\n                                  No events in range.\n                                </p>`\n                              : rangeEvents.map(\n                                  (e) => html`\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=${() => this.openEvent(e)}\n                                    >\n                                      <div\n                                        class=\"bg-background/60 flex w-10 shrink-0 flex-col items-center rounded-md border px-1 py-1 text-center\"\n                                      >\n                                        <span class=\"text-muted-foreground text-xs uppercase\">\n                                          ${new Date(`${e.date}T00:00:00`).toLocaleDateString('en-US', { month: 'short' })}\n                                        </span>\n                                        <span class=\"text-sm leading-none font-semibold tabular-nums\">\n                                          ${new Date(`${e.date}T00:00:00`).getDate()}\n                                        </span>\n                                      </div>\n                                      <div\n                                        class=${['w-0.5 shrink-0 self-stretch rounded-full', this.metaFor(e.type).bar].join(' ')}\n                                      ></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}${e.location ? html`<span> ${e.location}</span>` : nothing}\n                                        </p>\n                                      </div>\n                                      <span\n                                        class=${[\n                                          'shrink-0 rounded px-1.5 py-0.5 text-xs tracking-wider uppercase ring-1 ring-inset',\n                                          this.metaFor(e.type).chip,\n                                        ].join(' ')}\n                                      >\n                                        ${this.metaFor(e.type).label}\n                                      </span>\n                                    </div>\n                                  `,\n                                )}\n                          </div>\n                        </div>\n                      `}\n                  ${this.showUpcoming\n                    ? html`\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                            ${this.loading\n                              ? Array.from({ length: 3 }).map(\n                                  () => html`\n                                    <div class=\"flex items-center gap-3 p-3\">\n                                      <uip-skeleton class=\"size-9 rounded-md\"></uip-skeleton>\n                                      <div class=\"flex-1 space-y-1\">\n                                        <uip-skeleton class=\"h-3 w-32\"></uip-skeleton>\n                                        <uip-skeleton class=\"h-2 w-20\"></uip-skeleton>\n                                      </div>\n                                    </div>\n                                  `,\n                                )\n                              : upcoming.length === 0\n                                ? html`<p class=\"text-muted-foreground px-4 py-6 text-center text-xs\">\n                                    Nothing on the horizon.\n                                  </p>`\n                                : upcoming.map(\n                                    (e) => html`\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=${() => this.openEvent(e)}\n                                      >\n                                        <div\n                                          class=\"bg-background flex size-10 shrink-0 flex-col items-center justify-center rounded-md border text-center\"\n                                        >\n                                          <span class=\"text-muted-foreground text-xs uppercase\">\n                                            ${new Date(`${e.date}T00:00:00`).toLocaleDateString('en-US', { month: 'short' })}\n                                          </span>\n                                          <span class=\"text-sm leading-none font-semibold tabular-nums\">\n                                            ${new Date(`${e.date}T00:00:00`).getDate()}\n                                          </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', this.metaFor(e.type).dot].join(' ')}></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}${e.location ? html`<span> ${e.location}</span>` : nothing}\n                                          </p>\n                                        </div>\n                                      </button>\n                                    `,\n                                  )}\n                          </div>\n                        </div>\n                      `\n                    : nothing}\n                </aside>\n              `\n            : nothing}\n        </div>\n\n        <uip-dialog\n          ?open=${this.dialogOpen}\n          heading=${this.selectedEvent?.title ?? ''}\n          description=${this.selectedEvent ? `${this.metaFor(this.selectedEvent.type).label}${this.selectedEvent.status ? ` ${this.selectedEvent.status}` : ''}` : ''}\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.dialogOpen = e.detail.open\n          }}\n        >\n          ${this.selectedEvent\n            ? html`\n                <div class=\"space-y-3 py-2\">\n                  ${this.selectedEvent.description\n                    ? html`<p class=\"text-muted-foreground text-sm\">${this.selectedEvent.description}</p>`\n                    : nothing}\n                  <uip-separator></uip-separator>\n                  <div class=\"grid gap-2 text-sm\">\n                    <div class=\"flex items-center gap-2\">\n                      ${icon(Clock, 'clock', 'text-muted-foreground size-4')}\n                      <span> ${this.selectedEvent.date} ${this.selectedEvent.start} - ${this.selectedEvent.end} </span>\n                    </div>\n                    ${this.selectedEvent.location\n                      ? html`<div class=\"flex items-center gap-2\">\n                          ${icon(MapPin, 'map-pin', 'text-muted-foreground size-4')}\n                          <span>${this.selectedEvent.location}</span>\n                        </div>`\n                      : nothing}\n                    ${this.selectedEvent.attendees?.length\n                      ? html`<div class=\"flex items-start gap-2\">\n                          ${icon(Users, 'users', 'text-muted-foreground mt-0.5 size-4')}\n                          <div class=\"flex flex-wrap items-center gap-1.5\">\n                            ${this.selectedEvent.attendees.map(\n                              (a) => html`\n                                <div class=\"bg-muted flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs\">\n                                  <uip-avatar class=\"size-4\">\n                                    <uip-avatar-fallback class=\"bg-primary/10 text-primary text-xs\">\n                                      ${this.initials(a)}\n                                    </uip-avatar-fallback>\n                                  </uip-avatar>\n                                  <span>${a}</span>\n                                </div>\n                              `,\n                            )}\n                          </div>\n                        </div>`\n                      : nothing}\n                  </div>\n                </div>\n                <div slot=\"footer\">\n                  <uip-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    @click=${() => {\n                      this.dispatchEvent(\n                        new CustomEvent('event-edit', { detail: this.selectedEvent, bubbles: true, composed: true }),\n                      )\n                      this.dialogOpen = false\n                    }}\n                  >\n                    ${icon(Pencil, 'pencil', 'size-3.5')} Edit\n                  </uip-button>\n                  <uip-button\n                    size=\"sm\"\n                    variant=\"destructive\"\n                    @click=${() => {\n                      this.dispatchEvent(\n                        new CustomEvent('event-delete', { detail: this.selectedEvent, bubbles: true, composed: true }),\n                      )\n                      this.dialogOpen = false\n                    }}\n                  >\n                    ${icon(Trash2, 'trash-2', 'size-3.5')} Cancel event\n                  </uip-button>\n                </div>\n              `\n            : nothing}\n        </uip-dialog>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-event-calendar-dashboard') ||\n  customElements.define('uip-event-calendar-dashboard', UipEventCalendarDashboard)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-event-calendar-dashboard': UipEventCalendarDashboard\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/event-calendar-dashboard/event-calendar-dashboard.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/avatar.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/dialog.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/select.json",
    "https://uipkge.dev/r/lit/separator.json",
    "https://uipkge.dev/r/lit/skeleton.json"
  ]
}
