{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"event-calendar-month-view","type":"registry:ui","title":"ARIA-grid month view with week rows, day cells, event chips, and overflow counts.","description":"ARIA-grid month view with week rows, day cells, event chips, and overflow counts.","dependencies":["date-fns","radix-ui"],"registryDependencies":["@neui/event-calendar","@neui/event-calendar-dnd","@neui/event-calendar-event","@neui/event-calendar-lib","@neui/event-calendar-types","popover","scroll-area"],"files":[{"path":"event-calendar-month-view.tsx","type":"registry:ui","content":"// Title: Event Calendar Month View\n// Description: ARIA-grid month view with week rows, day cells, event chips, and overflow counts.\n\n\"use client\"\n\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n type Ref,\n} from \"react\"\nimport {\n EventCalendarViewContext,\n useEventCalendar,\n useEventCalendarDay,\n useEventCalendarSelector,\n useEventCalendarSettings,\n useEventCalendarViewConfig,\n useEventCalendarViewSettings,\n useEventCalendarWeek,\n} from \"@/components/neui/event-calendar/event-calendar\"\nimport {\n useEventCalendarGestures,\n wasRecentChipPress,\n wasRecentDrag,\n} from \"@/components/neui/event-calendar/event-calendar-dnd\"\nimport {\n EVENT_CALENDAR_GHOST,\n EventCalendarEvent,\n} from \"@/components/neui/event-calendar/event-calendar-event\"\nimport {\n getDayKey,\n getRangeKey,\n resolveOffDay,\n toZoned,\n zonedStartOfDay,\n} from \"@/components/neui/event-calendar/event-calendar-lib\"\nimport type {\n EventCalendarDateRange,\n EventCalendarDragState,\n EventCalendarEventId,\n EventCalendarSegment,\n} from \"@/components/neui/event-calendar/event-calendar-types\"\nimport { addDays, format, getWeek } from \"date-fns\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\n// Layout-effect on the client (measure before paint, no flash), plain effect on\n// the server (never runs there) to avoid the SSR useLayoutEffect warning.\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" ? useLayoutEffect : useEffect\n\n// An occurrence key encodes the start instant and is also the chip's React key,\n// so committing a move re-keys the chip: React remounts it and the browser\n// drops focus to . The chip that owns focus is recorded here so the cell\n// rendering its replacement can hand focus back. Module scope because the drop\n// can land in a different cell than the one the chip left, and only one element\n// holds focus at a time anyway.\nlet focusedChip: {\n node: HTMLElement\n eventId: EventCalendarEventId\n recurrenceIndex?: number\n} | null = null\n\n/** Give focus back to the recorded chip's replacement, if `root` renders it. */\nfunction restoreChipFocus(\n root: HTMLElement | null,\n segments: EventCalendarSegment[]\n) {\n const pending = focusedChip\n // Only a chip removed WHILE focused needs help: a node still in the tree, or\n // a focus that has already moved on by itself, is left alone.\n if (!root || !pending || pending.node.isConnected) return\n const active = document.activeElement\n if (active && active !== document.body) return\n const index = segments.findIndex(\n (segment) =>\n segment.occurrence.eventId === pending.eventId &&\n segment.occurrence.recurrenceIndex === pending.recurrenceIndex\n )\n if (index < 0) return\n const chip = root.querySelectorAll(\n \"[data-slot=event-calendar-event]\"\n )[index]\n if (!chip) return\n // cleared first: focus() re-records through the new chip's own onFocus\n focusedChip = null\n chip.focus()\n}\n\ninterface EventCalendarMonthViewProps extends HTMLAttributes {\n maxEventsPerCell?: number | \"auto\"\n asChild?: boolean\n}\n\nfunction EventCalendarMonthView({\n className,\n asChild = false,\n maxEventsPerCell,\n ...props\n}: EventCalendarMonthViewProps) {\n const instance = useEventCalendar()\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n const visibleRange = useEventCalendarSelector<\n unknown,\n EventCalendarDateRange\n >((state) => state.visibleRange, {\n isEqual: (a, b) => getRangeKey(a) === getRangeKey(b),\n })\n const anchorDate = useEventCalendarSelector((state) => state.date)\n\n const { effective } = useEventCalendarViewSettings()\n const weeks = useMemo(() => {\n const days: Date[] = []\n let cursor = zonedStartOfDay(visibleRange.start, settings.timeZone)\n while (cursor < visibleRange.end) {\n days.push(cursor)\n cursor = zonedStartOfDay(\n addDays(toZoned(cursor, settings.timeZone), 1),\n settings.timeZone\n )\n }\n const rows: Date[][] = []\n for (let i = 0; i < days.length; i += 7) rows.push(days.slice(i, i + 7))\n if (effective.weekends) return rows\n return rows.map((row) =>\n row.filter(\n (day) =>\n !settings.weekendDays.includes(\n toZoned(day, settings.timeZone).getDay()\n )\n )\n )\n }, [\n visibleRange,\n settings.timeZone,\n settings.weekendDays,\n effective.weekends,\n ])\n\n const headerDays = weeks[0] ?? []\n const title = settings.i18n.functions.formatTitle(\"month\", {\n date: toZoned(anchorDate, settings.timeZone),\n activeRange: instance.api.getActiveRange(),\n visibleRange,\n locale: settings.locale,\n })\n\n const gridTemplateColumns = `${effective.weekNumbers ? \"var(--ec-week-number-w, 2.75rem) \" : \"\"}repeat(${headerDays.length}, minmax(0, 1fr))`\n const cap = maxEventsPerCell ?? viewConfig.maxEventsPerCell\n const contained = viewConfig.scrollMode !== \"page\"\n\n // \"auto\" fits as many event rows as the cell height allows and rolls the rest\n // into \"+N more\". Only the contained mode gives a cell a bounded height to\n // measure; page mode grows to fit, so \"auto\" there keeps the fixed fallback.\n const autoFit = cap === \"auto\" && contained\n // slotProbe resolves the event-row height (--ec-month-bar-h) to px, honoring\n // the current font size and any consumer override; contentProbe is the first\n // cell's flex-1 content area, whose height is the event space per cell.\n const slotProbeRef = useRef(null)\n const contentProbeRef = useRef(null)\n const [autoCap, setAutoCap] = useState(null)\n const measureCap = useCallback(() => {\n const content = contentProbeRef.current\n const slot = slotProbeRef.current\n if (!content || !slot) return\n const laneH = slot.getBoundingClientRect().height\n if (laneH <= 0) return\n const cs = getComputedStyle(content)\n const inner = content.clientHeight - (parseFloat(cs.paddingTop) || 0)\n const gap = parseFloat(cs.rowGap) || 0\n // N rows occupy N*laneH - gap (the last row has no trailing gap)\n setAutoCap(Math.max(1, Math.floor((inner + gap) / laneH)))\n }, [])\n useIsoLayoutEffect(() => {\n if (!autoFit) {\n setAutoCap(null)\n return\n }\n const content = contentProbeRef.current\n if (!content || typeof ResizeObserver === \"undefined\") return\n measureCap()\n const observer = new ResizeObserver(measureCap)\n observer.observe(content)\n return () => observer.disconnect()\n // re-observe the first cell after a re-layout (row count or month change)\n }, [autoFit, measureCap, weeks.length, anchorDate])\n const resolvedCap = cap === \"auto\" ? (autoFit ? (autoCap ?? 3) : 3) : cap\n\n const Comp = asChild ? Slot.Root : \"div\"\n\n return (\n \n \n \n {effective.weekNumbers && (\n \n )}\n {headerDays.map((day) => (\n \n {viewConfig.renderDayHeader?.({\n day,\n view: \"month\",\n isToday:\n getDayKey(day, settings.timeZone) ===\n getDayKey(new Date(), settings.timeZone),\n }) ?? (\n <>\n \n {format(\n toZoned(day, settings.timeZone),\n settings.i18n.formats.monthDayHeader,\n { locale: settings.locale }\n )}\n \n \n {format(\n toZoned(day, settings.timeZone),\n settings.i18n.formats.monthDayHeaderNarrow,\n { locale: settings.locale }\n )}\n \n \n )}\n \n ))}\n \n \n {weeks.map((week, rowIndex) => (\n \n ))}\n \n {autoFit && (\n \n )}\n \n \n )\n}\n\n/**\n * One month week row. Multi-day / all-day events render as CONTINUOUS bars in\n * an overlay grid that spans day columns (colStart -> colSpan) and stacks by\n * lane; single-day timed events render inside each cell below the reserved bar\n * lanes. This is what makes a cross-day event read as one whole block instead\n * of a chip repeated per cell.\n */\nfunction EventCalendarMonthWeek({\n week,\n gridTemplateColumns,\n showWeekNumber,\n cap,\n autoFit,\n contentRef,\n}: {\n week: Date[]\n gridTemplateColumns: string\n showWeekNumber: boolean\n cap: number\n autoFit: boolean\n /** Set on the first week only: forwarded to its first cell's content area so\n * the view can measure the per-cell event height for \"auto\". */\n contentRef?: Ref\n}) {\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n const { bars, rowStart } = useEventCalendarWeek(week[0])\n const colOffset = showWeekNumber ? 1 : 0\n const dayMs = 86400000\n const rowStartMs = zonedStartOfDay(\n rowStart ?? week[0],\n settings.timeZone\n ).getTime()\n // Day offsets from the TRUE row start (0-6) for each visible column, so a\n // weekends-hidden month still places bars on the right days.\n const offsets = week.map((d) =>\n Math.round(\n (zonedStartOfDay(d, settings.timeZone).getTime() - rowStartMs) / dayMs\n )\n )\n /** Clamp a day-offset span onto the visible columns; null = fully hidden. */\n const gridPos = (colStart: number, colSpan: number) => {\n let start = -1\n let end = -1\n for (let o = colStart; o < colStart + colSpan; o++) {\n const col = offsets.indexOf(o)\n if (col === -1) continue\n if (start === -1) start = col\n end = col\n }\n return start === -1 ? null : { col: start, span: end - start + 1 }\n }\n // bars fit within the cap; deeper lanes fall into each day's \"+N more\"\n const visibleBars = bars.filter((b) => (b.lane ?? 0) < cap)\n const covers = (b: EventCalendarSegment, dayOffset: number) =>\n (b.colStart ?? 0) <= dayOffset &&\n dayOffset < (b.colStart ?? 0) + (b.colSpan ?? 1)\n // Occurrence keys of the bars hidden in each column (lane >= cap). Threaded to\n // the cell so its \"+N more\" popover can list the hidden bars WITHOUT re-listing\n // the visible ones (day buckets carry no lane, so the week row - which owns bar\n // laning - is the only place that knows which bars are hidden).\n const hiddenBarKeysByCol = week.map(\n (_, col) =>\n new Set(\n bars\n .filter((b) => (b.lane ?? 0) >= cap && covers(b, offsets[col]))\n .map((b) => b.occurrence.key)\n )\n )\n\n // Live move/resize ghost at the PROPOSED day span. Standardized treatment\n // (EVENT_CALENDAR_GHOST): move = the event carried as a full clone, resize =\n // the same clone with a dashed boundary; invalid adds destructive marking\n // while the engine shows the not-allowed cursor + validation hint.\n const dragGhost = useEventCalendarSelector<\n unknown,\n | (Pick<\n EventCalendarDragState,\n | \"kind\"\n | \"valid\"\n | \"occurrence\"\n | \"proposedStart\"\n | \"proposedEnd\"\n | \"proposedAllDay\"\n > & {\n colStart: number\n colSpan: number\n isStart: boolean\n isEnd: boolean\n color?: string\n })\n | null\n >(\n (state) => {\n const drag = state.drag\n if (!drag) return null\n const rowEndMs = rowStartMs + 7 * dayMs\n const startDayMs = zonedStartOfDay(\n drag.proposedStart,\n settings.timeZone\n ).getTime()\n // exclusive end -> the last covered day\n const lastDayMs = zonedStartOfDay(\n new Date(drag.proposedEnd.getTime() - 1),\n settings.timeZone\n ).getTime()\n if (startDayMs >= rowEndMs || lastDayMs < rowStartMs) return null\n const startCol = Math.max(\n 0,\n Math.round((startDayMs - rowStartMs) / dayMs)\n )\n const endCol = Math.min(6, Math.round((lastDayMs - rowStartMs) / dayMs))\n if (endCol < startCol) return null\n return {\n kind: drag.kind,\n valid: drag.valid,\n occurrence: drag.occurrence,\n proposedStart: drag.proposedStart,\n proposedEnd: drag.proposedEnd,\n proposedAllDay: drag.proposedAllDay,\n colStart: startCol,\n colSpan: endCol - startCol + 1,\n isStart: startDayMs >= rowStartMs,\n isEnd: lastDayMs < rowEndMs,\n color: drag.occurrence.event.color,\n }\n },\n {\n isEqual: (a, b) =>\n a === b ||\n (a !== null &&\n b !== null &&\n a.colStart === b.colStart &&\n a.colSpan === b.colSpan &&\n a.valid === b.valid &&\n a.kind === b.kind &&\n a.proposedAllDay === b.proposedAllDay &&\n a.proposedStart.getTime() === b.proposedStart.getTime() &&\n a.proposedEnd.getTime() === b.proposedEnd.getTime()),\n }\n )\n const ghostPos = dragGhost\n ? gridPos(dragGhost.colStart, dragGhost.colSpan)\n : null\n const ghostLane = dragGhost\n ? (bars.find((b) => b.occurrence.key === dragGhost.occurrence.key)?.lane ??\n 0)\n : 0\n // A single-day timed MOVE is indicated INLINE in the target cell (a\n // placeholder at the time-sorted position, rendered by EventCalendarMonthCell)\n // rather than as a bar in this overlay, so only bar drags and every resize\n // render the overlay ghost. Bars = all-day or multi-day (span > 1 day).\n const ghostIsBar =\n !!dragGhost &&\n (dragGhost.kind !== \"move\" ||\n dragGhost.proposedAllDay ||\n dragGhost.proposedEnd.getTime() - dragGhost.proposedStart.getTime() >\n dayMs)\n\n return (\n \n {showWeekNumber && (\n \n {settings.i18n.labels.week(\n getWeek(toZoned(week[0], settings.timeZone), {\n // locale supplies firstWeekContainsDate, so a de/ISO calendar\n // numbers the year-boundary weeks its own way instead of falling\n // back to US numbering; weekStartsOn stays explicit so the number\n // keeps matching the rendered grid\n locale: settings.locale,\n weekStartsOn: settings.weekStartsOn,\n })\n )}\n \n )}\n {week.map((day, col) => (\n \n covers(b, offsets[col]) ? Math.max(max, (b.lane ?? 0) + 1) : max,\n 0\n )}\n hiddenBarKeys={hiddenBarKeysByCol[col]}\n isLast={col === week.length - 1}\n autoFit={autoFit}\n contentRef={col === 0 ? contentRef : undefined}\n />\n ))}\n {/* Continuous bar overlay: one element per bar, placed by grid-column so\n a cross-day span is a single unbroken block. pointer-events pass\n through the gaps to the cells below. NOT aria-hidden - these are the\n real interactive bars. */}\n {(visibleBars.length > 0 || (dragGhost && ghostPos && ghostIsBar)) && (\n \n {visibleBars.map((bar) => {\n const pos = gridPos(bar.colStart ?? 0, bar.colSpan ?? 1)\n if (!pos) return null\n return (\n \n {/* lane height minus the 2px inter-lane gap */}\n \n \n )\n })}\n {dragGhost && ghostPos && ghostIsBar && (\n \n \n {dragGhost.kind !== \"move\" && (\n \n )}\n \n \n )}\n \n )}\n \n )\n}\n\nfunction EventCalendarMonthCell({\n day,\n cap,\n reservedLanes,\n hiddenBarKeys,\n isLast,\n autoFit,\n contentRef,\n}: {\n day: Date\n cap: number\n reservedLanes: number\n /** Occurrence keys of the bars hidden in THIS column (lane >= cap), from the\n * week row. Lets the cell list hidden bars in its overflow popover without\n * re-listing the bars already visible in the row overlay. */\n hiddenBarKeys: Set\n /** Last column in the row - drops the right border so the grid's outer edge\n * is owned by the container, not a doubled cell border. Passed explicitly\n * because the bar overlay renders after the cells, so `:last-child` is\n * unreliable on rows that have bars. */\n isLast: boolean\n /** When true, the \"+N more\" chip is treated as taking a row so the visible\n * chips + indicator always fit the measured cell height. */\n autoFit: boolean\n /** Set on the first cell only: measured to derive the \"auto\" cap. */\n contentRef?: Ref\n}) {\n const instance = useEventCalendar()\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n const gestures = useEventCalendarGestures()\n const { segments, isToday, isOutside } = useEventCalendarDay(day)\n\n const dayStart = zonedStartOfDay(day, settings.timeZone)\n const dayEnd = addDays(toZoned(dayStart, settings.timeZone), 1)\n const { effective } = useEventCalendarViewSettings()\n const isOff = resolveOffDay(\n day,\n settings.timeZone,\n effective.offDays\n ? typeof viewConfig.offDays === \"object\"\n ? viewConfig.offDays\n : true\n : false,\n settings.weekendDays\n )\n const offClassName =\n (typeof viewConfig.offDays === \"object\" && viewConfig.offDays.className) ||\n \"bg-muted/25\"\n\n const isDropTarget = useEventCalendarSelector<\n unknown,\n \"valid\" | \"invalid\" | null\n >((state) => {\n const drag = state.drag\n if (!drag) return null\n const covered = drag.proposedStart < dayEnd && drag.proposedEnd > dayStart\n if (!covered) return null\n return drag.valid ? \"valid\" : \"invalid\"\n })\n // Hide hover affordances mid-gesture: the only intent is the drop target.\n // Gated on the one thing that reads it: a plain global boolean flips for all\n // 42 cells the moment a gesture starts and again when it ends, which is pure\n // waste in the default configuration where no add button renders.\n const isInteracting = useEventCalendarSelector((state) =>\n viewConfig.showDayAddButton\n ? state.drag !== null || state.slotDraft !== null\n : false\n )\n const inDraft = useEventCalendarSelector<\n unknown,\n { isStart: boolean; isEnd: boolean } | null\n >(\n (state) => {\n const draft = state.slotDraft\n if (!draft || !draft.allDay) return null\n if (draft.start >= dayEnd || draft.end <= dayStart) return null\n return {\n isStart: draft.start >= dayStart,\n isEnd: draft.end <= dayEnd,\n }\n },\n {\n isEqual: (a, b) =>\n a === b ||\n (a !== null &&\n b !== null &&\n a.isStart === b.isStart &&\n a.isEnd === b.isEnd),\n }\n )\n // A single-day timed MOVE landing on THIS day: expose the proposed\n // minute-of-day (+ color/validity) so the cell can render a drop placeholder\n // at the correct time-sorted position, instead of the overlay marking a bar\n // over the first chip. Skipped for bars (they keep the overlay ghost) and for\n // a no-op move back onto the event's own day (the dimmed source already marks\n // the spot).\n const inlineDrop = useEventCalendarSelector<\n unknown,\n { min: number; valid: boolean; color?: string } | null\n >(\n (state) => {\n const drag = state.drag\n if (!drag || drag.kind !== \"move\" || drag.proposedAllDay) return null\n if (drag.proposedEnd.getTime() - drag.proposedStart.getTime() > 86400000)\n return null\n const dropDayMs = zonedStartOfDay(\n drag.proposedStart,\n settings.timeZone\n ).getTime()\n if (dropDayMs !== dayStart.getTime()) return null\n if (\n zonedStartOfDay(drag.occurrence.start, settings.timeZone).getTime() ===\n dayStart.getTime()\n )\n return null\n return {\n min: (drag.proposedStart.getTime() - dropDayMs) / 60000,\n valid: drag.valid,\n color: drag.occurrence.event.color,\n }\n },\n {\n isEqual: (a, b) =>\n a === b ||\n (a !== null &&\n b !== null &&\n a.min === b.min &&\n a.valid === b.valid &&\n a.color === b.color),\n }\n )\n\n // Bars (allDay/multi-day) are drawn by the week-row overlay; the cell renders\n // only single-day timed events, below the reserved bar lanes.\n const extraHidden = hiddenBarKeys.size\n const hiddenBarSegs = segments.allDay.filter((s) =>\n hiddenBarKeys.has(s.occurrence.key)\n )\n const m = segments.timed.length\n const timedSlots = Math.max(0, cap - reservedLanes)\n\n // Static (at-rest) split: what the cell shows with no drag, and - crucially -\n // what the \"+N more\" popover lists. The popover carries ONLY the hidden events\n // (hidden bars + timed past the cap), never the chips already visible in the\n // cell, so it never duplicates them. autoFit gives up one timed row to the\n // \"+N more\" indicator so the visible chips fit the clipped cell height.\n const staticOverflow = extraHidden > 0 || m > timedSlots\n const staticShown =\n autoFit && staticOverflow ? Math.max(0, timedSlots - 1) : timedSlots\n const overflowSegments = [\n ...hiddenBarSegs,\n ...segments.timed.slice(staticShown),\n ]\n\n // Live split: while a timed chip is dragged onto this day, render the day\n // exactly as it will look AFTER the drop. The dragged chip is a phantom in the\n // time-sorted order and is shown as the placeholder - inline where it lands,\n // or ON the \"+N more\" indicator when it lands in the overflow bucket (so the\n // user sees the drop will push it into \"more\"). The \"+N more\" count always\n // reflects the post-drop hidden total.\n let visibleTimed: EventCalendarSegment[]\n let overflowCount: number\n let placeholderIndex: number\n let placeholderAtMore: boolean\n if (!inlineDrop) {\n visibleTimed = segments.timed.slice(0, staticShown)\n overflowCount = overflowSegments.length\n placeholderIndex = -1\n placeholderAtMore = false\n } else {\n // rank of the dragged chip in the resulting time-sorted list (chips are\n // time-ordered, so this is the count starting at or before its time)\n const insertRank = segments.timed.filter(\n (s) => (s.startMin ?? 0) <= inlineDrop.min\n ).length\n const dropOverflow = extraHidden > 0 || m + 1 > timedSlots\n // rows for timed items INCLUDING the phantom, before the \"+N more\" row\n const vis = !dropOverflow\n ? m + 1\n : autoFit\n ? Math.max(0, timedSlots - 1)\n : timedSlots\n placeholderAtMore = insertRank >= vis\n visibleTimed = placeholderAtMore\n ? segments.timed.slice(0, vis)\n : segments.timed.slice(0, Math.max(0, vis - 1))\n placeholderIndex = placeholderAtMore ? -1 : insertRank\n overflowCount =\n extraHidden + (m - visibleTimed.length) + (placeholderAtMore ? 1 : 0)\n }\n\n // No dep array: the replacement for a chip that lost focus to a commit can\n // appear on any re-render of any cell, and a cell with nothing to restore\n // bails after two comparisons.\n const rootRef = useRef(null)\n useIsoLayoutEffect(() => {\n restoreChipFocus(rootRef.current, visibleTimed)\n })\n\n // Faint dashed drop placeholder, tinted to the dragged event's color, echoing\n // the move ghost (EVENT_CALENDAR_GHOST.move); one chip-height tall so chips\n // shift by exactly one row when it is inserted.\n const dropPlaceholder = inlineDrop ? (\n \n ) : null\n\n const defaultContent = (\n <>\n \n {reservedLanes > 0 && (\n \n )}\n {visibleTimed.flatMap((segment, i) => {\n const chip = (\n {\n focusedChip = {\n node: e.currentTarget,\n eventId: segment.occurrence.eventId,\n recurrenceIndex: segment.occurrence.recurrenceIndex,\n }\n }}\n // A chip still in the tree lost focus on its own, so there is\n // nothing to restore; only a blur from the remount is kept.\n onBlur={(e) => {\n if (e.currentTarget.isConnected) focusedChip = null\n }}\n // Hold a fixed height like the all-day lane above; without this\n // the chip flex-shrinks to whatever room the cell has left, so\n // cells with a reserved bar lane or a second chip render shorter\n // chips.\n className=\"shrink-0\"\n />\n )\n return i === placeholderIndex ? [dropPlaceholder, chip] : [chip]\n })}\n {placeholderIndex >= 0 &&\n placeholderIndex >= visibleTimed.length &&\n dropPlaceholder}\n {overflowCount > 0 && (\n \n )}\n \n {/* Day number + add affordance, bottom-right (Notion-style) */}\n \n {viewConfig.showDayAddButton && !isInteracting && (\n {\n e.stopPropagation()\n settings.onSlotClick?.(\n { date: day, allDay: true, view: \"month\" },\n e\n )\n }}\n >\n \n \n )}\n \n {format(\n toZoned(day, settings.timeZone),\n settings.i18n.formats.monthCellDay,\n { locale: settings.locale }\n )}\n \n \n \n )\n\n const content =\n viewConfig.renderMonthCell?.({\n day,\n segments,\n isToday,\n isOutside,\n overflowCount,\n defaultContent,\n }) ?? defaultContent\n\n return (\n {\n const target = e.target as HTMLElement\n if (target.closest(\"[data-slot=event-calendar-event]\")) return\n if (target.closest(\"[data-slot=event-calendar-more]\")) return\n gestures.beginCreate(e, day, true)\n }}\n onClick={(e) => {\n if (wasRecentDrag() || wasRecentChipPress()) return\n settings.onSlotClick?.({ date: day, allDay: true, view: \"month\" }, e)\n }}\n >\n {inDraft && (\n \n )}\n {content}\n \n )\n}\n\ninterface EventCalendarMoreIndicatorProps {\n day: Date\n count: number\n /** The OVERFLOW (hidden) segments for this day - bars first, then timed. The\n * popover lists only these, never the chips already visible in the cell. */\n segments: EventCalendarSegment[]\n /** Set while a timed chip is dragged and will land in THIS overflow bucket:\n * the indicator itself becomes the drop placeholder (dashed, event-tinted) so\n * it reads as \"the chip joins the +N more list\". */\n dropInto?: { color?: string; valid: boolean }\n}\n\n/**\n * \"+N more\" trigger opening a popover with the day's full event list.\n * onMoreClick returning false suppresses the built-in popover.\n */\nfunction EventCalendarMoreIndicator({\n day,\n count,\n segments,\n dropInto,\n}: EventCalendarMoreIndicatorProps) {\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n const [open, setOpen] = useState(false)\n const headerId = useId()\n\n // Grabbing a chip from this list starts a drag; close the popover so it does\n // not sit over the drop target while the event is carried to another day.\n const isDragging = useEventCalendarSelector(\n (state) => state.drag !== null\n )\n useEffect(() => {\n if (isDragging) setOpen(false)\n }, [isDragging])\n\n return (\n \n {\n e.stopPropagation()\n const verdict = settings.onMoreClick?.(\n day,\n segments.map((segment) => segment.occurrence),\n e\n )\n if (verdict === false) {\n e.preventDefault()\n setOpen(false)\n }\n }}\n >\n {viewConfig.renderMoreIndicator?.({ day, count, segments }) ??\n settings.i18n.labels.more(count)}\n \n e.stopPropagation()}\n >\n {viewConfig.renderMoreContent ? (\n viewConfig.renderMoreContent({\n day,\n segments,\n close: () => setOpen(false),\n })\n ) : (\n \n )}\n \n \n )\n}\n\n/** Built-in \"+N more\" popover body: day header + the day's chips. */\nfunction EventCalendarMoreDefaultContent({\n day,\n segments,\n headerId,\n}: {\n day: Date\n segments: EventCalendarSegment[]\n /** Names the popover dialog: the header IS the list's accessible name. */\n headerId?: string\n}) {\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n return (\n <>\n \n {format(\n toZoned(day, settings.timeZone),\n settings.i18n.formats.moreDayHeader,\n { locale: settings.locale }\n )}\n \n {/* The scroll region breaks out of the popover's right padding (-me-2)\n so the scrollbar sits flush in the gutter; the list then pads itself\n back (ps-1 aligns with the header, pe-4 clears the ~10px bar with a\n gap) and adds py-1 so the first/last focus ring is not clipped by\n the overflow. Layout is identical with or without a scrollbar. */}\n {viewConfig.scrollbars === \"native\" ? (\n \n
\n {segments.map((segment) => (\n \n ))}\n
\n \n ) : (\n \n
\n {segments.map((segment) => (\n \n ))}\n
\n
\n )}\n \n )\n}\n\nexport { EventCalendarMonthView, EventCalendarMoreIndicator }\nexport type { EventCalendarMonthViewProps, EventCalendarMoreIndicatorProps }","target":"components/neui/event-calendar/event-calendar-month-view.tsx"}]}