{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "event-calendar",
  "title": "Event Calendar",
  "type": "registry:ui",
  "description": "Full-featured event calendar primitive supporting month, week, day, work-week, and category/resource views, timed interval grids with collision clustering, all-day header, live current-time indicator, and customizable event templates.",
  "categories": [
    "productivity",
    "dashboard"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/event-calendar/date-utils.ts",
      "content": "import type { CalendarEvent, PositionedEvent } from './types'\n\nexport function parseDate(d: string | Date | undefined | null): Date {\n  if (!d) return new Date()\n  if (d instanceof Date) return new Date(d.getTime())\n\n  // Format: YYYY-MM-DD\n  if (/^\\d{4}-\\d{2}-\\d{2}$/.test(d)) {\n    const [y, m, day] = d.split('-').map(Number)\n    return new Date(y, m - 1, day, 0, 0, 0)\n  }\n\n  // Format: YYYY-MM-DD HH:mm or YYYY-MM-DDTHH:mm\n  const matchTime = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})[T ](\\d{2}):(\\d{2})/)\n  if (matchTime) {\n    const [, y, m, day, h, min] = matchTime.map(Number)\n    return new Date(y, m - 1, day, h, min, 0)\n  }\n\n  const parsed = new Date(d)\n  return isNaN(parsed.getTime()) ? new Date() : parsed\n}\n\nexport function formatDateKey(d: Date): string {\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 isSameDay(d1: Date, d2: Date): boolean {\n  return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate()\n}\n\nexport function isToday(d: Date): boolean {\n  return isSameDay(d, new Date())\n}\n\nexport function formatTime(minutes: number, format: '12h' | '24h' = '12h'): string {\n  const totalMin = Math.max(0, Math.min(1439, Math.round(minutes)))\n  const hour = Math.floor(totalMin / 60)\n  const min = totalMin % 60\n\n  if (format === '24h') {\n    return `${String(hour).padStart(2, '0')}:${String(min).padStart(2, '0')}`\n  }\n\n  const ampm = hour >= 12 ? 'PM' : 'AM'\n  const h12 = hour % 12 || 12\n  return `${h12}:${String(min).padStart(2, '0')} ${ampm}`\n}\n\nexport function formatHourLabel(hour: number, format: '12h' | '24h' = '12h'): string {\n  const normalizedHour = ((hour % 24) + 24) % 24\n  if (format === '24h') {\n    return `${String(normalizedHour).padStart(2, '0')}:00`\n  }\n  if (normalizedHour === 0) return '12 AM'\n  if (normalizedHour === 12) return '12 PM'\n  return normalizedHour > 12 ? `${normalizedHour - 12} PM` : `${normalizedHour} AM`\n}\n\nexport function getEventMinutes(dateInput: string | Date | undefined, defaultMinutes = 0): number {\n  if (!dateInput) return defaultMinutes\n  if (dateInput instanceof Date) {\n    return dateInput.getHours() * 60 + dateInput.getMinutes()\n  }\n  if (typeof dateInput === 'string') {\n    if (/^\\d{1,2}:\\d{2}$/.test(dateInput)) {\n      const [h, m] = dateInput.split(':').map(Number)\n      return h * 60 + m\n    }\n    const parsed = parseDate(dateInput)\n    if (/^\\d{4}-\\d{2}-\\d{2}$/.test(dateInput)) {\n      return defaultMinutes\n    }\n    return parsed.getHours() * 60 + parsed.getMinutes()\n  }\n  return defaultMinutes\n}\n\nexport function getWeekDays(baseDate: Date, weekStartsOn: 0 | 1 = 0): Date[] {\n  const date = parseDate(baseDate)\n  const day = date.getDay()\n  const diff = (day - weekStartsOn + 7) % 7\n  const startOfWeek = new Date(date.getFullYear(), date.getMonth(), date.getDate() - diff)\n\n  const days: Date[] = []\n  for (let i = 0; i < 7; i++) {\n    const d = new Date(startOfWeek)\n    d.setDate(startOfWeek.getDate() + i)\n    days.push(d)\n  }\n  return days\n}\n\nexport function getWorkWeekDays(baseDate: Date): Date[] {\n  const date = parseDate(baseDate)\n  const day = date.getDay()\n  const diff = (day - 1 + 7) % 7\n  const monday = new Date(date.getFullYear(), date.getMonth(), date.getDate() - diff)\n\n  const days: Date[] = []\n  for (let i = 0; i < 5; i++) {\n    const d = new Date(monday)\n    d.setDate(monday.getDate() + i)\n    days.push(d)\n  }\n  return days\n}\n\nexport function getMonthDays(\n  cursor: Date,\n  weekStartsOn: 0 | 1 = 0,\n): { date: Date; dateKey: string; inMonth: boolean; isToday: boolean }[] {\n  const y = cursor.getFullYear()\n  const m = cursor.getMonth()\n  const firstDay = new Date(y, m, 1)\n  const offset = (firstDay.getDay() - weekStartsOn + 7) % 7\n\n  const startDate = new Date(y, m, 1 - offset)\n  const days: { date: Date; dateKey: string; inMonth: boolean; isToday: boolean }[] = []\n\n  for (let i = 0; i < 42; i++) {\n    const d = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + i)\n    days.push({\n      date: d,\n      dateKey: formatDateKey(d),\n      inMonth: d.getMonth() === m,\n      isToday: isToday(d),\n    })\n  }\n  return days\n}\n\nexport function calculateTimedEventPositions(\n  events: CalendarEvent[],\n  dayDate: Date,\n  firstInterval = 0,\n  intervalCount = 24,\n  intervalMinutes = 60,\n): PositionedEvent[] {\n  const dayKey = formatDateKey(dayDate)\n  const startDayMinutes = firstInterval * 60\n  const totalMinutes = intervalCount * intervalMinutes\n\n  const timed = events\n    .filter((e) => {\n      if (e.allDay) return false\n      const sKey =\n        typeof e.start === 'string' && /^\\d{4}-\\d{2}-\\d{2}/.test(e.start)\n          ? e.start.slice(0, 10)\n          : formatDateKey(parseDate(e.start))\n      return sKey === dayKey\n    })\n    .map((event) => {\n      const sMin = getEventMinutes(event.start, 9 * 60)\n      let eMin = getEventMinutes(event.end, sMin + 60)\n      if (eMin <= sMin) eMin = sMin + 30\n      return {\n        event,\n        startMinutes: sMin,\n        endMinutes: eMin,\n      }\n    })\n    .sort((a, b) => a.startMinutes - b.startMinutes || b.endMinutes - b.startMinutes - (a.endMinutes - a.startMinutes))\n\n  if (timed.length === 0) return []\n\n  type ClusterItem = { event: CalendarEvent; startMinutes: number; endMinutes: number; col: number }\n  const clusters: ClusterItem[][] = []\n  let currentCluster: ClusterItem[] = []\n  let clusterEnd = -1\n\n  for (const item of timed) {\n    if (currentCluster.length === 0 || item.startMinutes < clusterEnd) {\n      currentCluster.push({ ...item, col: 0 })\n      clusterEnd = Math.max(clusterEnd, item.endMinutes)\n    } else {\n      clusters.push(currentCluster)\n      currentCluster = [{ ...item, col: 0 }]\n      clusterEnd = item.endMinutes\n    }\n  }\n  if (currentCluster.length > 0) {\n    clusters.push(currentCluster)\n  }\n\n  const positioned: PositionedEvent[] = []\n  for (const cluster of clusters) {\n    const cols: number[] = []\n    for (const item of cluster) {\n      let placedCol = -1\n      for (let c = 0; c < cols.length; c++) {\n        if (item.startMinutes >= cols[c]) {\n          placedCol = c\n          cols[c] = item.endMinutes\n          break\n        }\n      }\n      if (placedCol === -1) {\n        placedCol = cols.length\n        cols.push(item.endMinutes)\n      }\n      item.col = placedCol\n    }\n\n    const totalCols = Math.max(1, cols.length)\n    for (const item of cluster) {\n      const topPct = Math.max(0, Math.min(100, ((item.startMinutes - startDayMinutes) / totalMinutes) * 100))\n      const bottomPct = Math.max(0, Math.min(100, ((item.endMinutes - startDayMinutes) / totalMinutes) * 100))\n      const heightPct = Math.max(1.5, bottomPct - topPct)\n      const widthPct = 100 / totalCols\n      const leftPct = item.col * widthPct\n\n      positioned.push({\n        event: item.event,\n        top: topPct,\n        height: heightPct,\n        left: leftPct,\n        width: widthPct,\n        startMinutes: item.startMinutes,\n        endMinutes: item.endMinutes,\n      })\n    }\n  }\n\n  return positioned\n}\n\nexport function getCurrentTimePosition(\n  firstInterval = 0,\n  intervalCount = 24,\n  intervalMinutes = 60,\n): number | null {\n  const now = new Date()\n  const curMinutes = now.getHours() * 60 + now.getMinutes()\n  const startMinutes = firstInterval * 60\n  const totalMinutes = intervalCount * intervalMinutes\n\n  if (curMinutes < startMinutes || curMinutes > startMinutes + totalMinutes) {\n    return null\n  }\n  return ((curMinutes - startMinutes) / totalMinutes) * 100\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/event-calendar/date-utils.ts"
    },
    {
      "path": "packages/elements-lit/src/components/event-calendar/event-calendar.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ChevronLeft, ChevronRight, Clock } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport type { CalendarCategory, CalendarEvent, CalendarView, PositionedEvent, TimeClickPayload } from './types'\nimport {\n  calculateTimedEventPositions,\n  formatDateKey,\n  formatHourLabel,\n  formatTime,\n  getMonthDays,\n  getWeekDays,\n  getWorkWeekDays,\n  isSameDay,\n  isToday,\n  parseDate,\n} from './date-utils'\nimport { calendarEventVariants } from './event-calendar.variants'\n\nexport type { CalendarCategory, CalendarEvent, CalendarView, TimeClickPayload } from './types'\n\n/**\n * <uip-event-calendar> — Full-featured event calendar supporting month, week,\n * work-week, day, and category/resource views with timed event positioning.\n */\nexport class UipEventCalendar extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    value: { attribute: false },\n    defaultValue: { attribute: false },\n    view: { type: String },\n    defaultView: { type: String, attribute: 'default-view' },\n    events: { type: Array, attribute: false },\n    categories: { type: Array, attribute: false },\n    weekStartsOn: { type: Number, attribute: 'week-starts-on' },\n    firstInterval: { type: Number, attribute: 'first-interval' },\n    intervalCount: { type: Number, attribute: 'interval-count' },\n    intervalMinutes: { type: Number, attribute: 'interval-minutes' },\n    intervalHeight: { type: Number, attribute: 'interval-height' },\n    timeFormat: { type: String, attribute: 'time-format' },\n    maxEventsPerDay: { type: Number, attribute: 'max-events-per-day' },\n    showNowIndicator: { type: Boolean, attribute: 'show-now-indicator' },\n    showHeader: {\n      type: Boolean,\n      attribute: 'show-header',\n      converter: { fromAttribute: (v: string | null) => v !== null && v !== 'false' },\n    },\n    renderEvent: { attribute: false },\n    activeDate: { state: true },\n    activeView: { state: true },\n  }\n\n  value?: string | Date\n  defaultValue?: string | Date\n  view?: CalendarView\n  defaultView: CalendarView = 'month'\n  events: CalendarEvent[] = []\n  categories: (string | CalendarCategory)[] = []\n  weekStartsOn: 0 | 1 = 0\n  firstInterval = 0\n  intervalCount = 24\n  intervalMinutes = 60\n  intervalHeight = 52\n  timeFormat: '12h' | '24h' = '12h'\n  maxEventsPerDay = 3\n  showNowIndicator = true\n  showHeader = true\n  renderEvent?: (params: { event: CalendarEvent; view: CalendarView; isAllDay: boolean }) => unknown\n\n  activeDate: Date = new Date()\n  activeView: CalendarView = 'month'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'event-calendar')\n    this.activeDate = parseDate(this.value ?? this.defaultValue ?? new Date())\n    this.activeView = this.view ?? this.defaultView\n  }\n\n  updated(changedProperties: Map<string, unknown>) {\n    if (changedProperties.has('value') && this.value !== undefined) {\n      this.activeDate = parseDate(this.value)\n    }\n    if (changedProperties.has('view') && this.view !== undefined) {\n      this.activeView = this.view\n    }\n  }\n\n  private setDate(d: Date) {\n    this.activeDate = d\n    this.dispatchEvent(new CustomEvent('date-change', { detail: { date: d }, bubbles: true, composed: true }))\n    this.requestUpdate()\n  }\n\n  private setView(v: CalendarView) {\n    this.activeView = v\n    this.dispatchEvent(new CustomEvent('view-change', { detail: { view: v }, bubbles: true, composed: true }))\n    this.requestUpdate()\n  }\n\n  private prev() {\n    const d = new Date(this.activeDate)\n    if (this.activeView === 'month') d.setMonth(d.getMonth() - 1)\n    else if (this.activeView === 'week' || this.activeView === 'work-week') d.setDate(d.getDate() - 7)\n    else d.setDate(d.getDate() - 1)\n    this.setDate(d)\n  }\n\n  private next() {\n    const d = new Date(this.activeDate)\n    if (this.activeView === 'month') d.setMonth(d.getMonth() + 1)\n    else if (this.activeView === 'week' || this.activeView === 'work-week') d.setDate(d.getDate() + 7)\n    else d.setDate(d.getDate() + 1)\n    this.setDate(d)\n  }\n\n  private today() {\n    this.setDate(new Date())\n  }\n\n  private onEventClick(event: CalendarEvent, e: Event) {\n    e.stopPropagation()\n    this.dispatchEvent(new CustomEvent('event-click', { detail: { event }, bubbles: true, composed: true }))\n  }\n\n  private onDateClick(dateKey: string) {\n    this.dispatchEvent(new CustomEvent('date-click', { detail: { date: dateKey }, bubbles: true, composed: true }))\n  }\n\n  private renderCalendarHeader() {\n    const title = (() => {\n      const locale = 'en-US'\n      if (this.activeView === 'month') {\n        return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(this.activeDate)\n      }\n      if (this.activeView === 'week' || this.activeView === 'work-week') {\n        const days = this.activeView === 'week' ? getWeekDays(this.activeDate, this.weekStartsOn) : getWorkWeekDays(this.activeDate)\n        const first = days[0]!\n        const last = days[days.length - 1]!\n        return `${first.toLocaleDateString(locale, { month: 'short', day: 'numeric' })} – ${last.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })}`\n      }\n      return new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }).format(this.activeDate)\n    })()\n\n    const views: CalendarView[] = ['month', 'week', 'day', 'category']\n\n    return html`\n      <div class=\"flex flex-wrap items-center justify-between gap-3 border-b border-border pb-3\">\n        <div class=\"flex items-center gap-2\">\n          <div class=\"flex items-center gap-1\">\n            <button\n              type=\"button\"\n              class=\"border border-input hover:bg-accent text-foreground size-8 inline-flex items-center justify-center rounded-md transition-colors\"\n              aria-label=\"Previous\"\n              @click=${this.prev}\n            >\n              ${icon(ChevronLeft, 'chevron-left', 'size-4')}\n            </button>\n            <button\n              type=\"button\"\n              class=\"border border-input hover:bg-accent text-foreground size-8 inline-flex items-center justify-center rounded-md transition-colors\"\n              aria-label=\"Next\"\n              @click=${this.next}\n            >\n              ${icon(ChevronRight, 'chevron-right', 'size-4')}\n            </button>\n          </div>\n          <button\n            type=\"button\"\n            class=\"border border-input hover:bg-accent text-foreground h-8 px-2.5 rounded-md text-xs font-medium transition-colors\"\n            @click=${this.today}\n          >\n            Today\n          </button>\n          <span class=\"text-base font-semibold tracking-tight ml-2\">${title}</span>\n        </div>\n\n        <div class=\"flex items-center rounded-md border border-input p-0.5 text-xs font-medium bg-muted/30\">\n          ${views.map(\n            (v) => html`\n              <button\n                type=\"button\"\n                class=${cn(\n                  'rounded px-2.5 py-1 capitalize transition-colors',\n                  this.activeView === v\n                    ? 'bg-background text-foreground shadow-xs'\n                    : 'text-muted-foreground hover:text-foreground',\n                )}\n                @click=${() => this.setView(v)}\n              >\n                ${v}\n              </button>\n            `,\n          )}\n        </div>\n      </div>\n    `\n  }\n\n  private renderMonthView() {\n    const days = getMonthDays(this.activeDate, this.weekStartsOn)\n    const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n      .slice(this.weekStartsOn)\n      .concat(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].slice(0, this.weekStartsOn))\n\n    return html`\n      <div class=\"flex flex-col\">\n        <!-- Weekday header -->\n        <div class=\"grid grid-cols-7 border-b border-border bg-muted/40 text-center text-xs font-medium text-muted-foreground\">\n          ${weekdays.map((wd) => html`<div class=\"py-2\">${wd}</div>`)}\n        </div>\n\n        <!-- Month Grid -->\n        <div class=\"grid grid-cols-7 border-b border-l border-border\">\n          ${days.map(({ date, dateKey, inMonth, isToday: today }) => {\n            const dayEvents = this.events.filter((e) => {\n              const sKey = typeof e.start === 'string' ? e.start.slice(0, 10) : formatDateKey(parseDate(e.start))\n              return sKey === dateKey\n            })\n\n            const visible = dayEvents.slice(0, this.maxEventsPerDay)\n            const remaining = dayEvents.length - visible.length\n\n            return html`\n              <div\n                class=${cn(\n                  'min-h-[100px] border-r border-t border-border p-1.5 flex flex-col gap-1 transition-colors',\n                  !inMonth && 'bg-muted/20 text-muted-foreground/50',\n                )}\n                @click=${() => this.onDateClick(dateKey)}\n              >\n                <div class=\"flex items-center justify-between\">\n                  <span\n                    class=${cn(\n                      'size-6 flex items-center justify-center rounded-full text-xs font-medium select-none',\n                      today && 'bg-primary text-primary-foreground font-bold',\n                    )}\n                  >\n                    ${date.getDate()}\n                  </span>\n                </div>\n\n                <div class=\"flex flex-col gap-1 flex-1\">\n                  ${visible.map((event) => {\n                    if (this.renderEvent) {\n                      return html`<div @click=${(e: Event) => this.onEventClick(event, e)}>\n                        ${this.renderEvent({ event, view: 'month', isAllDay: Boolean(event.allDay) })}\n                      </div>`\n                    }\n                    const variant = (event.color as any) ?? 'default'\n                    return html`\n                      <button\n                        type=\"button\"\n                        class=${cn(calendarEventVariants({ variant, size: 'sm' }), 'truncate')}\n                        @click=${(e: Event) => this.onEventClick(event, e)}\n                      >\n                        <span class=\"truncate font-medium\">${event.title}</span>\n                      </button>\n                    `\n                  })}\n                  ${remaining > 0\n                    ? html`<span class=\"text-[10px] text-muted-foreground font-medium pl-1\">+${remaining} more</span>`\n                    : nothing}\n                </div>\n              </div>\n            `\n          })}\n        </div>\n      </div>\n    `\n  }\n\n  private renderTimeGrid(days: Date[]) {\n    const intervals = Array.from({ length: this.intervalCount }, (_, i) => this.firstInterval + i)\n\n    return html`\n      <div class=\"flex flex-col overflow-x-auto\">\n        <!-- Day header row -->\n        <div class=\"flex border-b border-border bg-muted/40\">\n          <div class=\"w-16 shrink-0 border-r border-border\"></div>\n          ${days.map(\n            (d) => html`\n              <div class=\"flex-1 py-2 text-center text-xs font-medium border-r border-border last:border-r-0\">\n                <span class=${cn(isToday(d) && 'text-primary font-bold')}>\n                  ${d.toLocaleDateString('en-US', { weekday: 'short', month: 'numeric', day: 'numeric' })}\n                </span>\n              </div>\n            `,\n          )}\n        </div>\n\n        <!-- Interval rows -->\n        <div class=\"relative flex\">\n          <!-- Time labels column -->\n          <div class=\"w-16 shrink-0 flex flex-col text-right pr-2 text-xs text-muted-foreground border-r border-border select-none\">\n            ${intervals.map(\n              (hour) => html`\n                <div style=\"height: ${this.intervalHeight}px;\" class=\"flex items-start justify-end pt-1\">\n                  ${formatHourLabel(hour, this.timeFormat)}\n                </div>\n              `,\n            )}\n          </div>\n\n          <!-- Day columns with positioned events -->\n          ${days.map((day) => {\n            const positioned = calculateTimedEventPositions(\n              this.events,\n              day,\n              this.firstInterval,\n              this.intervalCount,\n              this.intervalMinutes,\n            )\n\n            return html`\n              <div class=\"relative flex-1 border-r border-border last:border-r-0\">\n                <!-- Interval grid lines -->\n                ${intervals.map(\n                  () => html`\n                    <div\n                      style=\"height: ${this.intervalHeight}px;\"\n                      class=\"border-b border-border/50 hover:bg-muted/10 cursor-pointer\"\n                    ></div>\n                  `,\n                )}\n\n                <!-- Positioned Events -->\n                ${positioned.map(({ event, top, height, left, width }) => {\n                  const variant = (event.color as any) ?? 'default'\n                  return html`\n                    <button\n                      type=\"button\"\n                      class=${cn(\n                        calendarEventVariants({ variant, size: 'sm' }),\n                        'absolute rounded p-1 text-xs transition-opacity',\n                      )}\n                      style=\"top: ${top}%; height: ${height}%; left: ${left}%; width: calc(${width}% - 2px);\"\n                      @click=${(e: Event) => this.onEventClick(event, e)}\n                    >\n                      <p class=\"font-semibold truncate\">${event.title}</p>\n                      ${event.location\n                        ? html`<p class=\"truncate opacity-75 text-[10px]\">${event.location}</p>`\n                        : nothing}\n                    </button>\n                  `\n                })}\n              </div>\n            `\n          })}\n        </div>\n      </div>\n    `\n  }\n\n  private renderCategoryView() {\n    const cats: CalendarCategory[] = this.categories.map((c, idx) =>\n      typeof c === 'string' ? { id: c, name: c } : c,\n    )\n    const intervals = Array.from({ length: this.intervalCount }, (_, i) => this.firstInterval + i)\n    const day = this.activeDate\n\n    return html`\n      <div class=\"flex flex-col overflow-x-auto\">\n        <!-- Category header row -->\n        <div class=\"flex border-b border-border bg-muted/40\">\n          <div class=\"w-16 shrink-0 border-r border-border\"></div>\n          ${cats.map(\n            (cat) => html`\n              <div class=\"flex-1 py-2 text-center text-xs font-medium border-r border-border last:border-r-0\">\n                <span style=\"color: ${cat.color ?? 'inherit'};\" class=\"font-bold\">${cat.name}</span>\n              </div>\n            `,\n          )}\n        </div>\n\n        <!-- Interval rows -->\n        <div class=\"relative flex\">\n          <!-- Time labels -->\n          <div class=\"w-16 shrink-0 flex flex-col text-right pr-2 text-xs text-muted-foreground border-r border-border select-none\">\n            ${intervals.map(\n              (hour) => html`\n                <div style=\"height: ${this.intervalHeight}px;\" class=\"flex items-start justify-end pt-1\">\n                  ${formatHourLabel(hour, this.timeFormat)}\n                </div>\n              `,\n            )}\n          </div>\n\n          <!-- Category columns -->\n          ${cats.map((cat) => {\n            const catEvents = this.events.filter((e) => e.category === cat.id)\n            const positioned = calculateTimedEventPositions(\n              catEvents,\n              day,\n              this.firstInterval,\n              this.intervalCount,\n              this.intervalMinutes,\n            )\n\n            return html`\n              <div class=\"relative flex-1 border-r border-border last:border-r-0\">\n                ${intervals.map(\n                  () => html`\n                    <div\n                      style=\"height: ${this.intervalHeight}px;\"\n                      class=\"border-b border-border/50 hover:bg-muted/10 cursor-pointer\"\n                    ></div>\n                  `,\n                )}\n\n                <!-- Positioned Events -->\n                ${positioned.map(({ event, top, height, left, width }) => {\n                  const variant = (event.color as any) ?? 'default'\n                  return html`\n                    <button\n                      type=\"button\"\n                      class=${cn(\n                        calendarEventVariants({ variant, size: 'sm' }),\n                        'absolute rounded p-1 text-xs transition-opacity',\n                      )}\n                      style=\"top: ${top}%; height: ${height}%; left: ${left}%; width: calc(${width}% - 2px);\"\n                      @click=${(e: Event) => this.onEventClick(event, e)}\n                    >\n                      <p class=\"font-semibold truncate\">${event.title}</p>\n                      ${event.location\n                        ? html`<p class=\"truncate opacity-75 text-[10px]\">${event.location}</p>`\n                        : nothing}\n                    </button>\n                  `\n                })}\n              </div>\n            `\n          })}\n        </div>\n      </div>\n    `\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"flex flex-col gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground shadow-xs\">\n        ${this.showHeader ? this.renderCalendarHeader() : nothing}\n        ${this.activeView === 'month'\n          ? this.renderMonthView()\n          : this.activeView === 'category'\n            ? this.renderCategoryView()\n            : this.activeView === 'work-week'\n              ? this.renderTimeGrid(getWorkWeekDays(this.activeDate))\n              : this.activeView === 'day'\n                ? this.renderTimeGrid([this.activeDate])\n                : this.renderTimeGrid(getWeekDays(this.activeDate, this.weekStartsOn))}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-event-calendar') || customElements.define('uip-event-calendar', UipEventCalendar)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-event-calendar': UipEventCalendar\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/event-calendar/event-calendar.ts"
    },
    {
      "path": "packages/elements-lit/src/components/event-calendar/event-calendar.variants.ts",
      "content": "import { cva, type VariantProps } from 'class-variance-authority'\n\nexport const calendarEventVariants = cva(\n  'group relative flex cursor-pointer flex-col overflow-hidden text-left transition-colors select-none border font-normal focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring',\n  {\n    variants: {\n      variant: {\n        default: 'bg-secondary/80 text-secondary-foreground border-border/50 hover:bg-secondary',\n        primary: 'bg-primary/10 text-primary border-primary/25 hover:bg-primary/20',\n        secondary: 'bg-muted/90 text-foreground border-border/60 hover:bg-muted',\n        success:\n          'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/30 hover:bg-emerald-500/20',\n        warning: 'bg-amber-500/10 text-amber-800 dark:text-amber-400 border-amber-500/30 hover:bg-amber-500/20',\n        destructive: 'bg-destructive/10 text-destructive border-destructive/30 hover:bg-destructive/20',\n        info: 'bg-sky-500/10 text-sky-700 dark:text-sky-400 border-sky-500/30 hover:bg-sky-500/20',\n        purple: 'bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/30 hover:bg-purple-500/20',\n        rose: 'bg-rose-500/10 text-rose-700 dark:text-rose-400 border-rose-500/30 hover:bg-rose-500/20',\n      },\n      size: {\n        sm: 'rounded-sm px-1.5 py-0.5 text-[11px] leading-tight',\n        default: 'rounded-md px-2 py-1 text-xs leading-normal',\n        lg: 'rounded-md px-2.5 py-1.5 text-sm leading-snug',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n)\n\nexport type CalendarEventVariants = VariantProps<typeof calendarEventVariants>\n",
      "type": "registry:file",
      "target": "src/uip/components/event-calendar/event-calendar.variants.ts"
    },
    {
      "path": "packages/elements-lit/src/components/event-calendar/types.ts",
      "content": "export type CalendarView = 'month' | 'week' | 'day' | 'work-week' | 'category'\n\nexport interface CalendarCategory {\n  id: string\n  name: string\n  color?: string\n}\n\nexport interface CalendarEvent {\n  id?: string\n  title: string\n  start: string | Date\n  end?: string | Date\n  allDay?: boolean\n  color?:\n    | 'default'\n    | 'primary'\n    | 'secondary'\n    | 'success'\n    | 'warning'\n    | 'destructive'\n    | 'info'\n    | 'purple'\n    | 'rose'\n    | string\n  category?: string\n  description?: string\n  location?: string\n  [key: string]: any\n}\n\nexport interface TimeClickPayload {\n  date: string\n  time: string\n  hour: number\n  minute: number\n  category?: string\n}\n\nexport interface PositionedEvent {\n  event: CalendarEvent\n  top: number\n  height: number\n  left: number\n  width: number\n  startMinutes: number\n  endMinutes: number\n}\n\nexport interface CalendarDayCell {\n  date: Date\n  dateKey: string\n  dayNumber: number\n  inMonth: boolean\n  isToday: boolean\n  events: CalendarEvent[]\n  allDayEvents: CalendarEvent[]\n  timedEvents: CalendarEvent[]\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/event-calendar/types.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
