{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "event-calendar",
  "title": "Event Calendar",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/event-calendar/EventCalendar.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { CalendarCategory, CalendarEvent, CalendarView, TimeClickPayload } from './types'\n\n  export interface EventCalendarProps extends HTMLAttributes<HTMLDivElement> {\n    /** Active date cursor. Use `bind:value` for two-way binding. */\n    value?: string | Date\n    /** Active view. Use `bind:view` for two-way binding. */\n    view?: CalendarView\n    events?: CalendarEvent[]\n    categories?: (string | CalendarCategory)[]\n    weekStartsOn?: 0 | 1\n    firstInterval?: number\n    intervalCount?: number\n    intervalMinutes?: number\n    intervalHeight?: number\n    timeFormat?: '12h' | '24h'\n    maxEventsPerDay?: number\n    showNowIndicator?: boolean\n    showHeader?: boolean\n    /** Fired with the new cursor date whenever navigation changes it (React parity — `bind:value` still works). */\n    onDateChange?: (date: Date) => void\n    /** Fired with the new view whenever the view changes (React parity — `bind:view` still works). */\n    onViewChange?: (view: CalendarView) => void\n    onEventClick?: (event: CalendarEvent) => void\n    onDateClick?: (date: string) => void\n    onTimeClick?: (payload: TimeClickPayload) => void\n    onMoreClick?: (payload: { date: string; events: CalendarEvent[] }) => void\n    /** Custom header toolbar. Receives the cursor, view, title, and navigation actions. */\n    header?: Snippet<\n      [\n        {\n          currentDate: Date\n          view: CalendarView\n          title: string\n          prev: () => void\n          next: () => void\n          today: () => void\n          setView: (view: CalendarView) => void\n        },\n      ]\n    >\n    /** Extra actions rendered inside the default header, before the view switcher. */\n    headerActions?: Snippet\n    /** Custom event card. */\n    event?: Snippet<[{ event: CalendarEvent; view: CalendarView; isAllDay: boolean }]>\n    /** Custom week/day column header. */\n    dayHeader?: Snippet<[{ date: Date; dateKey: string; isToday: boolean; view: CalendarView }]>\n    /** Custom all-day row content. */\n    allDay?: Snippet<[{ date: Date; dateKey: string; events: CalendarEvent[] }]>\n    /** Custom time-gutter label. */\n    interval?: Snippet<[{ hour: number; time: string; label: string }]>\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { ChevronLeft, ChevronRight, Clock } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n  import { cn } from '$lib/utils'\n  import {\n    calculateTimedEventPositions,\n    formatDateKey,\n    formatHourLabel,\n    formatTime,\n    getCurrentTimePosition,\n    getEventMinutes,\n    getMonthDays,\n    getWeekDays,\n    getWorkWeekDays,\n    isToday,\n    parseDate,\n  } from './date-utils'\n  import { calendarEventVariants, type CalendarEventVariants } from './event-calendar.variants'\n\n  let {\n    class: className,\n    value = $bindable(new Date()),\n    view = $bindable<CalendarView>('month'),\n    events = [],\n    categories = [],\n    weekStartsOn = 0,\n    firstInterval = 0,\n    intervalCount = 24,\n    intervalMinutes = 60,\n    intervalHeight = 52,\n    timeFormat = '12h',\n    maxEventsPerDay = 3,\n    showNowIndicator = true,\n    showHeader = true,\n    onDateChange,\n    onViewChange,\n    onEventClick,\n    onDateClick,\n    onTimeClick,\n    onMoreClick,\n    header,\n    headerActions,\n    event: eventSnippet,\n    dayHeader,\n    allDay,\n    interval: intervalSnippet,\n    ref = $bindable(null),\n    ...restProps\n  }: EventCalendarProps = $props()\n\n  // Active date cursor\n  const activeDate = $derived(parseDate(value))\n\n  function setDate(d: Date) {\n    value = d\n    onDateChange?.(d)\n  }\n\n  function setView(v: CalendarView) {\n    view = v\n    onViewChange?.(v)\n  }\n\n  // Normalized categories\n  const normalizedCategories = $derived(\n    categories.map((c, idx) => {\n      if (typeof c === 'string') return { id: c, name: c }\n      return { id: c.id || `cat-${idx}`, name: c.name || `Category ${idx + 1}`, color: c.color }\n    }),\n  )\n\n  // Current time line update timer\n  let nowPosition = $state<number | null>(null)\n\n  $effect(() => {\n    const update = () => {\n      nowPosition = showNowIndicator ? getCurrentTimePosition(firstInterval, intervalCount, intervalMinutes) : null\n    }\n    update()\n    const timer = setInterval(update, 30000)\n    return () => clearInterval(timer)\n  })\n\n  // \"+N more\" overflow panel (hand-rolled inline panel; the Vue twin uses Popover).\n  let moreOpenKey = $state<string | null>(null)\n\n  $effect(() => {\n    if (moreOpenKey === null) return\n    function onPointerDown(e: PointerEvent) {\n      if ((e.target as Element).closest?.('[data-slot=\"more-popover\"]')) return\n      moreOpenKey = null\n    }\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') moreOpenKey = null\n    }\n    document.addEventListener('pointerdown', onPointerDown, true)\n    document.addEventListener('keydown', onKeyDown)\n    return () => {\n      document.removeEventListener('pointerdown', onPointerDown, true)\n      document.removeEventListener('keydown', onKeyDown)\n    }\n  })\n\n  // Navigation controls\n  function handlePrev() {\n    const d = new Date(activeDate)\n    if (view === 'month') {\n      d.setMonth(d.getMonth() - 1)\n    } else if (view === 'week' || view === 'work-week') {\n      d.setDate(d.getDate() - 7)\n    } else if (view === 'day' || view === 'category') {\n      d.setDate(d.getDate() - 1)\n    }\n    setDate(d)\n  }\n\n  function handleNext() {\n    const d = new Date(activeDate)\n    if (view === 'month') {\n      d.setMonth(d.getMonth() + 1)\n    } else if (view === 'week' || view === 'work-week') {\n      d.setDate(d.getDate() + 7)\n    } else if (view === 'day' || view === 'category') {\n      d.setDate(d.getDate() + 1)\n    }\n    setDate(d)\n  }\n\n  function handleToday() {\n    setDate(new Date())\n  }\n\n  // Header title calculation\n  const formattedTitle = $derived.by(() => {\n    const d = activeDate\n    if (view === 'month') {\n      return d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })\n    }\n    if (view === 'week') {\n      const days = getWeekDays(d, weekStartsOn)\n      const first = days[0]!\n      const last = days[6]!\n      if (first.getMonth() === last.getMonth()) {\n        return `${first.toLocaleDateString('en-US', { month: 'short' })} ${first.getDate()} – ${last.getDate()}, ${first.getFullYear()}`\n      }\n      return `${first.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${last.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`\n    }\n    if (view === 'work-week') {\n      const days = getWorkWeekDays(d)\n      const first = days[0]!\n      const last = days[4]!\n      return `${first.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${last.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`\n    }\n    // day / category\n    return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })\n  })\n\n  // Month View Data\n  const monthDays = $derived(getMonthDays(activeDate, weekStartsOn))\n\n  // Events grouped by start-day key, built once per `events` change (preserves input order per day).\n  const eventsByDay = $derived.by(() => {\n    const map = new Map<string, CalendarEvent[]>()\n    for (const e of events) {\n      const startStr =\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      const list = map.get(startStr)\n      if (list) list.push(e)\n      else map.set(startStr, [e])\n    }\n    return map\n  })\n\n  function getEventsForDay(dateKey: string): CalendarEvent[] {\n    return eventsByDay.get(dateKey) ?? []\n  }\n\n  // Interval array for time grid\n  const intervals = $derived.by(() => {\n    const list: { hour: number; label: string; time: string }[] = []\n    for (let i = 0; i < intervalCount; i++) {\n      const hour = (firstInterval + Math.floor((i * intervalMinutes) / 60)) % 24\n      const min = (i * intervalMinutes) % 60\n      const time = `${String(hour).padStart(2, '0')}:${String(min).padStart(2, '0')}`\n      const label = min === 0 ? formatHourLabel(hour, timeFormat) : ''\n      list.push({ hour, label, time })\n    }\n    return list\n  })\n\n  // Week Days\n  const weekDays = $derived(getWeekDays(activeDate, weekStartsOn))\n  // Work Week Days\n  const workWeekDays = $derived(getWorkWeekDays(activeDate))\n\n  // Columns for the week / work-week / day time grid.\n  const timeGridDays = $derived(view === 'week' ? weekDays : view === 'work-week' ? workWeekDays : [activeDate])\n  const timeGridCols = $derived(view === 'week' ? 'grid-cols-7' : view === 'work-week' ? 'grid-cols-5' : 'grid-cols-1')\n\n  // All day events for a day\n  function getAllDayEventsForDay(dayDate: Date): CalendarEvent[] {\n    const dayKey = formatDateKey(dayDate)\n    return events.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  }\n\n  // Day header formatters\n  const weekdayHeaderLabels = $derived(\n    weekStartsOn === 1 ? ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],\n  )\n\n  const viewTabs: { id: CalendarView; label: string }[] = [\n    { id: 'month', label: 'Month' },\n    { id: 'week', label: 'Week' },\n    { id: 'work-week', label: 'Work' },\n    { id: 'day', label: 'Day' },\n    { id: 'category', label: 'Category' },\n  ]\n\n  function getEventVariant(event: CalendarEvent): NonNullable<CalendarEventVariants['variant']> {\n    if (\n      event.color === 'primary' ||\n      event.color === 'secondary' ||\n      event.color === 'success' ||\n      event.color === 'warning' ||\n      event.color === 'destructive' ||\n      event.color === 'info' ||\n      event.color === 'purple' ||\n      event.color === 'rose'\n    ) {\n      return event.color\n    }\n    return 'default'\n  }\n\n  function handleEventClick(event: CalendarEvent, e: MouseEvent) {\n    e.stopPropagation()\n    onEventClick?.(event)\n  }\n\n  function handleTimeClick(dayDate: Date, hour: number, minute: number, category?: string) {\n    const dateStr = formatDateKey(dayDate)\n    const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`\n    onTimeClick?.({ date: dateStr, time: timeStr, hour, minute, category })\n  }\n\n  function handleMoreClick(dateKey: string, dayEvents: CalendarEvent[], e: MouseEvent) {\n    e.stopPropagation()\n    onMoreClick?.({ date: dateKey, events: dayEvents })\n    moreOpenKey = moreOpenKey === dateKey ? null : dateKey\n  }\n</script>\n\n<div\n  bind:this={ref}\n  data-slot=\"event-calendar\"\n  class={cn(\n    'border-border bg-card text-foreground flex w-full flex-col overflow-hidden rounded-xl border shadow-xs',\n    className,\n  )}\n  {...restProps}\n>\n  <!-- Built-in Header Toolbar -->\n  {#if showHeader}\n    {#if header}\n      {@render header({\n        currentDate: activeDate,\n        view,\n        title: formattedTitle,\n        prev: handlePrev,\n        next: handleNext,\n        today: handleToday,\n        setView,\n      })}\n    {:else}\n      <header\n        class=\"border-border bg-card/60 flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3 backdrop-blur-xs\"\n      >\n        <div class=\"flex items-center gap-2\">\n          <Button variant=\"outline\" size=\"sm\" class=\"h-8 px-2.5 text-xs font-medium\" onclick={handleToday}>\n            Today\n          </Button>\n          <div class=\"flex items-center gap-0.5\">\n            <Button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Previous period\" onclick={handlePrev}>\n              <ChevronLeft class=\"size-4\" />\n            </Button>\n            <Button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Next period\" onclick={handleNext}>\n              <ChevronRight class=\"size-4\" />\n            </Button>\n          </div>\n          <h2 class=\"text-foreground ml-1 text-base font-semibold tracking-tight sm:text-lg\">\n            {formattedTitle}\n          </h2>\n        </div>\n\n        <div class=\"flex items-center gap-1.5\">\n          {@render headerActions?.()}\n          <div class=\"border-border bg-muted/40 flex items-center rounded-lg border p-0.5\">\n            {#each viewTabs as tab (tab.id)}\n              {#if tab.id !== 'category' || normalizedCategories.length > 0}\n                <button\n                  type=\"button\"\n                  class={cn(\n                    'rounded-md px-2.5 py-1 text-xs font-medium transition-[color,background-color,box-shadow]',\n                    view === tab.id\n                      ? 'bg-background text-foreground shadow-xs'\n                      : 'text-muted-foreground hover:text-foreground',\n                  )}\n                  onclick={() => setView(tab.id)}\n                >\n                  {tab.label}\n                </button>\n              {/if}\n            {/each}\n          </div>\n        </div>\n      </header>\n    {/if}\n  {/if}\n\n  <!-- VIEW 1: MONTH VIEW -->\n  {#if view === 'month'}\n    <div class=\"flex flex-1 flex-col\">\n      <!-- Weekday headers -->\n      <div\n        class=\"border-border bg-muted/20 text-muted-foreground grid grid-cols-7 border-b text-center text-xs font-medium\"\n      >\n        {#each weekdayHeaderLabels as dayName, idx (idx)}\n          <div class=\"border-border/40 border-r py-2 last:border-r-0\">\n            {dayName}\n          </div>\n        {/each}\n      </div>\n\n      <!-- 6-week month grid -->\n      <div class=\"divide-border/40 grid min-h-[580px] flex-1 grid-cols-7 grid-rows-6 divide-x divide-y\">\n        {#each monthDays as cell, cellIndex (cell.dateKey)}\n          {@const dayEvents = getEventsForDay(cell.dateKey)}\n          {@const visibleEvents = dayEvents.length <= maxEventsPerDay ? dayEvents : dayEvents.slice(0, maxEventsPerDay - 1)}\n          {@const hiddenCount = dayEvents.length - visibleEvents.length}\n          <div\n            class={cn(\n              'group relative flex min-h-[96px] cursor-pointer flex-col p-1.5 transition-colors',\n              cell.inMonth ? 'bg-card hover:bg-muted/15' : 'bg-muted/10 text-muted-foreground/50 hover:bg-muted/20',\n            )}\n            onclick={() => onDateClick?.(cell.dateKey)}\n          >\n            <!-- Cell Header: Day Number -->\n            <div class=\"mb-1 flex items-center justify-between\">\n              <span\n                class={cn(\n                  'inline-flex min-w-[20px] items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium transition-colors',\n                  cell.isToday\n                    ? 'bg-primary text-primary-foreground font-semibold shadow-xs'\n                    : cell.inMonth\n                      ? 'text-foreground/90'\n                      : 'text-muted-foreground/60',\n                )}\n              >\n                {cell.date.getDate()}\n              </span>\n            </div>\n\n            <!-- Events in Day Cell -->\n            <div class=\"flex flex-1 flex-col gap-1 overflow-hidden\">\n              {#each visibleEvents as evt, idx (evt.id || idx)}\n                {#if eventSnippet}\n                  {@render eventSnippet({ event: evt, view: 'month', isAllDay: Boolean(evt.allDay) })}\n                {:else}\n                  <div\n                    data-slot=\"event-card\"\n                    class={cn(calendarEventVariants({ variant: getEventVariant(evt), size: 'sm' }), 'w-full truncate')}\n                    onclick={(e) => handleEventClick(evt, e)}\n                  >\n                    <div class=\"flex items-center gap-1 truncate font-medium\">\n                      {#if !evt.allDay}\n                        <span class=\"shrink-0 font-mono text-[10px] opacity-75\">\n                          {formatTime(getEventMinutes(evt.start, 540), timeFormat)}\n                        </span>\n                      {/if}\n                      <span class=\"truncate\">{evt.title}</span>\n                    </div>\n                  </div>\n                {/if}\n              {/each}\n            </div>\n\n            <!-- +N more button with panel -->\n            {#if hiddenCount > 0}\n              <div class=\"mt-auto pt-0.5\" data-slot=\"more-popover\">\n                <button\n                  type=\"button\"\n                  class=\"text-primary hover:text-primary/80 hover:bg-primary/10 flex items-center gap-0.5 rounded-sm px-1 py-0.5 text-[11px] font-semibold transition-colors hover:underline\"\n                  onclick={(e) => handleMoreClick(cell.dateKey, dayEvents, e)}\n                  aria-expanded={moreOpenKey === cell.dateKey}\n                >\n                  +{hiddenCount} more\n                </button>\n                {#if moreOpenKey === cell.dateKey}\n                  <div\n                    class={cn(\n                      'border-border bg-popover absolute z-30 w-64 rounded-md border p-2 shadow-lg',\n                      cellIndex >= 28 ? 'bottom-full mb-1' : 'top-full mt-1',\n                      cellIndex % 7 >= 5 ? 'right-0' : 'left-0',\n                    )}\n                    onclick={(e) => e.stopPropagation()}\n                  >\n                    <div\n                      class=\"border-border mb-1.5 flex items-center justify-between border-b pb-1.5 text-xs font-semibold\"\n                    >\n                      <span>\n                        {cell.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', weekday: 'short' })}\n                      </span>\n                      <span class=\"text-muted-foreground text-[11px] font-normal\">\n                        {dayEvents.length} events\n                      </span>\n                    </div>\n                    <div class=\"flex max-h-48 flex-col gap-1 overflow-y-auto\">\n                      {#each dayEvents as evt, idx (evt.id || idx)}\n                        <div\n                          class={cn(\n                            calendarEventVariants({ variant: getEventVariant(evt), size: 'sm' }),\n                            'w-full',\n                          )}\n                          onclick={(e) => handleEventClick(evt, e)}\n                        >\n                          <div class=\"flex items-center gap-1 truncate font-medium\">\n                            {#if !evt.allDay}\n                              <span class=\"shrink-0 font-mono text-[10px] opacity-75\">\n                                {formatTime(getEventMinutes(evt.start, 540), timeFormat)}\n                              </span>\n                            {/if}\n                            <span class=\"truncate\">{evt.title}</span>\n                          </div>\n                        </div>\n                      {/each}\n                    </div>\n                  </div>\n                {/if}\n              </div>\n            {/if}\n          </div>\n        {/each}\n      </div>\n    </div>\n  {:else if view === 'week' || view === 'work-week' || view === 'day'}\n    <!-- VIEW 2 & 3 & 4: WEEK, WORK-WEEK, DAY VIEWS -->\n    <div class=\"flex flex-1 flex-col overflow-hidden\">\n      <!-- Top Columns Header (Sticky) -->\n      <div class=\"border-border bg-muted/20 flex border-b select-none\">\n        <!-- Time gutter header spacer -->\n        <div\n          class=\"border-border/50 text-muted-foreground w-16 shrink-0 border-r py-2.5 text-center text-xs font-medium\"\n        >\n          <Clock class=\"mx-auto size-3.5 opacity-60\" />\n        </div>\n\n        <!-- Columns (7 for week, 5 for work-week, 1 for day) -->\n        <div class={cn('divide-border/50 grid flex-1 divide-x', timeGridCols)}>\n          {#each timeGridDays as d (formatDateKey(d))}\n            <div\n              class={cn(\n                'hover:bg-muted/30 flex cursor-pointer flex-col items-center justify-center py-2 transition-colors',\n                isToday(d) && 'bg-primary/5',\n              )}\n              onclick={() => onDateClick?.(formatDateKey(d))}\n            >\n              {#if dayHeader}\n                {@render dayHeader({ date: d, dateKey: formatDateKey(d), isToday: isToday(d), view })}\n              {:else}\n                <span class=\"text-muted-foreground text-[11px] font-medium tracking-wider uppercase\">\n                  {d.toLocaleDateString('en-US', { weekday: view === 'day' ? 'long' : 'short' })}\n                </span>\n                <span\n                  class={cn(\n                    'mt-0.5 inline-flex size-7 items-center justify-center rounded-full text-xs font-semibold transition-[color,background-color,box-shadow]',\n                    isToday(d) ? 'bg-primary text-primary-foreground shadow-xs' : 'text-foreground',\n                  )}\n                >\n                  {d.getDate()}\n                </span>\n              {/if}\n            </div>\n          {/each}\n        </div>\n      </div>\n\n      <!-- Pinned All-Day Section (if all-day events exist) -->\n      <div class=\"border-border bg-muted/10 flex border-b text-xs\">\n        <div\n          class=\"border-border/50 text-muted-foreground w-16 shrink-0 border-r p-2 text-right text-[10px] font-semibold tracking-wider uppercase\"\n        >\n          All-day\n        </div>\n        <div class={cn('divide-border/50 grid flex-1 divide-x', timeGridCols)}>\n          {#each timeGridDays as d (formatDateKey(d))}\n            <div class=\"flex min-h-[32px] flex-col gap-1 p-1\">\n              {#if allDay}\n                {@render allDay({ date: d, dateKey: formatDateKey(d), events: getAllDayEventsForDay(d) })}\n              {:else}\n                {#each getAllDayEventsForDay(d) as evt (evt.id || evt.title)}\n                  <div\n                    data-slot=\"event-card\"\n                    class={cn(\n                      calendarEventVariants({ variant: getEventVariant(evt), size: 'sm' }),\n                      'w-full truncate py-0.5',\n                    )}\n                    onclick={(e) => handleEventClick(evt, e)}\n                  >\n                    <span class=\"truncate font-medium\">{evt.title}</span>\n                  </div>\n                {/each}\n              {/if}\n            </div>\n          {/each}\n        </div>\n      </div>\n\n      <!-- Scrollable Time Intervals Grid -->\n      <div class=\"relative flex max-h-[640px] min-h-[480px] flex-1 overflow-y-auto\">\n        <!-- Time Gutter -->\n        <div class=\"border-border/50 bg-card w-16 shrink-0 border-r select-none\">\n          {#each intervals as interval (interval.time)}\n            <div\n              style=\"height: {intervalHeight}px\"\n              class=\"border-border/30 text-muted-foreground relative border-b pr-2.5 text-right text-[11px] font-medium\"\n            >\n              {#if intervalSnippet}\n                {@render intervalSnippet({ hour: interval.hour, time: interval.time, label: interval.label })}\n              {:else if interval.label}\n                <span class=\"relative -top-2 block\">\n                  {interval.label}\n                </span>\n              {/if}\n            </div>\n          {/each}\n        </div>\n\n        <!-- Day Columns Grid -->\n        <div class={cn('divide-border/50 relative grid flex-1 divide-x', timeGridCols)}>\n          {#each timeGridDays as d (formatDateKey(d))}\n            <div class=\"relative flex flex-col\">\n              <!-- Interval Rows (Clickable for time click) -->\n              {#each intervals as interval (interval.time)}\n                <div\n                  style=\"height: {intervalHeight}px\"\n                  class=\"border-border/30 hover:bg-muted/20 cursor-pointer border-b transition-colors\"\n                  onclick={() => handleTimeClick(d, interval.hour, 0)}\n                ></div>\n              {/each}\n\n              <!-- Timed Events Container (Absolute Overlay) -->\n              <div class=\"pointer-events-none absolute inset-0 p-0.5\">\n                {#each calculateTimedEventPositions(events, d, firstInterval, intervalCount, intervalMinutes) as item (item.event.id || item.event.title)}\n                  <div\n                    style=\"top: {item.top}%; height: {item.height}%; left: calc({item.left}% + 2px); width: calc({item.width}% - 4px);\"\n                    class=\"pointer-events-auto absolute z-10\"\n                  >\n                    {#if eventSnippet}\n                      {@render eventSnippet({ event: item.event, view, isAllDay: false })}\n                    {:else}\n                      <div\n                        data-slot=\"event-card\"\n                        class={cn(\n                          calendarEventVariants({ variant: getEventVariant(item.event) }),\n                          'flex h-full w-full flex-col justify-start overflow-hidden rounded-md p-1.5 leading-tight shadow-xs',\n                        )}\n                        onclick={(e) => handleEventClick(item.event, e)}\n                      >\n                        <span class=\"truncate text-xs font-semibold\">{item.event.title}</span>\n                        <span class=\"truncate font-mono text-[10px] opacity-80\">\n                          {formatTime(item.startMinutes, timeFormat)} – {formatTime(item.endMinutes, timeFormat)}\n                        </span>\n                        {#if item.event.location}\n                          <span class=\"mt-auto truncate text-[10px] opacity-70\">\n                            📍 {item.event.location}\n                          </span>\n                        {/if}\n                      </div>\n                    {/if}\n                  </div>\n                {/each}\n\n                <!-- Live Current Time Indicator -->\n                {#if isToday(d) && nowPosition !== null}\n                  <div style=\"top: {nowPosition}%\" class=\"pointer-events-none absolute right-0 left-0 z-20\">\n                    <div class=\"relative w-full border-t-2 border-red-500 dark:border-red-400\">\n                      <div\n                        class=\"ring-background absolute -top-1 -left-1 size-2 rounded-full bg-red-500 ring-2 dark:bg-red-400\"\n                      ></div>\n                    </div>\n                  </div>\n                {/if}\n              </div>\n            </div>\n          {/each}\n        </div>\n      </div>\n    </div>\n  {:else if view === 'category'}\n    <!-- VIEW 5: CATEGORY / RESOURCE VIEW -->\n    <div class=\"flex flex-1 flex-col overflow-hidden\">\n      <!-- Category Columns Header -->\n      <div class=\"border-border bg-muted/20 flex border-b select-none\">\n        <div\n          class=\"border-border/50 text-muted-foreground w-16 shrink-0 border-r py-2.5 text-center text-xs font-medium\"\n        >\n          <Clock class=\"mx-auto size-3.5 opacity-60\" />\n        </div>\n\n        <div\n          class=\"divide-border/50 grid flex-1 divide-x\"\n          style=\"grid-template-columns: repeat({Math.max(1, normalizedCategories.length)}, minmax(0, 1fr));\"\n        >\n          {#each normalizedCategories as cat (cat.id)}\n            <div\n              class=\"text-foreground flex items-center justify-center gap-1.5 px-2 py-2.5 text-center text-xs font-semibold\"\n            >\n              {#if cat.color}\n                <span class=\"size-2 shrink-0 rounded-full\" style=\"background-color: {cat.color};\"></span>\n              {/if}\n              <span class=\"truncate\">{cat.name}</span>\n            </div>\n          {/each}\n        </div>\n      </div>\n\n      <!-- Category Intervals Grid -->\n      <div class=\"relative flex max-h-[640px] min-h-[480px] flex-1 overflow-y-auto\">\n        <!-- Time Gutter -->\n        <div class=\"border-border/50 bg-card w-16 shrink-0 border-r select-none\">\n          {#each intervals as interval (interval.time)}\n            <div\n              style=\"height: {intervalHeight}px\"\n              class=\"border-border/30 text-muted-foreground relative border-b pr-2.5 text-right text-[11px] font-medium\"\n            >\n              {#if interval.label}\n                <span class=\"relative -top-2 block\">\n                  {interval.label}\n                </span>\n              {/if}\n            </div>\n          {/each}\n        </div>\n\n        <!-- Category Columns -->\n        <div\n          class=\"divide-border/50 relative grid flex-1 divide-x\"\n          style=\"grid-template-columns: repeat({Math.max(1, normalizedCategories.length)}, minmax(0, 1fr));\"\n        >\n          {#each normalizedCategories as cat (cat.id)}\n            <div class=\"relative flex flex-col\">\n              <!-- Interval Rows -->\n              {#each intervals as interval (interval.time)}\n                <div\n                  style=\"height: {intervalHeight}px\"\n                  class=\"border-border/30 hover:bg-muted/20 cursor-pointer border-b transition-colors\"\n                  onclick={() => handleTimeClick(activeDate, interval.hour, 0, cat.id)}\n                ></div>\n              {/each}\n\n              <!-- Category Timed Events Container -->\n              <div class=\"pointer-events-none absolute inset-0 p-0.5\">\n                {#each calculateTimedEventPositions(events.filter((e) => e.category === cat.id || e.category === cat.name), activeDate, firstInterval, intervalCount, intervalMinutes) as item (item.event.id || item.event.title)}\n                  <div\n                    style=\"top: {item.top}%; height: {item.height}%; left: calc({item.left}% + 2px); width: calc({item.width}% - 4px);\"\n                    class=\"pointer-events-auto absolute z-10\"\n                  >\n                    {#if eventSnippet}\n                      {@render eventSnippet({ event: item.event, view: 'category', isAllDay: false })}\n                    {:else}\n                      <div\n                        data-slot=\"event-card\"\n                        class={cn(\n                          calendarEventVariants({ variant: getEventVariant(item.event) }),\n                          'flex h-full w-full flex-col justify-start overflow-hidden rounded-md p-1.5 leading-tight shadow-xs',\n                        )}\n                        onclick={(e) => handleEventClick(item.event, e)}\n                      >\n                        <span class=\"truncate text-xs font-semibold\">{item.event.title}</span>\n                        <span class=\"truncate font-mono text-[10px] opacity-80\">\n                          {formatTime(item.startMinutes, timeFormat)} – {formatTime(item.endMinutes, timeFormat)}\n                        </span>\n                      </div>\n                    {/if}\n                  </div>\n                {/each}\n\n                <!-- Live Current Time Indicator -->\n                {#if isToday(activeDate) && nowPosition !== null}\n                  <div style=\"top: {nowPosition}%\" class=\"pointer-events-none absolute right-0 left-0 z-20\">\n                    <div class=\"relative w-full border-t-2 border-red-500 dark:border-red-400\">\n                      <div\n                        class=\"ring-background absolute -top-1 -left-1 size-2 rounded-full bg-red-500 ring-2 dark:bg-red-400\"\n                      ></div>\n                    </div>\n                  </div>\n                {/if}\n              </div>\n            </div>\n          {/each}\n        </div>\n      </div>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/event-calendar/EventCalendar.svelte"
    },
    {
      "path": "packages/registry-svelte/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:ui",
      "target": "~/src/lib/components/ui/event-calendar/event-calendar.variants.ts"
    },
    {
      "path": "packages/registry-svelte/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: number = 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    // HH:mm\n    if (/^\\d{1,2}:\\d{2}$/.test(dateInput)) {\n      const [h, m] = dateInput.split(':').map(Number)\n      return h * 60 + m\n    }\n    // Check if contains time\n    const parsed = parseDate(dateInput)\n    // If input had no time specified (just YYYY-MM-DD), default to defaultMinutes\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  // Always Monday through Friday\n  const date = parseDate(baseDate)\n  const day = date.getDay()\n  const diff = (day - 1 + 7) % 7 // Monday offset\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: number = 0,\n  intervalCount: number = 24,\n  intervalMinutes: number = 60,\n): PositionedEvent[] {\n  const dayKey = formatDateKey(dayDate)\n  const startDayMinutes = firstInterval * 60\n  const totalMinutes = intervalCount * intervalMinutes\n\n  // Filter events belonging to this day that are timed\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 // minimum 30 min duration\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  // Assign columns for overlapping groups\n  const positioned: PositionedEvent[] = []\n  // Cluster overlapping events\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  // For each cluster, assign columns\n  for (const cluster of clusters) {\n    const cols: number[] = [] // end time of each column\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: number = 0,\n  intervalCount: number = 24,\n  intervalMinutes: number = 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:ui",
      "target": "~/src/lib/components/ui/event-calendar/date-utils.ts"
    },
    {
      "path": "packages/registry-svelte/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  /** YYYY-MM-DD or YYYY-MM-DD HH:mm or ISO string or Date */\n  start: string | Date\n  /** YYYY-MM-DD or YYYY-MM-DD HH:mm or ISO string or Date */\n  end?: string | Date\n  allDay?: boolean\n  color?:\n    'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'info' | 'purple' | 'rose' | 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:ui",
      "target": "~/src/lib/components/ui/event-calendar/types.ts"
    },
    {
      "path": "packages/registry-svelte/components/event-calendar/index.ts",
      "content": "export { default as EventCalendar, type EventCalendarProps } from './EventCalendar.svelte'\nexport * from './types'\nexport * from './event-calendar.variants'\nexport * from './date-utils'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/event-calendar/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte",
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/button.json"
  ],
  "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"
  ]
}