{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "event-calendar", "title": "Event Calendar", "description": "月・週・日・アジェンダ表示に対応したドラッグ&ドロップ可能なイベントカレンダー", "dependencies": [ "@dnd-kit/core", "@dnd-kit/utilities", "@remixicon/react", "dayjs", "lucide-react", "sonner" ], "registryDependencies": [ "button", "calendar", "checkbox", "dialog", "dropdown-menu", "input", "label", "popover", "radio-group", "select", "sonner", "textarea" ], "files": [ { "path": "registry/br-hr/blocks/event-calendar/components/event-calendar.tsx", "content": "\"use client\";\n\nimport { RiCalendarCheckLine } from \"@remixicon/react\";\nimport {\n\tChevronDownIcon,\n\tChevronLeftIcon,\n\tChevronRightIcon,\n\tPlusIcon,\n\tSquareDashedMousePointer,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"sonner\";\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/br-hr/ui/button\";\nimport dayjs from \"../dayjs\";\nimport {\n\tAgendaDaysToShow,\n\tAgendaView,\n\taddHoursToDate,\n\tCalendarDndProvider,\n\ttype CalendarEvent,\n\ttype CalendarView,\n\tDayView,\n\tEventDialog,\n\tEventGap,\n\tEventHeight,\n\tgetHolidayFetchRange,\n\tMonthView,\n\ttype PendingTimeRange,\n\tuseHolidays,\n\tWeekCellsHeight,\n\tWeekView,\n} from \".\";\n\nexport interface EventCalendarProps {\n\tevents?: CalendarEvent[];\n\tonEventAdd?: (event: CalendarEvent) => void;\n\tonEventsAdd?: (events: CalendarEvent[]) => void;\n\tonEventUpdate?: (event: CalendarEvent) => void;\n\tonEventDelete?: (eventId: string) => void;\n\tclassName?: string;\n\tinitialView?: CalendarView;\n}\n\nexport function EventCalendar({\n\tevents = [],\n\tonEventAdd,\n\tonEventsAdd,\n\tonEventUpdate,\n\tonEventDelete,\n\tclassName,\n\tinitialView = \"month\",\n}: EventCalendarProps) {\n\tconst [currentDate, setCurrentDate] = useState(() => dayjs().toDate());\n\tconst [view, setView] = useState(initialView);\n\tconst [isEventDialogOpen, setIsEventDialogOpen] = useState(false);\n\tconst [selectedEvent, setSelectedEvent] = useState(\n\t\tnull,\n\t);\n\tconst [isMultiSelectMode, setIsMultiSelectMode] = useState(false);\n\tconst [pendingRanges, setPendingRanges] = useState([]);\n\tconst [isBatchDialogOpen, setIsBatchDialogOpen] = useState(false);\n\n\tconst holidayFetchRange = useMemo(\n\t\t() => getHolidayFetchRange(currentDate, view),\n\t\t[currentDate, view],\n\t);\n\n\tconst { holidays } = useHolidays({\n\t\tstart: holidayFetchRange.start,\n\t\tend: holidayFetchRange.end,\n\t});\n\n\tconst clearPendingRanges = useCallback(() => {\n\t\tsetPendingRanges([]);\n\t\tsetIsBatchDialogOpen(false);\n\t}, []);\n\n\tconst resetAfterEventCreate = useCallback(() => {\n\t\tsetPendingRanges([]);\n\t\tsetIsBatchDialogOpen(false);\n\t\tsetIsMultiSelectMode(false);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tsetPendingRanges([]);\n\t\tsetIsBatchDialogOpen(false);\n\t\tif (view !== \"week\") {\n\t\t\tsetIsMultiSelectMode(false);\n\t\t}\n\t}, [view]);\n\n\t// Add keyboard shortcuts for view switching\n\tuseEffect(() => {\n\t\tconst handleKeyDown = (e: KeyboardEvent) => {\n\t\t\t// Skip if user is typing in an input, textarea or contentEditable element\n\t\t\t// or if the event dialog is open\n\t\t\tif (\n\t\t\t\tisEventDialogOpen ||\n\t\t\t\te.target instanceof HTMLInputElement ||\n\t\t\t\te.target instanceof HTMLTextAreaElement ||\n\t\t\t\t(e.target instanceof HTMLElement && e.target.isContentEditable)\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tswitch (e.key.toLowerCase()) {\n\t\t\t\tcase \"m\":\n\t\t\t\t\tsetView(\"month\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"w\":\n\t\t\t\t\tsetView(\"week\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"d\":\n\t\t\t\t\tsetView(\"day\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"a\":\n\t\t\t\t\tsetView(\"agenda\");\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (e.key === \"Escape\" && pendingRanges.length > 0) {\n\t\t\t\tclearPendingRanges();\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"keydown\", handleKeyDown);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"keydown\", handleKeyDown);\n\t\t};\n\t}, [isEventDialogOpen, pendingRanges.length, clearPendingRanges]);\n\n\tconst handlePrevious = () => {\n\t\tif (view === \"month\") {\n\t\t\tsetCurrentDate(dayjs(currentDate).subtract(1, \"month\").toDate());\n\t\t} else if (view === \"week\") {\n\t\t\tsetCurrentDate(dayjs(currentDate).subtract(1, \"week\").toDate());\n\t\t} else if (view === \"day\") {\n\t\t\tsetCurrentDate(dayjs(currentDate).subtract(1, \"day\").toDate());\n\t\t} else if (view === \"agenda\") {\n\t\t\t// For agenda view, go back 30 days (a full month)\n\t\t\tsetCurrentDate(\n\t\t\t\tdayjs(currentDate).subtract(AgendaDaysToShow, \"day\").toDate(),\n\t\t\t);\n\t\t}\n\t};\n\n\tconst handleNext = () => {\n\t\tif (view === \"month\") {\n\t\t\tsetCurrentDate(dayjs(currentDate).add(1, \"month\").toDate());\n\t\t} else if (view === \"week\") {\n\t\t\tsetCurrentDate(dayjs(currentDate).add(1, \"week\").toDate());\n\t\t} else if (view === \"day\") {\n\t\t\tsetCurrentDate(dayjs(currentDate).add(1, \"day\").toDate());\n\t\t} else if (view === \"agenda\") {\n\t\t\t// For agenda view, go forward 30 days (a full month)\n\t\t\tsetCurrentDate(dayjs(currentDate).add(AgendaDaysToShow, \"day\").toDate());\n\t\t}\n\t};\n\n\tconst handleToday = () => {\n\t\tsetCurrentDate(dayjs().toDate());\n\t};\n\n\tconst handleEventSelect = (event: CalendarEvent) => {\n\t\tconsole.log(\"Event selected:\", event); // Debug log\n\t\tsetSelectedEvent(event);\n\t\tsetIsEventDialogOpen(true);\n\t};\n\n\tconst handleEventCreate = (startTime: Date, endTime?: Date) => {\n\t\t// Snap to 15-minute intervals\n\t\tconst minutes = startTime.getMinutes();\n\t\tconst remainder = minutes % 15;\n\t\tif (remainder !== 0) {\n\t\t\tif (remainder < 7.5) {\n\t\t\t\tstartTime.setMinutes(minutes - remainder);\n\t\t\t} else {\n\t\t\t\tstartTime.setMinutes(minutes + (15 - remainder));\n\t\t\t}\n\t\t\tstartTime.setSeconds(0);\n\t\t\tstartTime.setMilliseconds(0);\n\t\t}\n\n\t\tconst newEvent: CalendarEvent = {\n\t\t\tid: \"\",\n\t\t\ttitle: \"\",\n\t\t\tstart: startTime,\n\t\t\tend: endTime ?? addHoursToDate(startTime, 1),\n\t\t\tallDay: false,\n\t\t};\n\t\tsetSelectedEvent(newEvent);\n\t\tsetIsEventDialogOpen(true);\n\t};\n\n\tconst handleRangeAdd = (range: PendingTimeRange) => {\n\t\tsetPendingRanges((prev) => [...prev, range]);\n\t};\n\n\tconst handleConfirmMultiCreate = () => {\n\t\tif (pendingRanges.length === 0) return;\n\n\t\tsetSelectedEvent(null);\n\t\tsetIsBatchDialogOpen(true);\n\t\tsetIsEventDialogOpen(true);\n\t};\n\n\tconst handleBatchSave = (data: {\n\t\ttitle: string;\n\t\tdescription?: string;\n\t\tlocation?: string;\n\t\tcolor?: CalendarEvent[\"color\"];\n\t}) => {\n\t\tconst newEvents: CalendarEvent[] = pendingRanges.map((range) => ({\n\t\t\tid: crypto.randomUUID(),\n\t\t\ttitle: data.title,\n\t\t\tdescription: data.description,\n\t\t\tlocation: data.location,\n\t\t\tcolor: data.color,\n\t\t\tstart: range.start,\n\t\t\tend: range.end,\n\t\t\tallDay: false,\n\t\t}));\n\n\t\tif (onEventsAdd) {\n\t\t\tonEventsAdd(newEvents);\n\t\t} else {\n\t\t\tfor (const event of newEvents) {\n\t\t\t\tonEventAdd?.(event);\n\t\t\t}\n\t\t}\n\n\t\ttoast(`${newEvents.length}件の予定を追加しました`, {\n\t\t\tdescription: data.title,\n\t\t\tposition: \"bottom-left\",\n\t\t});\n\t\tsetIsEventDialogOpen(false);\n\t\tsetSelectedEvent(null);\n\t\tsetIsBatchDialogOpen(false);\n\t\tresetAfterEventCreate();\n\t};\n\n\tconst handleEventSave = (event: CalendarEvent) => {\n\t\tif (event.id) {\n\t\t\tonEventUpdate?.(event);\n\t\t\t// Show toast notification when an event is updated\n\t\t\ttoast(`Event \"${event.title}\" updated`, {\n\t\t\t\tdescription: dayjs(event.start).format(\"MMM D, YYYY\"),\n\t\t\t\tposition: \"bottom-left\",\n\t\t\t});\n\t\t} else {\n\t\t\tonEventAdd?.({\n\t\t\t\t...event,\n\t\t\t\tid: Math.random().toString(36).substring(2, 11),\n\t\t\t});\n\t\t\t// Show toast notification when an event is added\n\t\t\ttoast(`Event \"${event.title}\" added`, {\n\t\t\t\tdescription: dayjs(event.start).format(\"MMM D, YYYY\"),\n\t\t\t\tposition: \"bottom-left\",\n\t\t\t});\n\t\t\tresetAfterEventCreate();\n\t\t}\n\t\tsetIsEventDialogOpen(false);\n\t\tsetSelectedEvent(null);\n\t\tsetIsBatchDialogOpen(false);\n\t};\n\n\tconst handleEventDelete = (eventId: string) => {\n\t\tconst deletedEvent = events.find((e) => e.id === eventId);\n\t\tonEventDelete?.(eventId);\n\t\tsetIsEventDialogOpen(false);\n\t\tsetSelectedEvent(null);\n\n\t\t// Show toast notification when an event is deleted\n\t\tif (deletedEvent) {\n\t\t\ttoast(`Event \"${deletedEvent.title}\" deleted`, {\n\t\t\t\tdescription: dayjs(deletedEvent.start).format(\"MMM D, YYYY\"),\n\t\t\t\tposition: \"bottom-left\",\n\t\t\t});\n\t\t}\n\t};\n\n\tconst handleEventUpdate = (updatedEvent: CalendarEvent) => {\n\t\tonEventUpdate?.(updatedEvent);\n\n\t\t// Show toast notification when an event is updated via drag and drop\n\t\ttoast(`Event \"${updatedEvent.title}\" moved`, {\n\t\t\tdescription: dayjs(updatedEvent.start).format(\"MMM D, YYYY\"),\n\t\t\tposition: \"bottom-left\",\n\t\t});\n\t};\n\n\tconst viewTitle = useMemo(() => {\n\t\tconst current = dayjs(currentDate);\n\n\t\tif (view === \"month\") {\n\t\t\treturn current.format(\"YYYY年 M月\");\n\t\t}\n\t\tif (view === \"week\") {\n\t\t\tconst start = current.startOf(\"week\");\n\t\t\tconst end = current.endOf(\"week\");\n\t\t\tif (start.isSame(end, \"month\")) {\n\t\t\t\treturn start.format(\"YYYY年 M月\");\n\t\t\t}\n\t\t\treturn `${start.format(\"YYYY年 M月\")} - ${end.format(\"YYYY年 M月\")}`;\n\t\t}\n\t\tif (view === \"day\") {\n\t\t\tconst dayTitle = current.format(\"YYYY年 M月 D日(ddd)\");\n\t\t\treturn (\n\t\t\t\t<>\n\t\t\t\t\t\n\t\t\t\t\t\t{dayTitle}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{dayTitle}\n\t\t\t\t\t\n\t\t\t\t\t{dayTitle}\n\t\t\t\t\n\t\t\t);\n\t\t}\n\t\tif (view === \"agenda\") {\n\t\t\tconst start = current;\n\t\t\tconst end = current.add(AgendaDaysToShow - 1, \"day\");\n\t\t\tif (start.isSame(end, \"month\")) {\n\t\t\t\treturn start.format(\"YYYY年 M月\");\n\t\t\t}\n\t\t\treturn `${start.format(\"YYYY年 M月\")} - ${end.format(\"YYYY年 M月\")}`;\n\t\t}\n\t\treturn current.format(\"YYYY年 M月\");\n\t}, [currentDate, view]);\n\n\tconst getViewLavel = (view: CalendarView) => {\n\t\tswitch (view) {\n\t\t\tcase \"month\":\n\t\t\t\treturn \"月\";\n\t\t\tcase \"week\":\n\t\t\t\treturn \"週\";\n\t\t\tcase \"day\":\n\t\t\t\treturn \"日\";\n\t\t\tcase \"agenda\":\n\t\t\t\treturn \"今月の予定\";\n\t\t\tdefault:\n\t\t\t\treturn \"月\";\n\t\t}\n\t};\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t今日\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t

\n\t\t\t\t\t\t\t{viewTitle}\n\t\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t setView(\"month\")}>\n\t\t\t\t\t\t\t\t\t{getViewLavel(\"month\")}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t setView(\"week\")}>\n\t\t\t\t\t\t\t\t\t{getViewLavel(\"week\")}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t setView(\"day\")}>\n\t\t\t\t\t\t\t\t\t{getViewLavel(\"day\")}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t setView(\"agenda\")}>\n\t\t\t\t\t\t\t\t\t{getViewLavel(\"agenda\")}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{view === \"week\" && (\n\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\tsetIsMultiSelectMode((current) => {\n\t\t\t\t\t\t\t\t\t\tif (current) {\n\t\t\t\t\t\t\t\t\t\t\tclearPendingRanges();\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn !current;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\taria-pressed={isMultiSelectMode}\n\t\t\t\t\t\t\t\taria-label=\"複数選択モード\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t複数選択\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\tsetSelectedEvent(null);\n\t\t\t\t\t\t\t\tsetIsBatchDialogOpen(false);\n\t\t\t\t\t\t\t\tsetIsEventDialogOpen(true);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t新しい予定\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\n\n\t\t\t\t
\n\t\t\t\t\t{view === \"month\" && (\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\t{view === \"week\" && (\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\t{view === \"day\" && (\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\t{view === \"agenda\" && (\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t
\n\n\t\t\t\t{view === \"week\" && pendingRanges.length > 0 && (\n\t\t\t\t\t
\n\t\t\t\t\t\t

\n\t\t\t\t\t\t\t{pendingRanges.length}件選択中\n\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t)}\n\n\t\t\t\t {\n\t\t\t\t\t\tsetIsEventDialogOpen(false);\n\t\t\t\t\t\tsetSelectedEvent(null);\n\t\t\t\t\t\tsetIsBatchDialogOpen(false);\n\t\t\t\t\t}}\n\t\t\t\t\tonSave={handleEventSave}\n\t\t\t\t\tonSaveBatch={handleBatchSave}\n\t\t\t\t\tonDelete={handleEventDelete}\n\t\t\t\t/>\n\t\t\t
\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/index.ts", "content": "\"use client\";\n\n// Constants and utility exports\nexport * from \"../constants\";\n// Hook exports\nexport * from \"../hooks/use-current-time-indicator\";\nexport * from \"../hooks/use-event-visibility\";\nexport * from \"../hooks/use-holidays\";\nexport * from \"../hooks/use-time-range-selection\";\n// Type exports\nexport type {\n\tCalendarEvent,\n\tCalendarHoliday,\n\tCalendarView,\n\tEventColor,\n\tPendingTimeRange,\n} from \"../types\";\nexport * from \"../utils\";\n// Component exports\nexport { AgendaView } from \"./agenda-view\";\nexport { CalendarDndProvider, useCalendarDnd } from \"./calendar-dnd-context\";\nexport { DayView } from \"./day-view\";\nexport { DraggableEvent } from \"./draggable-event\";\nexport { DroppableCell } from \"./droppable-cell\";\nexport { EventCalendar } from \"./event-calendar\";\nexport { EventDialog } from \"./event-dialog\";\nexport { EventItem } from \"./event-item\";\nexport { EventsPopup } from \"./events-popup\";\nexport { HolidayLabel } from \"./holiday-label\";\nexport { MonthView } from \"./month-view\";\nexport { TimeGridColumn } from \"./time-grid-column\";\nexport { WeekView } from \"./week-view\";\n", "type": "registry:lib" }, { "path": "registry/br-hr/blocks/event-calendar/components/agenda-view.tsx", "content": "\"use client\";\n\nimport { Calendar } from \"lucide-react\";\nimport { useMemo } from \"react\";\nimport dayjs from \"../dayjs\";\nimport {\n\tAgendaDaysToShow,\n\ttype CalendarEvent,\n\ttype CalendarHoliday,\n\tEventItem,\n\tgetAgendaEventsForDay,\n\tgetHolidaysForDay,\n\tHolidayLabel,\n} from \".\";\n\ninterface AgendaViewProps {\n\tcurrentDate: Date;\n\tevents: CalendarEvent[];\n\tholidays?: CalendarHoliday[];\n\tonEventSelect: (event: CalendarEvent) => void;\n}\n\nexport function AgendaView({\n\tcurrentDate,\n\tevents,\n\tholidays = [],\n\tonEventSelect,\n}: AgendaViewProps) {\n\t// Show events for the next days based on constant\n\tconst days = useMemo(() => {\n\t\tconsole.log(\"Agenda view updating with date:\", currentDate.toISOString());\n\t\treturn Array.from({ length: AgendaDaysToShow }, (_, i) =>\n\t\t\tdayjs(currentDate).add(i, \"day\").toDate(),\n\t\t);\n\t}, [currentDate]);\n\n\tconst handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n\t\te.stopPropagation();\n\t\tconsole.log(\"Agenda view event clicked:\", event);\n\t\tonEventSelect(event);\n\t};\n\n\tconst hasContent = days.some((day) => {\n\t\tconst dayEvents = getAgendaEventsForDay(events, day);\n\t\tconst dayHolidays = getHolidaysForDay(holidays, day);\n\t\treturn dayEvents.length > 0 || dayHolidays.length > 0;\n\t});\n\n\treturn (\n\t\t
\n\t\t\t{!hasContent ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

予定がありません

\n\t\t\t\t\t

この期間に予定はありません。

\n\t\t\t\t
\n\t\t\t) : (\n\t\t\t\tdays.map((day) => {\n\t\t\t\t\tconst dayEvents = getAgendaEventsForDay(events, day);\n\t\t\t\t\tconst dayHolidays = getHolidaysForDay(holidays, day);\n\n\t\t\t\t\tif (dayEvents.length === 0 && dayHolidays.length === 0) return null;\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{dayjs(day).format(\"M月D日(ddd)\")}\n\t\t\t\t\t\t\t\t{dayHolidays.map((holiday) => (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{dayEvents.map((event) => (\n\t\t\t\t\t\t\t\t\t handleEventClick(event, e)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t)}\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/calendar-dnd-context.tsx", "content": "\"use client\";\n\nimport {\n\tDndContext,\n\ttype DragEndEvent,\n\ttype DragOverEvent,\n\tDragOverlay,\n\ttype DragStartEvent,\n\tMouseSensor,\n\tPointerSensor,\n\tTouchSensor,\n\ttype UniqueIdentifier,\n\tuseSensor,\n\tuseSensors,\n} from \"@dnd-kit/core\";\nimport {\n\tcreateContext,\n\ttype ReactNode,\n\tuseContext,\n\tuseId,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport dayjs from \"../dayjs\";\n\nimport { type CalendarEvent, EventItem } from \".\";\n\n// Define the context type\ntype CalendarDndContextType = {\n\tactiveEvent: CalendarEvent | null;\n\tactiveId: UniqueIdentifier | null;\n\tactiveView: \"month\" | \"week\" | \"day\" | null;\n\tcurrentTime: Date | null;\n\teventHeight: number | null;\n\tisMultiDay: boolean;\n\tmultiDayWidth: number | null;\n\tdragHandlePosition: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tdata?: {\n\t\t\tisFirstDay?: boolean;\n\t\t\tisLastDay?: boolean;\n\t\t};\n\t} | null;\n};\n\n// Create the context\nconst CalendarDndContext = createContext({\n\tactiveEvent: null,\n\tactiveId: null,\n\tactiveView: null,\n\tcurrentTime: null,\n\teventHeight: null,\n\tisMultiDay: false,\n\tmultiDayWidth: null,\n\tdragHandlePosition: null,\n});\n\n// Hook to use the context\nexport const useCalendarDnd = () => useContext(CalendarDndContext);\n\n// Props for the provider\ninterface CalendarDndProviderProps {\n\tchildren: ReactNode;\n\tonEventUpdate: (event: CalendarEvent) => void;\n}\n\nexport function CalendarDndProvider({\n\tchildren,\n\tonEventUpdate,\n}: CalendarDndProviderProps) {\n\tconst [activeEvent, setActiveEvent] = useState(null);\n\tconst [activeId, setActiveId] = useState(null);\n\tconst [activeView, setActiveView] = useState<\"month\" | \"week\" | \"day\" | null>(\n\t\tnull,\n\t);\n\tconst [currentTime, setCurrentTime] = useState(null);\n\tconst [eventHeight, setEventHeight] = useState(null);\n\tconst [isMultiDay, setIsMultiDay] = useState(false);\n\tconst [multiDayWidth, setMultiDayWidth] = useState(null);\n\tconst [dragHandlePosition, setDragHandlePosition] = useState<{\n\t\tx?: number;\n\t\ty?: number;\n\t\tdata?: {\n\t\t\tisFirstDay?: boolean;\n\t\t\tisLastDay?: boolean;\n\t\t};\n\t} | null>(null);\n\n\t// Store original event dimensions\n\tconst eventDimensions = useRef<{ height: number }>({ height: 0 });\n\n\t// Configure sensors for better drag detection\n\tconst sensors = useSensors(\n\t\tuseSensor(MouseSensor, {\n\t\t\t// Require the mouse to move by 5px before activating\n\t\t\tactivationConstraint: {\n\t\t\t\tdistance: 5,\n\t\t\t},\n\t\t}),\n\t\tuseSensor(TouchSensor, {\n\t\t\t// Press delay of 250ms, with tolerance of 5px of movement\n\t\t\tactivationConstraint: {\n\t\t\t\tdelay: 250,\n\t\t\t\ttolerance: 5,\n\t\t\t},\n\t\t}),\n\t\tuseSensor(PointerSensor, {\n\t\t\t// Require the pointer to move by 5px before activating\n\t\t\tactivationConstraint: {\n\t\t\t\tdistance: 5,\n\t\t\t},\n\t\t}),\n\t);\n\n\t// Generate a stable ID for the DndContext\n\tconst dndContextId = useId();\n\n\tconst handleDragStart = (event: DragStartEvent) => {\n\t\tconst { active } = event;\n\n\t\t// Add safety check for data.current\n\t\tif (!active.data.current) {\n\t\t\tconsole.error(\"Missing data in drag start event\", event);\n\t\t\treturn;\n\t\t}\n\n\t\tconst {\n\t\t\tevent: calendarEvent,\n\t\t\tview,\n\t\t\theight,\n\t\t\tisMultiDay: eventIsMultiDay,\n\t\t\tmultiDayWidth: eventMultiDayWidth,\n\t\t\tdragHandlePosition: eventDragHandlePosition,\n\t\t} = active.data.current as {\n\t\t\tevent: CalendarEvent;\n\t\t\tview: \"month\" | \"week\" | \"day\";\n\t\t\theight?: number;\n\t\t\tisMultiDay?: boolean;\n\t\t\tmultiDayWidth?: number;\n\t\t\tdragHandlePosition?: {\n\t\t\t\tx?: number;\n\t\t\t\ty?: number;\n\t\t\t\tdata?: {\n\t\t\t\t\tisFirstDay?: boolean;\n\t\t\t\t\tisLastDay?: boolean;\n\t\t\t\t};\n\t\t\t};\n\t\t};\n\n\t\tsetActiveEvent(calendarEvent);\n\t\tsetActiveId(active.id);\n\t\tsetActiveView(view);\n\t\tsetCurrentTime(new Date(calendarEvent.start));\n\t\tsetIsMultiDay(eventIsMultiDay || false);\n\t\tsetMultiDayWidth(eventMultiDayWidth || null);\n\t\tsetDragHandlePosition(eventDragHandlePosition || null);\n\n\t\t// Store event height if provided\n\t\tif (height) {\n\t\t\teventDimensions.current.height = height;\n\t\t\tsetEventHeight(height);\n\t\t}\n\t};\n\n\tconst handleDragOver = (event: DragOverEvent) => {\n\t\tconst { over } = event;\n\n\t\tif (over && activeEvent && over.data.current) {\n\t\t\tconst { date, time } = over.data.current as { date: Date; time?: number };\n\n\t\t\t// Update time for week/day views\n\t\t\tif (time !== undefined && activeView !== \"month\") {\n\t\t\t\tconst newTime = new Date(date);\n\n\t\t\t\t// Calculate hours and minutes with 15-minute precision\n\t\t\t\tconst hours = Math.floor(time);\n\t\t\t\tconst fractionalHour = time - hours;\n\n\t\t\t\t// Map to nearest 15 minute interval (0, 0.25, 0.5, 0.75)\n\t\t\t\tlet minutes = 0;\n\t\t\t\tif (fractionalHour < 0.125) minutes = 0;\n\t\t\t\telse if (fractionalHour < 0.375) minutes = 15;\n\t\t\t\telse if (fractionalHour < 0.625) minutes = 30;\n\t\t\t\telse minutes = 45;\n\n\t\t\t\tnewTime.setHours(hours, minutes, 0, 0);\n\n\t\t\t\t// Only update if time has changed\n\t\t\t\tif (\n\t\t\t\t\t!currentTime ||\n\t\t\t\t\tnewTime.getHours() !== currentTime.getHours() ||\n\t\t\t\t\tnewTime.getMinutes() !== currentTime.getMinutes() ||\n\t\t\t\t\tnewTime.getDate() !== currentTime.getDate() ||\n\t\t\t\t\tnewTime.getMonth() !== currentTime.getMonth() ||\n\t\t\t\t\tnewTime.getFullYear() !== currentTime.getFullYear()\n\t\t\t\t) {\n\t\t\t\t\tsetCurrentTime(newTime);\n\t\t\t\t}\n\t\t\t} else if (activeView === \"month\") {\n\t\t\t\t// For month view, just update the date but preserve time\n\t\t\t\tconst newTime = new Date(date);\n\t\t\t\tif (currentTime) {\n\t\t\t\t\tnewTime.setHours(\n\t\t\t\t\t\tcurrentTime.getHours(),\n\t\t\t\t\t\tcurrentTime.getMinutes(),\n\t\t\t\t\t\tcurrentTime.getSeconds(),\n\t\t\t\t\t\tcurrentTime.getMilliseconds(),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Only update if date has changed\n\t\t\t\tif (\n\t\t\t\t\t!currentTime ||\n\t\t\t\t\tnewTime.getDate() !== currentTime.getDate() ||\n\t\t\t\t\tnewTime.getMonth() !== currentTime.getMonth() ||\n\t\t\t\t\tnewTime.getFullYear() !== currentTime.getFullYear()\n\t\t\t\t) {\n\t\t\t\t\tsetCurrentTime(newTime);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst handleDragEnd = (event: DragEndEvent) => {\n\t\tconst { active, over } = event;\n\n\t\t// Add robust error checking\n\t\tif (!over || !activeEvent || !currentTime) {\n\t\t\t// Reset state and exit early\n\t\t\tsetActiveEvent(null);\n\t\t\tsetActiveId(null);\n\t\t\tsetActiveView(null);\n\t\t\tsetCurrentTime(null);\n\t\t\tsetEventHeight(null);\n\t\t\tsetIsMultiDay(false);\n\t\t\tsetMultiDayWidth(null);\n\t\t\tsetDragHandlePosition(null);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// Safely access data with checks\n\t\t\tif (!active.data.current || !over.data.current) {\n\t\t\t\tthrow new Error(\"Missing data in drag event\");\n\t\t\t}\n\n\t\t\tconst activeData = active.data.current as {\n\t\t\t\tevent?: CalendarEvent;\n\t\t\t\tview?: string;\n\t\t\t};\n\t\t\tconst overData = over.data.current as { date?: Date; time?: number };\n\n\t\t\t// Verify we have all required data\n\t\t\tif (!activeData.event || !overData.date) {\n\t\t\t\tthrow new Error(\"Missing required event data\");\n\t\t\t}\n\n\t\t\tconst calendarEvent = activeData.event;\n\t\t\tconst date = overData.date;\n\t\t\tconst time = overData.time;\n\n\t\t\t// Calculate new start time\n\t\t\tconst newStart = new Date(date);\n\n\t\t\t// If time is provided (for week/day views), set the hours and minutes\n\t\t\tif (time !== undefined) {\n\t\t\t\tconst hours = Math.floor(time);\n\t\t\t\tconst fractionalHour = time - hours;\n\n\t\t\t\t// Map to nearest 15 minute interval (0, 0.25, 0.5, 0.75)\n\t\t\t\tlet minutes = 0;\n\t\t\t\tif (fractionalHour < 0.125) minutes = 0;\n\t\t\t\telse if (fractionalHour < 0.375) minutes = 15;\n\t\t\t\telse if (fractionalHour < 0.625) minutes = 30;\n\t\t\t\telse minutes = 45;\n\n\t\t\t\tnewStart.setHours(hours, minutes, 0, 0);\n\t\t\t} else {\n\t\t\t\t// For month view, preserve the original time from currentTime\n\t\t\t\tnewStart.setHours(\n\t\t\t\t\tcurrentTime.getHours(),\n\t\t\t\t\tcurrentTime.getMinutes(),\n\t\t\t\t\tcurrentTime.getSeconds(),\n\t\t\t\t\tcurrentTime.getMilliseconds(),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Calculate new end time based on the original duration\n\t\t\tconst originalStart = new Date(calendarEvent.start);\n\t\t\tconst originalEnd = new Date(calendarEvent.end);\n\t\t\tconst durationMinutes = dayjs(originalEnd).diff(\n\t\t\t\tdayjs(originalStart),\n\t\t\t\t\"minute\",\n\t\t\t);\n\t\t\tconst newEnd = dayjs(newStart).add(durationMinutes, \"minute\").toDate();\n\n\t\t\t// Only update if the start time has actually changed\n\t\t\tconst hasStartTimeChanged =\n\t\t\t\toriginalStart.getFullYear() !== newStart.getFullYear() ||\n\t\t\t\toriginalStart.getMonth() !== newStart.getMonth() ||\n\t\t\t\toriginalStart.getDate() !== newStart.getDate() ||\n\t\t\t\toriginalStart.getHours() !== newStart.getHours() ||\n\t\t\t\toriginalStart.getMinutes() !== newStart.getMinutes();\n\n\t\t\tif (hasStartTimeChanged) {\n\t\t\t\t// Update the event only if the time has changed\n\t\t\t\tonEventUpdate({\n\t\t\t\t\t...calendarEvent,\n\t\t\t\t\tstart: newStart,\n\t\t\t\t\tend: newEnd,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error in drag end handler:\", error);\n\t\t} finally {\n\t\t\t// Always reset state\n\t\t\tsetActiveEvent(null);\n\t\t\tsetActiveId(null);\n\t\t\tsetActiveView(null);\n\t\t\tsetCurrentTime(null);\n\t\t\tsetEventHeight(null);\n\t\t\tsetIsMultiDay(false);\n\t\t\tsetMultiDayWidth(null);\n\t\t\tsetDragHandlePosition(null);\n\t\t}\n\t};\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t{children}\n\n\t\t\t\t\n\t\t\t\t\t{activeEvent && activeView && (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/day-view.tsx", "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { EndHour, StartHour } from \"../constants\";\nimport { areIntervalsOverlapping, eachHourOfInterval } from \"../date-utils\";\nimport dayjs from \"../dayjs\";\nimport {\n\ttype CalendarEvent,\n\ttype CalendarHoliday,\n\tDraggableEvent,\n\tEventItem,\n\tgetDayCellBackgroundClasses,\n\tgetHolidaysForDay,\n\tHolidayLabel,\n\tisMultiDayEvent,\n\tTimeGridColumn,\n\tuseCurrentTimeIndicator,\n\tWeekCellsHeight,\n} from \".\";\n\ninterface DayViewProps {\n\tcurrentDate: Date;\n\tevents: CalendarEvent[];\n\tholidays?: CalendarHoliday[];\n\tonEventSelect: (event: CalendarEvent) => void;\n\tonEventCreate: (startTime: Date, endTime?: Date) => void;\n}\n\ninterface PositionedEvent {\n\tevent: CalendarEvent;\n\ttop: number;\n\theight: number;\n\tleft: number;\n\twidth: number;\n\tzIndex: number;\n}\n\nexport function DayView({\n\tcurrentDate,\n\tevents,\n\tholidays = [],\n\tonEventSelect,\n\tonEventCreate,\n}: DayViewProps) {\n\tconst dayHolidays = useMemo(\n\t\t() => getHolidaysForDay(holidays, currentDate),\n\t\t[holidays, currentDate],\n\t);\n\tconst hours = useMemo(() => {\n\t\tconst dayStart = dayjs(currentDate).startOf(\"day\");\n\t\treturn eachHourOfInterval(\n\t\t\tdayStart.add(StartHour, \"hour\").toDate(),\n\t\t\tdayStart.add(EndHour - 1, \"hour\").toDate(),\n\t\t);\n\t}, [currentDate]);\n\n\tconst dayEvents = useMemo(() => {\n\t\treturn events\n\t\t\t.filter((event) => {\n\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\tconst dayTime = dayjs(currentDate);\n\t\t\t\treturn (\n\t\t\t\t\tdayTime.isSame(eventStart, \"day\") ||\n\t\t\t\t\tdayTime.isSame(eventEnd, \"day\") ||\n\t\t\t\t\t(dayTime.isAfter(eventStart, \"day\") &&\n\t\t\t\t\t\tdayTime.isBefore(eventEnd, \"day\"))\n\t\t\t\t);\n\t\t\t})\n\t\t\t.sort(\n\t\t\t\t(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(),\n\t\t\t);\n\t}, [currentDate, events]);\n\n\t// Filter all-day events\n\tconst allDayEvents = useMemo(() => {\n\t\treturn dayEvents.filter((event) => {\n\t\t\t// Include explicitly marked all-day events or multi-day events\n\t\t\treturn event.allDay || isMultiDayEvent(event);\n\t\t});\n\t}, [dayEvents]);\n\n\t// Get only single-day time-based events\n\tconst timeEvents = useMemo(() => {\n\t\treturn dayEvents.filter((event) => {\n\t\t\t// Exclude all-day events and multi-day events\n\t\t\treturn !event.allDay && !isMultiDayEvent(event);\n\t\t});\n\t}, [dayEvents]);\n\n\t// Process events to calculate positions\n\tconst positionedEvents = useMemo(() => {\n\t\tconst result: PositionedEvent[] = [];\n\t\tconst dayStart = dayjs(currentDate).startOf(\"day\").toDate();\n\n\t\t// Sort events by start time and duration\n\t\tconst sortedEvents = [...timeEvents].sort((a, b) => {\n\t\t\tconst aStart = new Date(a.start);\n\t\t\tconst bStart = new Date(b.start);\n\t\t\tconst aEnd = new Date(a.end);\n\t\t\tconst bEnd = new Date(b.end);\n\n\t\t\t// First sort by start time\n\t\t\tif (aStart < bStart) return -1;\n\t\t\tif (aStart > bStart) return 1;\n\n\t\t\t// If start times are equal, sort by duration (longer events first)\n\t\t\tconst aDuration = dayjs(aEnd).diff(dayjs(aStart), \"minute\");\n\t\t\tconst bDuration = dayjs(bEnd).diff(dayjs(bStart), \"minute\");\n\t\t\treturn bDuration - aDuration;\n\t\t});\n\n\t\t// Track columns for overlapping events\n\t\tconst columns: { event: CalendarEvent; end: Date }[][] = [];\n\n\t\tsortedEvents.forEach((event) => {\n\t\t\tconst eventStart = new Date(event.start);\n\t\t\tconst eventEnd = new Date(event.end);\n\n\t\t\t// Adjust start and end times if they're outside this day\n\t\t\tconst adjustedStart = dayjs(currentDate).isSame(eventStart, \"day\")\n\t\t\t\t? eventStart\n\t\t\t\t: dayStart;\n\t\t\tconst adjustedEnd = dayjs(currentDate).isSame(eventEnd, \"day\")\n\t\t\t\t? eventEnd\n\t\t\t\t: dayjs(dayStart).add(24, \"hour\").toDate();\n\n\t\t\t// Calculate top position and height\n\t\t\tconst adjustedStartTime = dayjs(adjustedStart);\n\t\t\tconst adjustedEndTime = dayjs(adjustedEnd);\n\t\t\tconst startHour =\n\t\t\t\tadjustedStartTime.hour() + adjustedStartTime.minute() / 60;\n\t\t\tconst endHour = adjustedEndTime.hour() + adjustedEndTime.minute() / 60;\n\t\t\tconst top = (startHour - StartHour) * WeekCellsHeight;\n\t\t\tconst height = (endHour - startHour) * WeekCellsHeight;\n\n\t\t\t// Find a column for this event\n\t\t\tlet columnIndex = 0;\n\t\t\tlet placed = false;\n\n\t\t\twhile (!placed) {\n\t\t\t\tconst col = columns[columnIndex] || [];\n\t\t\t\tif (col.length === 0) {\n\t\t\t\t\tcolumns[columnIndex] = col;\n\t\t\t\t\tplaced = true;\n\t\t\t\t} else {\n\t\t\t\t\tconst overlaps = col.some((c) =>\n\t\t\t\t\t\tareIntervalsOverlapping(\n\t\t\t\t\t\t\t{ start: adjustedStart, end: adjustedEnd },\n\t\t\t\t\t\t\t{ start: new Date(c.event.start), end: new Date(c.event.end) },\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tif (!overlaps) {\n\t\t\t\t\t\tplaced = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolumnIndex++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Ensure column is initialized before pushing\n\t\t\tconst currentColumn = columns[columnIndex] || [];\n\t\t\tcolumns[columnIndex] = currentColumn;\n\t\t\tcurrentColumn.push({ event, end: adjustedEnd });\n\n\t\t\t// First column takes full width, others are indented by 10% and take 90% width\n\t\t\tconst width = columnIndex === 0 ? 1 : 0.9;\n\t\t\tconst left = columnIndex === 0 ? 0 : columnIndex * 0.1;\n\n\t\t\tresult.push({\n\t\t\t\tevent,\n\t\t\t\ttop,\n\t\t\t\theight,\n\t\t\t\tleft,\n\t\t\t\twidth,\n\t\t\t\tzIndex: 10 + columnIndex, // Higher columns get higher z-index\n\t\t\t});\n\t\t});\n\n\t\treturn result;\n\t}, [currentDate, timeEvents]);\n\n\tconst handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n\t\te.stopPropagation();\n\t\tonEventSelect(event);\n\t};\n\n\tconst showAllDaySection = allDayEvents.length > 0 || dayHolidays.length > 0;\n\tconst { currentTimePosition, currentTimeVisible } = useCurrentTimeIndicator(\n\t\tcurrentDate,\n\t\t\"day\",\n\t);\n\n\treturn (\n\t\t
\n\t\t\t{showAllDaySection && (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t終日\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{dayHolidays.map((holiday) => (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t{allDayEvents.map((event) => {\n\t\t\t\t\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\t\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\t\t\t\t\tconst isFirstDay = dayjs(currentDate).isSame(eventStart, \"day\");\n\t\t\t\t\t\t\t\tconst isLastDay = dayjs(currentDate).isSame(eventEnd, \"day\");\n\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t handleEventClick(event, e)}\n\t\t\t\t\t\t\t\t\t\tevent={event}\n\t\t\t\t\t\t\t\t\t\tview=\"month\"\n\t\t\t\t\t\t\t\t\t\tisFirstDay={isFirstDay}\n\t\t\t\t\t\t\t\t\t\tisLastDay={isLastDay}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{/* Always show the title in day view for better usability */}\n\t\t\t\t\t\t\t\t\t\t
{event.title}
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{hours.map((hour, index) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{index > 0 && (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{dayjs(hour).format(\"HH:00\")}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t
\n\t\t\t\t\t))}\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t\t\t{positionedEvents.map((positionedEvent) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t handleEventClick(positionedEvent.event, e)}\n\t\t\t\t\t\t\t\t\tshowTime\n\t\t\t\t\t\t\t\t\theight={positionedEvent.height}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t))}\n\n\t\t\t\t\t{currentTimeVisible && (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/draggable-event.tsx", "content": "\"use client\";\n\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { useRef, useState } from \"react\";\nimport dayjs from \"../dayjs\";\n\nimport { type CalendarEvent, EventItem, useCalendarDnd } from \".\";\n\ninterface DraggableEventProps {\n\tevent: CalendarEvent;\n\tview: \"month\" | \"week\" | \"day\";\n\tshowTime?: boolean;\n\tonClick?: (e: React.MouseEvent) => void;\n\theight?: number;\n\tisMultiDay?: boolean;\n\tmultiDayWidth?: number;\n\tisFirstDay?: boolean;\n\tisLastDay?: boolean;\n\t\"aria-hidden\"?: boolean | \"true\" | \"false\";\n}\n\nexport function DraggableEvent({\n\tevent,\n\tview,\n\tshowTime,\n\tonClick,\n\theight,\n\tisMultiDay,\n\tmultiDayWidth,\n\tisFirstDay = true,\n\tisLastDay = true,\n\t\"aria-hidden\": ariaHidden,\n}: DraggableEventProps) {\n\tconst { activeId } = useCalendarDnd();\n\tconst elementRef = useRef(null);\n\tconst [dragHandlePosition, setDragHandlePosition] = useState<{\n\t\tx: number;\n\t\ty: number;\n\t} | null>(null);\n\n\t// Check if this is a multi-day event\n\tconst eventStart = new Date(event.start);\n\tconst eventEnd = new Date(event.end);\n\tconst isMultiDayEvent =\n\t\tisMultiDay ||\n\t\tevent.allDay ||\n\t\tdayjs(eventEnd).diff(dayjs(eventStart), \"day\") >= 1;\n\n\tconst { attributes, listeners, setNodeRef, transform, isDragging } =\n\t\tuseDraggable({\n\t\t\tid: `${event.id}-${view}`,\n\t\t\tdata: {\n\t\t\t\tevent,\n\t\t\t\tview,\n\t\t\t\theight: height || elementRef.current?.offsetHeight || null,\n\t\t\t\tisMultiDay: isMultiDayEvent,\n\t\t\t\tmultiDayWidth: multiDayWidth,\n\t\t\t\tdragHandlePosition,\n\t\t\t\tisFirstDay,\n\t\t\t\tisLastDay,\n\t\t\t},\n\t\t});\n\n\t// Handle mouse down to track where on the event the user clicked\n\tconst handleMouseDown = (e: React.MouseEvent) => {\n\t\tif (elementRef.current) {\n\t\t\tconst rect = elementRef.current.getBoundingClientRect();\n\t\t\tsetDragHandlePosition({\n\t\t\t\tx: e.clientX - rect.left,\n\t\t\t\ty: e.clientY - rect.top,\n\t\t\t});\n\t\t}\n\t};\n\n\t// Don't render if this event is being dragged\n\tif (isDragging || activeId === `${event.id}-${view}`) {\n\t\treturn (\n\t\t\t\n\t\t);\n\t}\n\n\tconst style = transform\n\t\t? {\n\t\t\t\ttransform: CSS.Translate.toString(transform),\n\t\t\t\theight: height || \"auto\",\n\t\t\t\twidth:\n\t\t\t\t\tisMultiDayEvent && multiDayWidth ? `${multiDayWidth}%` : undefined,\n\t\t\t}\n\t\t: {\n\t\t\t\theight: height || \"auto\",\n\t\t\t\twidth:\n\t\t\t\t\tisMultiDayEvent && multiDayWidth ? `${multiDayWidth}%` : undefined,\n\t\t\t};\n\n\t// Handle touch start to track where on the event the user touched\n\tconst handleTouchStart = (e: React.TouchEvent) => {\n\t\tif (elementRef.current) {\n\t\t\tconst rect = elementRef.current.getBoundingClientRect();\n\t\t\tconst touch = e.touches[0];\n\t\t\tif (touch) {\n\t\t\t\tsetDragHandlePosition({\n\t\t\t\t\tx: touch.clientX - rect.left,\n\t\t\t\t\ty: touch.clientY - rect.top,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tsetNodeRef(node);\n\t\t\t\tif (elementRef) elementRef.current = node;\n\t\t\t}}\n\t\t\tstyle={style}\n\t\t\tclassName=\"touch-none\"\n\t\t>\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/droppable-cell.tsx", "content": "\"use client\";\n\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { cn } from \"@/lib/utils\";\nimport { useCalendarDnd } from \".\";\n\ninterface DroppableCellProps {\n\tid: string;\n\tdate: Date;\n\ttime?: number; // For week/day views, represents hours (e.g., 9.25 for 9:15)\n\tchildren?: React.ReactNode;\n\tclassName?: string;\n\tonClick?: () => void;\n}\n\nexport function DroppableCell({\n\tid,\n\tdate,\n\ttime,\n\tchildren,\n\tclassName,\n\tonClick,\n}: DroppableCellProps) {\n\tconst { activeEvent } = useCalendarDnd();\n\n\tconst { setNodeRef, isOver } = useDroppable({\n\t\tid,\n\t\tdata: {\n\t\t\tdate,\n\t\t\ttime,\n\t\t},\n\t});\n\n\t// Format time for display in tooltip (only for debugging)\n\tconst formattedTime =\n\t\ttime !== undefined\n\t\t\t? `${Math.floor(time)}:${Math.round((time - Math.floor(time)) * 60)\n\t\t\t\t\t.toString()\n\t\t\t\t\t.padStart(2, \"0\")}`\n\t\t\t: null;\n\n\treturn (\n\t\t// biome-ignore lint/a11y/noStaticElementInteractions: 外部交互需要\n\t\t// biome-ignore lint/a11y/useKeyWithClickEvents: 外部交互需要\n\t\t\n\t\t\t{children}\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/event-dialog.tsx", "content": "\"use client\";\n\nimport { RiCalendarLine, RiDeleteBinLine } from \"@remixicon/react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { ja as dayPickerJa } from \"react-day-picker/locale\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n\tDialog,\n\tDialogContent,\n\tDialogDescription,\n\tDialogFooter,\n\tDialogHeader,\n\tDialogTitle,\n} from \"@/components/ui/dialog\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/br-hr/ui/button\";\nimport { Calendar } from \"@/registry/br-hr/ui/calendar\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/registry/br-hr/ui/popover\";\nimport {\n\tSelect,\n\tSelectContent,\n\tSelectItem,\n\tSelectTrigger,\n\tSelectValue,\n} from \"@/registry/br-hr/ui/select\";\nimport { Input } from \"@/registry/new-york/ui/input\";\nimport { Label } from \"@/registry/new-york/ui/label\";\nimport { Textarea } from \"@/registry/new-york/ui/textarea\";\nimport {\n\tDefaultEndHour,\n\tDefaultStartHour,\n\tEndHour,\n\tStartHour,\n} from \"../constants\";\nimport dayjs from \"../dayjs\";\nimport type { CalendarEvent, EventColor, PendingTimeRange } from \".\";\n\nexport interface BatchEventData {\n\ttitle: string;\n\tdescription?: string;\n\tlocation?: string;\n\tcolor?: EventColor;\n}\n\ninterface EventDialogProps {\n\tevent: CalendarEvent | null;\n\tpendingRanges?: PendingTimeRange[];\n\tisOpen: boolean;\n\tonClose: () => void;\n\tonSave: (event: CalendarEvent) => void;\n\tonSaveBatch?: (data: BatchEventData) => void;\n\tonDelete: (eventId: string) => void;\n}\n\nexport function EventDialog({\n\tevent,\n\tpendingRanges,\n\tisOpen,\n\tonClose,\n\tonSave,\n\tonSaveBatch,\n\tonDelete,\n}: EventDialogProps) {\n\tconst [title, setTitle] = useState(\"\");\n\tconst [description, setDescription] = useState(\"\");\n\tconst [startDate, setStartDate] = useState(() => dayjs().toDate());\n\tconst [endDate, setEndDate] = useState(() => dayjs().toDate());\n\tconst [startTime, setStartTime] = useState(`${DefaultStartHour}:00`);\n\tconst [endTime, setEndTime] = useState(`${DefaultEndHour}:00`);\n\tconst [allDay, setAllDay] = useState(false);\n\tconst [location, setLocation] = useState(\"\");\n\tconst [color, setColor] = useState(\"sky\");\n\tconst [error, setError] = useState(null);\n\tconst [startDateOpen, setStartDateOpen] = useState(false);\n\tconst [endDateOpen, setEndDateOpen] = useState(false);\n\n\t// Debug log to check what event is being passed\n\tuseEffect(() => {\n\t\tconsole.log(\"EventDialog received event:\", event);\n\t}, [event]);\n\n\tconst isBatchMode = Boolean(pendingRanges && pendingRanges.length > 0);\n\n\tconst sortedPendingRanges = useMemo(() => {\n\t\tif (!pendingRanges) return [];\n\t\treturn [...pendingRanges].sort(\n\t\t\t(a, b) => a.start.getTime() - b.start.getTime(),\n\t\t);\n\t}, [pendingRanges]);\n\n\tconst resetForm = () => {\n\t\tsetTitle(\"\");\n\t\tsetDescription(\"\");\n\t\tsetStartDate(dayjs().toDate());\n\t\tsetEndDate(dayjs().toDate());\n\t\tsetStartTime(`${DefaultStartHour}:00`);\n\t\tsetEndTime(`${DefaultEndHour}:00`);\n\t\tsetAllDay(false);\n\t\tsetLocation(\"\");\n\t\tsetColor(\"sky\");\n\t\tsetError(null);\n\t\tsetStartDateOpen(false);\n\t\tsetEndDateOpen(false);\n\t};\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: isOpen/pendingRangesでフォームを同期\n\tuseEffect(() => {\n\t\tif (!isOpen) {\n\t\t\tresetForm();\n\t\t\treturn;\n\t\t}\n\n\t\tif (isBatchMode) {\n\t\t\tsetTitle(\"\");\n\t\t\tsetDescription(\"\");\n\t\t\tsetLocation(\"\");\n\t\t\tsetColor(\"sky\");\n\t\t\tsetAllDay(false);\n\t\t\tsetError(null);\n\t\t\treturn;\n\t\t}\n\n\t\tif (event) {\n\t\t\tsetTitle(event.title || \"\");\n\t\t\tsetDescription(event.description || \"\");\n\n\t\t\tconst start = new Date(event.start);\n\t\t\tconst end = new Date(event.end);\n\n\t\t\tsetStartDate(start);\n\t\t\tsetEndDate(end);\n\t\t\tsetStartTime(formatTimeForInput(start));\n\t\t\tsetEndTime(formatTimeForInput(end));\n\t\t\tsetAllDay(event.allDay || false);\n\t\t\tsetLocation(event.location || \"\");\n\t\t\tsetColor((event.color as EventColor) || \"sky\");\n\t\t\tsetError(null);\n\t\t} else {\n\t\t\tresetForm();\n\t\t}\n\t}, [isOpen, event, isBatchMode, pendingRanges]);\n\n\tconst formatTimeForInput = (date: Date) => {\n\t\tconst hours = date.getHours().toString().padStart(2, \"0\");\n\t\tconst minutes = Math.floor(date.getMinutes() / 15) * 15;\n\t\treturn `${hours}:${minutes.toString().padStart(2, \"0\")}`;\n\t};\n\n\t// Memoize time options so they're only calculated once\n\tconst timeOptions = useMemo(() => {\n\t\tconst options = [];\n\t\tfor (let hour = StartHour; hour <= EndHour; hour++) {\n\t\t\tfor (let minute = 0; minute < 60; minute += 15) {\n\t\t\t\tconst formattedHour = hour.toString().padStart(2, \"0\");\n\t\t\t\tconst formattedMinute = minute.toString().padStart(2, \"0\");\n\t\t\t\tconst value = `${formattedHour}:${formattedMinute}`;\n\t\t\t\t// Use a fixed date to avoid unnecessary date object creations\n\t\t\t\tconst date = new Date(2000, 0, 1, hour, minute);\n\t\t\t\tconst label = dayjs(date).format(\"HH:mm\");\n\t\t\t\toptions.push({ value, label });\n\t\t\t}\n\t\t}\n\t\treturn options;\n\t}, []); // Empty dependency array ensures this only runs once\n\n\tconst handleSave = () => {\n\t\tif (isBatchMode) {\n\t\t\tconst eventTitle = title.trim() ? title : \"(タイトルなし)\";\n\t\t\tonSaveBatch?.({\n\t\t\t\ttitle: eventTitle,\n\t\t\t\tdescription: description || undefined,\n\t\t\t\tlocation: location || undefined,\n\t\t\t\tcolor,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst start = new Date(startDate);\n\t\tconst end = new Date(endDate);\n\n\t\tif (!allDay) {\n\t\t\tconst [startHours = 0, startMinutes = 0] = startTime\n\t\t\t\t.split(\":\")\n\t\t\t\t.map(Number);\n\t\t\tconst [endHours = 0, endMinutes = 0] = endTime.split(\":\").map(Number);\n\n\t\t\tif (\n\t\t\t\tstartHours < StartHour ||\n\t\t\t\tstartHours > EndHour ||\n\t\t\t\tendHours < StartHour ||\n\t\t\t\tendHours > EndHour\n\t\t\t) {\n\t\t\t\tsetError(\n\t\t\t\t\t`時刻は ${StartHour}:00 から ${EndHour}:00 の間で選択してください`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tstart.setHours(startHours, startMinutes, 0);\n\t\t\tend.setHours(endHours, endMinutes, 0);\n\t\t} else {\n\t\t\tstart.setHours(0, 0, 0, 0);\n\t\t\tend.setHours(23, 59, 59, 999);\n\t\t}\n\n\t\t// Validate that end date is not before start date\n\t\tif (dayjs(end).isBefore(start)) {\n\t\t\tsetError(\"終了日は開始日より前にできません\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Use generic title if empty\n\t\tconst eventTitle = title.trim() ? title : \"(タイトルなし)\";\n\n\t\tonSave({\n\t\t\tid: event?.id || \"\",\n\t\t\ttitle: eventTitle,\n\t\t\tdescription,\n\t\t\tstart,\n\t\t\tend,\n\t\t\tallDay,\n\t\t\tlocation,\n\t\t\tcolor,\n\t\t});\n\t};\n\n\tconst handleDelete = () => {\n\t\tif (event?.id) {\n\t\t\tonDelete(event.id);\n\t\t}\n\t};\n\n\t// Updated color options to match types.ts\n\tconst colorOptions: Array<{\n\t\tvalue: EventColor;\n\t\tlabel: string;\n\t\tbgClass: string;\n\t\tborderClass: string;\n\t}> = [\n\t\t{\n\t\t\tvalue: \"sky\",\n\t\t\tlabel: \"水色\",\n\t\t\tbgClass: \"bg-sky-400 data-[state=checked]:bg-sky-400\",\n\t\t\tborderClass: \"border-sky-400 data-[state=checked]:border-sky-400\",\n\t\t},\n\t\t{\n\t\t\tvalue: \"amber\",\n\t\t\tlabel: \"黄\",\n\t\t\tbgClass: \"bg-amber-400 data-[state=checked]:bg-amber-400\",\n\t\t\tborderClass: \"border-amber-400 data-[state=checked]:border-amber-400\",\n\t\t},\n\t\t{\n\t\t\tvalue: \"violet\",\n\t\t\tlabel: \"紫\",\n\t\t\tbgClass: \"bg-violet-400 data-[state=checked]:bg-violet-400\",\n\t\t\tborderClass: \"border-violet-400 data-[state=checked]:border-violet-400\",\n\t\t},\n\t\t{\n\t\t\tvalue: \"rose\",\n\t\t\tlabel: \"ピンク\",\n\t\t\tbgClass: \"bg-rose-400 data-[state=checked]:bg-rose-400\",\n\t\t\tborderClass: \"border-rose-400 data-[state=checked]:border-rose-400\",\n\t\t},\n\t\t{\n\t\t\tvalue: \"emerald\",\n\t\t\tlabel: \"緑\",\n\t\t\tbgClass: \"bg-emerald-400 data-[state=checked]:bg-emerald-400\",\n\t\t\tborderClass: \"border-emerald-400 data-[state=checked]:border-emerald-400\",\n\t\t},\n\t\t{\n\t\t\tvalue: \"orange\",\n\t\t\tlabel: \"オレンジ\",\n\t\t\tbgClass: \"bg-orange-400 data-[state=checked]:bg-orange-400\",\n\t\t\tborderClass: \"border-orange-400 data-[state=checked]:border-orange-400\",\n\t\t},\n\t];\n\n\treturn (\n\t\t !open && onClose()}>\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{isBatchMode\n\t\t\t\t\t\t\t? `${pendingRanges?.length}件の予定を作成`\n\t\t\t\t\t\t\t: event?.id\n\t\t\t\t\t\t\t\t? \"予定を編集\"\n\t\t\t\t\t\t\t\t: \"予定を作成\"}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{isBatchMode\n\t\t\t\t\t\t\t? \"選択した時間枠に同じ内容の予定を作成します\"\n\t\t\t\t\t\t\t: event?.id\n\t\t\t\t\t\t\t\t? \"予定の詳細を編集します\"\n\t\t\t\t\t\t\t\t: \"新しい予定をカレンダーに追加します\"}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t{error && (\n\t\t\t\t\t
\n\t\t\t\t\t\t{error}\n\t\t\t\t\t
\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t setTitle(e.target.value)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t setDescription(e.target.value)}\n\t\t\t\t\t\t\trows={3}\n\t\t\t\t\t\t/>\n\t\t\t\t\t
\n\n\t\t\t\t\t{isBatchMode ? (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t{sortedPendingRanges.map((range) => (\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{dayjs(range.start).format(\"M月D日(ddd)\")}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t{dayjs(range.start).format(\"HH:mm\")} –{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t{dayjs(range.end).format(\"HH:mm\")}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{startDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? dayjs(startDate).format(\"YYYY年M月D日\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"日付を選択\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (date) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetStartDate(date);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (dayjs(endDate).isBefore(date)) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetEndDate(date);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetError(null);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetStartDateOpen(false);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t\t{!allDay && (\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{endDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? dayjs(endDate).format(\"YYYY年M月D日\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"日付を選択\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (date) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetEndDate(date);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetError(null);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetEndDateOpen(false);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t\t{!allDay && (\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t setAllDay(checked === true)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t setLocation(e.target.value)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t色\n\t\t\t\t\t\t\n\t\t\t\t\t\t setColor(value)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{colorOptions.map((colorOption) => (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{event?.id && (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/event-item.tsx", "content": "\"use client\";\n\nimport type { DraggableAttributes } from \"@dnd-kit/core\";\nimport type { SyntheticListenerMap } from \"@dnd-kit/core/dist/hooks/utilities\";\nimport { useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport dayjs from \"../dayjs\";\nimport {\n\ttype CalendarEvent,\n\tgetBorderRadiusClasses,\n\tgetEventColorClasses,\n} from \".\";\n\nconst formatTimeWithOptionalMinutes = (date: Date) => {\n\tconst d = dayjs(date);\n\treturn d.minute() === 0\n\t\t? d.format(\"HH:00\").toLowerCase()\n\t\t: d.format(\"HH:mm\").toLowerCase();\n};\n\ninterface EventWrapperProps {\n\tevent: CalendarEvent;\n\tisFirstDay?: boolean;\n\tisLastDay?: boolean;\n\tisDragging?: boolean;\n\tonClick?: (e: React.MouseEvent) => void;\n\tclassName?: string;\n\tchildren: React.ReactNode;\n\tcurrentTime?: Date;\n\tdndListeners?: SyntheticListenerMap;\n\tdndAttributes?: DraggableAttributes;\n\tonMouseDown?: (e: React.MouseEvent) => void;\n\tonTouchStart?: (e: React.TouchEvent) => void;\n}\n\n// Shared wrapper component for event styling\nfunction EventWrapper({\n\tevent,\n\tisFirstDay = true,\n\tisLastDay = true,\n\tisDragging,\n\tonClick,\n\tclassName,\n\tchildren,\n\tcurrentTime,\n\tdndListeners,\n\tdndAttributes,\n\tonMouseDown,\n\tonTouchStart,\n}: EventWrapperProps) {\n\t// Always use the currentTime (if provided) to determine if the event is in the past\n\tconst displayEnd = currentTime\n\t\t? new Date(\n\t\t\t\tnew Date(currentTime).getTime() +\n\t\t\t\t\t(new Date(event.end).getTime() - new Date(event.start).getTime()),\n\t\t\t)\n\t\t: new Date(event.end);\n\n\tconst isEventInPast = dayjs(displayEnd).isBefore(dayjs());\n\n\treturn (\n\t\t\n\t\t\t{children}\n\t\t\n\t);\n}\n\ninterface EventItemProps {\n\tevent: CalendarEvent;\n\tview: \"month\" | \"week\" | \"day\" | \"agenda\";\n\tisDragging?: boolean;\n\tonClick?: (e: React.MouseEvent) => void;\n\tshowTime?: boolean;\n\tcurrentTime?: Date; // For updating time during drag\n\tisFirstDay?: boolean;\n\tisLastDay?: boolean;\n\tchildren?: React.ReactNode;\n\tclassName?: string;\n\tdndListeners?: SyntheticListenerMap;\n\tdndAttributes?: DraggableAttributes;\n\tonMouseDown?: (e: React.MouseEvent) => void;\n\tonTouchStart?: (e: React.TouchEvent) => void;\n}\n\nexport function EventItem({\n\tevent,\n\tview,\n\tisDragging,\n\tonClick,\n\tshowTime,\n\tcurrentTime,\n\tisFirstDay = true,\n\tisLastDay = true,\n\tchildren,\n\tclassName,\n\tdndListeners,\n\tdndAttributes,\n\tonMouseDown,\n\tonTouchStart,\n}: EventItemProps) {\n\tconst eventColor = event.color;\n\n\t// Use the provided currentTime (for dragging) or the event's actual time\n\tconst displayStart = useMemo(() => {\n\t\treturn currentTime || new Date(event.start);\n\t}, [currentTime, event.start]);\n\n\tconst displayEnd = useMemo(() => {\n\t\treturn currentTime\n\t\t\t? new Date(\n\t\t\t\t\tnew Date(currentTime).getTime() +\n\t\t\t\t\t\t(new Date(event.end).getTime() - new Date(event.start).getTime()),\n\t\t\t\t)\n\t\t\t: new Date(event.end);\n\t}, [currentTime, event.start, event.end]);\n\n\t// Calculate event duration in minutes\n\tconst durationMinutes = useMemo(() => {\n\t\treturn dayjs(displayEnd).diff(dayjs(displayStart), \"minute\");\n\t}, [displayStart, displayEnd]);\n\n\tconst getEventTime = () => {\n\t\tif (event.allDay) return \"All day\";\n\n\t\t// For short events (less than 45 minutes), only show start time\n\t\tif (durationMinutes < 45) {\n\t\t\treturn formatTimeWithOptionalMinutes(displayStart);\n\t\t}\n\n\t\t// For longer events, show both start and end time\n\t\treturn `${formatTimeWithOptionalMinutes(displayStart)} - ${formatTimeWithOptionalMinutes(displayEnd)}`;\n\t};\n\n\tif (view === \"month\") {\n\t\treturn (\n\t\t\t\n\t\t\t\t{children || (\n\t\t\t\t\t\n\t\t\t\t\t\t{!event.allDay && (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{formatTimeWithOptionalMinutes(displayStart)}{\" \"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{event.title}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t);\n\t}\n\n\tif (view === \"week\" || view === \"day\") {\n\t\treturn (\n\t\t\t\n\t\t\t\t{durationMinutes < 45 ? (\n\t\t\t\t\t
\n\t\t\t\t\t\t{event.title}{\" \"}\n\t\t\t\t\t\t{showTime && (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{formatTimeWithOptionalMinutes(displayStart)}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t
{event.title}
\n\t\t\t\t\t\t{showTime && (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{getEventTime()}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t);\n\t}\n\n\t// Agenda view - kept separate since it's significantly different\n\treturn (\n\t\t\n\t\t\t
{event.title}
\n\t\t\t
\n\t\t\t\t{event.allDay ? (\n\t\t\t\t\t終日\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t\t\t{formatTimeWithOptionalMinutes(displayStart)} -{\" \"}\n\t\t\t\t\t\t{formatTimeWithOptionalMinutes(displayEnd)}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{event.location && (\n\t\t\t\t\t<>\n\t\t\t\t\t\t · \n\t\t\t\t\t\t{event.location}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t\t{event.description && (\n\t\t\t\t
{event.description}
\n\t\t\t)}\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/events-popup.tsx", "content": "\"use client\";\n\nimport { XIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport dayjs from \"../dayjs\";\n\nimport { type CalendarEvent, EventItem } from \".\";\n\ninterface EventsPopupProps {\n\tdate: Date;\n\tevents: CalendarEvent[];\n\tposition: { top: number; left: number };\n\tonClose: () => void;\n\tonEventSelect: (event: CalendarEvent) => void;\n}\n\nexport function EventsPopup({\n\tdate,\n\tevents,\n\tposition,\n\tonClose,\n\tonEventSelect,\n}: EventsPopupProps) {\n\tconst popupRef = useRef(null);\n\n\t// Handle click outside to close popup\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (\n\t\t\t\tpopupRef.current &&\n\t\t\t\t!popupRef.current.contains(event.target as Node)\n\t\t\t) {\n\t\t\t\tonClose();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\treturn () => {\n\t\t\tdocument.removeEventListener(\"mousedown\", handleClickOutside);\n\t\t};\n\t}, [onClose]);\n\n\t// Handle escape key to close popup\n\tuseEffect(() => {\n\t\tconst handleEscKey = (event: KeyboardEvent) => {\n\t\t\tif (event.key === \"Escape\") {\n\t\t\t\tonClose();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"keydown\", handleEscKey);\n\t\treturn () => {\n\t\t\tdocument.removeEventListener(\"keydown\", handleEscKey);\n\t\t};\n\t}, [onClose]);\n\n\tconst handleEventClick = (event: CalendarEvent) => {\n\t\tonEventSelect(event);\n\t\tonClose();\n\t};\n\n\t// Adjust position to ensure popup stays within viewport\n\tconst adjustedPosition = useMemo(() => {\n\t\tconst positionCopy = { ...position };\n\n\t\t// Check if we need to adjust the position to fit in the viewport\n\t\tif (popupRef.current) {\n\t\t\tconst rect = popupRef.current.getBoundingClientRect();\n\t\t\tconst viewportWidth = window.innerWidth;\n\t\t\tconst viewportHeight = window.innerHeight;\n\n\t\t\t// Adjust horizontally if needed\n\t\t\tif (positionCopy.left + rect.width > viewportWidth) {\n\t\t\t\tpositionCopy.left = Math.max(0, viewportWidth - rect.width);\n\t\t\t}\n\n\t\t\t// Adjust vertically if needed\n\t\t\tif (positionCopy.top + rect.height > viewportHeight) {\n\t\t\t\tpositionCopy.top = Math.max(0, viewportHeight - rect.height);\n\t\t\t}\n\t\t}\n\n\t\treturn positionCopy;\n\t}, [position]);\n\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t

{dayjs(date).format(\"YYYY年M月D日\")}

\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t{events.length === 0 ? (\n\t\t\t\t\t
\n\t\t\t\t\t\t予定がありません\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\tevents.map((event) => {\n\t\t\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\t\t\tconst isFirstDay = dayjs(date).isSame(eventStart, \"day\");\n\t\t\t\t\t\tconst isLastDay = dayjs(date).isSame(eventEnd, \"day\");\n\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t// biome-ignore lint/a11y/noStaticElementInteractions: 外部依存関係があるため\n\t\t\t\t\t\t\t// biome-ignore lint/a11y/useKeyWithClickEvents: 外部依存関係があるため\n\t\t\t\t\t\t\t handleEventClick(event)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t);\n\t\t\t\t\t})\n\t\t\t\t)}\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/holiday-label.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { CalendarHoliday } from \"../types\";\n\ninterface HolidayLabelProps {\n\tholiday: CalendarHoliday;\n\tvariant?: \"inline\" | \"chip\";\n\tclassName?: string;\n}\n\nexport function HolidayLabel({\n\tholiday,\n\tvariant = \"inline\",\n\tclassName,\n}: HolidayLabelProps) {\n\treturn (\n\t\t\n\t\t\t{holiday.name}\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/month-view.tsx", "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/registry/br-hr/ui/popover\";\nimport { DefaultStartHour } from \"../constants\";\nimport { eachDayOfInterval } from \"../date-utils\";\nimport dayjs from \"../dayjs\";\nimport {\n\ttype CalendarEvent,\n\ttype CalendarHoliday,\n\tDraggableEvent,\n\tDroppableCell,\n\tEventGap,\n\tEventHeight,\n\tEventItem,\n\tgetAllEventsForDay,\n\tgetDayCellBackgroundClasses,\n\tgetEventsForDay,\n\tgetHolidaysForDay,\n\tgetSpanningEventsForDay,\n\tHolidayLabel,\n\tsortEvents,\n\tuseEventVisibility,\n} from \".\";\n\nfunction getWeekdayHeaderClasses(index: number) {\n\treturn cn(\n\t\tindex === 0 && \"text-red-500\",\n\t\tindex === 6 && \"text-blue-500\",\n\t\tindex !== 0 && index !== 6 && \"text-muted-foreground/70\",\n\t);\n}\n\ninterface MonthViewProps {\n\tcurrentDate: Date;\n\tevents: CalendarEvent[];\n\tholidays?: CalendarHoliday[];\n\tonEventSelect: (event: CalendarEvent) => void;\n\tonEventCreate: (startTime: Date) => void;\n}\n\nexport function MonthView({\n\tcurrentDate,\n\tevents,\n\tholidays = [],\n\tonEventSelect,\n\tonEventCreate,\n}: MonthViewProps) {\n\tconst days = useMemo(() => {\n\t\tconst monthStart = dayjs(currentDate).startOf(\"month\");\n\t\tconst monthEnd = dayjs(currentDate).endOf(\"month\");\n\t\tconst calendarStart = monthStart.startOf(\"week\");\n\t\tconst calendarEnd = monthEnd.endOf(\"week\");\n\n\t\treturn eachDayOfInterval(calendarStart.toDate(), calendarEnd.toDate());\n\t}, [currentDate]);\n\n\tconst weekdays = useMemo(() => {\n\t\t// Fixed Sunday reference — weekday labels only, no runtime date needed\n\t\tconst sunday = dayjs(\"2024-01-07\");\n\t\treturn Array.from({ length: 7 }).map((_, i) =>\n\t\t\tsunday.add(i, \"day\").format(\"ddd\"),\n\t\t);\n\t}, []);\n\n\tconst weeks = useMemo(() => {\n\t\tconst result = [];\n\t\tlet week = [];\n\n\t\tfor (let i = 0; i < days.length; i++) {\n\t\t\tweek.push(days[i]);\n\t\t\tif (week.length === 7 || i === days.length - 1) {\n\t\t\t\tresult.push(week);\n\t\t\t\tweek = [];\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}, [days]);\n\n\tconst handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n\t\te.stopPropagation();\n\t\tonEventSelect(event);\n\t};\n\n\tconst [isMounted, setIsMounted] = useState(false);\n\tconst { contentRef, getVisibleEventCount } = useEventVisibility({\n\t\teventHeight: EventHeight,\n\t\teventGap: EventGap,\n\t});\n\n\tuseEffect(() => {\n\t\tsetIsMounted(true);\n\t}, []);\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t{weekdays.map((day, index) => (\n\t\t\t\t\t\n\t\t\t\t\t\t{day}\n\t\t\t\t\t
\n\t\t\t\t))}\n\t\t\t
\n\t\t\t
\n\t\t\t\t{weeks.map((week, weekIndex) => (\n\t\t\t\t\t*]:border-b-0\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{week.map((day, dayIndex) => {\n\t\t\t\t\t\t\tif (!day) return null; // Skip if day is undefined\n\n\t\t\t\t\t\t\tconst dayEvents = getEventsForDay(events, day);\n\t\t\t\t\t\t\tconst dayHolidays = getHolidaysForDay(holidays, day);\n\t\t\t\t\t\t\tconst spanningEvents = getSpanningEventsForDay(events, day);\n\t\t\t\t\t\t\tconst isCurrentMonth = dayjs(day).isSame(currentDate, \"month\");\n\t\t\t\t\t\t\tconst cellId = `month-cell-${day.toISOString()}`;\n\t\t\t\t\t\t\tconst allDayEvents = [...spanningEvents, ...dayEvents];\n\t\t\t\t\t\t\tconst allEvents = getAllEventsForDay(events, day);\n\n\t\t\t\t\t\t\tconst isReferenceCell = weekIndex === 0 && dayIndex === 0;\n\t\t\t\t\t\t\tconst visibleCount = isMounted\n\t\t\t\t\t\t\t\t? getVisibleEventCount(allDayEvents.length)\n\t\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\t\t\tconst hasMore =\n\t\t\t\t\t\t\t\tvisibleCount !== undefined &&\n\t\t\t\t\t\t\t\tallDayEvents.length > visibleCount;\n\t\t\t\t\t\t\tconst remainingCount = hasMore\n\t\t\t\t\t\t\t\t? allDayEvents.length - visibleCount\n\t\t\t\t\t\t\t\t: 0;\n\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\tconst startTime = new Date(day);\n\t\t\t\t\t\t\t\t\t\t\tstartTime.setHours(DefaultStartHour, 0, 0);\n\t\t\t\t\t\t\t\t\t\t\tonEventCreate(startTime);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t{dayjs(day).format(\"D\")}\n\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t{dayHolidays.length > 0 && (\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{sortEvents(allDayEvents).map((event, index) => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\t\t\t\t\t\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\t\t\t\t\t\t\t\t\tconst isFirstDay = dayjs(day).isSame(eventStart, \"day\");\n\t\t\t\t\t\t\t\t\t\t\t\tconst isLastDay = dayjs(day).isSame(eventEnd, \"day\");\n\n\t\t\t\t\t\t\t\t\t\t\t\tconst isHidden =\n\t\t\t\t\t\t\t\t\t\t\t\t\tisMounted && visibleCount && index >= visibleCount;\n\n\t\t\t\t\t\t\t\t\t\t\t\tif (!visibleCount) return null;\n\n\t\t\t\t\t\t\t\t\t\t\t\tif (!isFirstDay) {\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t handleEventClick(event, e)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tevent={event}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tview=\"month\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisFirstDay={isFirstDay}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisLastDay={isLastDay}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{!event.allDay && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{dayjs(event.start).format(\"H:mm\")}{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{event.title}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t handleEventClick(event, e)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisFirstDay={isFirstDay}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisLastDay={isLastDay}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t})}\n\n\t\t\t\t\t\t\t\t\t\t\t{hasMore && (\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t e.stopPropagation()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t他{remainingCount}件\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{dayjs(day).format(\"M月D日(ddd)\")}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{sortEvents(allEvents).map((event) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst isFirstDay = dayjs(day).isSame(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\teventStart,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"day\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst isLastDay = dayjs(day).isSame(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\teventEnd,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"day\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thandleEventClick(event, e)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tevent={event}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tview=\"month\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisFirstDay={isFirstDay}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisLastDay={isLastDay}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/time-grid-column.tsx", "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { EndHour, StartHour } from \"../constants\";\nimport { eachHourOfInterval } from \"../date-utils\";\nimport dayjs from \"../dayjs\";\nimport {\n\ttype PendingSelectionDisplay,\n\ttype TimeRangeSelectionMode,\n\tuseTimeRangeSelection,\n} from \"../hooks/use-time-range-selection\";\nimport { DroppableCell } from \"./droppable-cell\";\n\ninterface TimeGridColumnProps extends React.ComponentPropsWithoutRef<\"div\"> {\n\tdate: Date;\n\tdroppableIdPrefix: string;\n\tonEventCreate: (startTime: Date, endTime?: Date) => void;\n\tselectionMode?: TimeRangeSelectionMode;\n\tonRangeAdd?: (range: { start: Date; end: Date }) => void;\n\tpendingSelections?: PendingSelectionDisplay[];\n\tchildren?: React.ReactNode;\n}\n\nexport function TimeGridColumn({\n\tdate,\n\tdroppableIdPrefix,\n\tonEventCreate,\n\tselectionMode = \"single\",\n\tonRangeAdd,\n\tpendingSelections = [],\n\tchildren,\n\tclassName,\n\t...props\n}: TimeGridColumnProps) {\n\tconst hours = useMemo(() => {\n\t\tconst dayStart = dayjs(date).startOf(\"day\");\n\t\treturn eachHourOfInterval(\n\t\t\tdayStart.add(StartHour, \"hour\").toDate(),\n\t\t\tdayStart.add(EndHour - 1, \"hour\").toDate(),\n\t\t);\n\t}, [date]);\n\n\tconst { columnRef, activeSelection, handlers } = useTimeRangeSelection(date, {\n\t\tmode: selectionMode,\n\t\tonEventCreate,\n\t\tonRangeAdd,\n\t\tpendingSelections,\n\t});\n\n\treturn (\n\t\t\n\t\t\t{children}\n\n\t\t\t{pendingSelections.map((selection) => (\n\t\t\t\t\n\t\t\t))}\n\n\t\t\t{activeSelection && (\n\t\t\t\t\n\t\t\t)}\n\n\t\t\t{hours.map((hour) => {\n\t\t\t\tconst hourValue = dayjs(hour).hour();\n\t\t\t\treturn (\n\t\t\t\t\t\n\t\t\t\t\t\t{[0, 1, 2, 3].map((quarter) => {\n\t\t\t\t\t\t\tconst quarterHourTime = hourValue + quarter * 0.25;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t\n\t\t\t\t);\n\t\t\t})}\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/components/week-view.tsx", "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { EndHour, StartHour } from \"../constants\";\nimport {\n\tareIntervalsOverlapping,\n\teachDayOfInterval,\n\teachHourOfInterval,\n} from \"../date-utils\";\nimport dayjs from \"../dayjs\";\nimport { timeRangeToSelectionStyle } from \"../hooks/use-time-range-selection\";\nimport {\n\ttype CalendarEvent,\n\ttype CalendarHoliday,\n\tDraggableEvent,\n\tEventItem,\n\tgetDayCellBackgroundClasses,\n\tgetDayHeaderTextClasses,\n\tgetHolidaysForDay,\n\tHolidayLabel,\n\tisMultiDayEvent,\n\ttype PendingTimeRange,\n\tTimeGridColumn,\n\tuseCurrentTimeIndicator,\n\tWeekCellsHeight,\n} from \".\";\n\ninterface WeekViewProps {\n\tcurrentDate: Date;\n\tevents: CalendarEvent[];\n\tholidays?: CalendarHoliday[];\n\tonEventSelect: (event: CalendarEvent) => void;\n\tonEventCreate: (startTime: Date, endTime?: Date) => void;\n\tmultiSelectMode?: boolean;\n\tpendingRanges?: PendingTimeRange[];\n\tonRangeAdd?: (range: PendingTimeRange) => void;\n}\n\ninterface PositionedEvent {\n\tevent: CalendarEvent;\n\ttop: number;\n\theight: number;\n\tleft: number;\n\twidth: number;\n\tzIndex: number;\n}\n\nexport function WeekView({\n\tcurrentDate,\n\tevents,\n\tholidays = [],\n\tonEventSelect,\n\tonEventCreate,\n\tmultiSelectMode = false,\n\tpendingRanges = [],\n\tonRangeAdd,\n}: WeekViewProps) {\n\tconst days = useMemo(() => {\n\t\tconst weekStart = dayjs(currentDate).startOf(\"week\");\n\t\tconst weekEnd = dayjs(currentDate).endOf(\"week\");\n\t\treturn eachDayOfInterval(weekStart.toDate(), weekEnd.toDate());\n\t}, [currentDate]);\n\n\tconst weekStart = useMemo(\n\t\t() => dayjs(currentDate).startOf(\"week\").toDate(),\n\t\t[currentDate],\n\t);\n\n\tconst hours = useMemo(() => {\n\t\tconst dayStart = dayjs(currentDate).startOf(\"day\");\n\t\treturn eachHourOfInterval(\n\t\t\tdayStart.add(StartHour, \"hour\").toDate(),\n\t\t\tdayStart.add(EndHour - 1, \"hour\").toDate(),\n\t\t);\n\t}, [currentDate]);\n\n\t// Get all-day events and multi-day events for the week\n\tconst allDayEvents = useMemo(() => {\n\t\treturn events\n\t\t\t.filter((event) => {\n\t\t\t\t// Include explicitly marked all-day events or multi-day events\n\t\t\t\treturn event.allDay || isMultiDayEvent(event);\n\t\t\t})\n\t\t\t.filter((event) => {\n\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\treturn days.some((day) => {\n\t\t\t\t\tconst dayTime = dayjs(day);\n\t\t\t\t\treturn (\n\t\t\t\t\t\tdayTime.isSame(eventStart, \"day\") ||\n\t\t\t\t\t\tdayTime.isSame(eventEnd, \"day\") ||\n\t\t\t\t\t\t(dayTime.isAfter(eventStart, \"day\") &&\n\t\t\t\t\t\t\tdayTime.isBefore(eventEnd, \"day\"))\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t});\n\t}, [events, days]);\n\n\t// Process events for each day to calculate positions\n\tconst processedDayEvents = useMemo(() => {\n\t\tconst result = days.map((day) => {\n\t\t\t// Get events for this day that are not all-day events or multi-day events\n\t\t\tconst dayEvents = events.filter((event) => {\n\t\t\t\t// Skip all-day events and multi-day events\n\t\t\t\tif (event.allDay || isMultiDayEvent(event)) return false;\n\n\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\tconst eventEnd = new Date(event.end);\n\n\t\t\t\t// Check if event is on this day\n\t\t\t\tconst dayTime = dayjs(day);\n\t\t\t\treturn (\n\t\t\t\t\tdayTime.isSame(eventStart, \"day\") ||\n\t\t\t\t\tdayTime.isSame(eventEnd, \"day\") ||\n\t\t\t\t\t(dayTime.isAfter(eventStart, \"day\") &&\n\t\t\t\t\t\tdayTime.isBefore(eventEnd, \"day\"))\n\t\t\t\t);\n\t\t\t});\n\n\t\t\t// Sort events by start time and duration\n\t\t\tconst sortedEvents = [...dayEvents].sort((a, b) => {\n\t\t\t\tconst aStart = new Date(a.start);\n\t\t\t\tconst bStart = new Date(b.start);\n\t\t\t\tconst aEnd = new Date(a.end);\n\t\t\t\tconst bEnd = new Date(b.end);\n\n\t\t\t\t// First sort by start time\n\t\t\t\tif (aStart < bStart) return -1;\n\t\t\t\tif (aStart > bStart) return 1;\n\n\t\t\t\t// If start times are equal, sort by duration (longer events first)\n\t\t\t\tconst aDuration = dayjs(aEnd).diff(dayjs(aStart), \"minute\");\n\t\t\t\tconst bDuration = dayjs(bEnd).diff(dayjs(bStart), \"minute\");\n\t\t\t\treturn bDuration - aDuration;\n\t\t\t});\n\n\t\t\t// Calculate positions for each event\n\t\t\tconst positionedEvents: PositionedEvent[] = [];\n\t\t\tconst dayStart = dayjs(day).startOf(\"day\").toDate();\n\n\t\t\t// Track columns for overlapping events\n\t\t\tconst columns: { event: CalendarEvent; end: Date }[][] = [];\n\n\t\t\tsortedEvents.forEach((event) => {\n\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\tconst eventEnd = new Date(event.end);\n\n\t\t\t\t// Adjust start and end times if they're outside this day\n\t\t\t\tconst adjustedStart = dayjs(day).isSame(eventStart, \"day\")\n\t\t\t\t\t? eventStart\n\t\t\t\t\t: dayStart;\n\t\t\t\tconst adjustedEnd = dayjs(day).isSame(eventEnd, \"day\")\n\t\t\t\t\t? eventEnd\n\t\t\t\t\t: dayjs(dayStart).add(24, \"hour\").toDate();\n\n\t\t\t\t// Calculate top position and height\n\t\t\t\tconst adjustedStartTime = dayjs(adjustedStart);\n\t\t\t\tconst adjustedEndTime = dayjs(adjustedEnd);\n\t\t\t\tconst startHour =\n\t\t\t\t\tadjustedStartTime.hour() + adjustedStartTime.minute() / 60;\n\t\t\t\tconst endHour = adjustedEndTime.hour() + adjustedEndTime.minute() / 60;\n\n\t\t\t\t// Adjust the top calculation to account for the new start time\n\t\t\t\tconst top = (startHour - StartHour) * WeekCellsHeight;\n\t\t\t\tconst height = (endHour - startHour) * WeekCellsHeight;\n\n\t\t\t\t// Find a column for this event\n\t\t\t\tlet columnIndex = 0;\n\t\t\t\tlet placed = false;\n\n\t\t\t\twhile (!placed) {\n\t\t\t\t\tconst col = columns[columnIndex] || [];\n\t\t\t\t\tif (col.length === 0) {\n\t\t\t\t\t\tcolumns[columnIndex] = col;\n\t\t\t\t\t\tplaced = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst overlaps = col.some((c) =>\n\t\t\t\t\t\t\tareIntervalsOverlapping(\n\t\t\t\t\t\t\t\t{ start: adjustedStart, end: adjustedEnd },\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tstart: new Date(c.event.start),\n\t\t\t\t\t\t\t\t\tend: new Date(c.event.end),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!overlaps) {\n\t\t\t\t\t\t\tplaced = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcolumnIndex++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Ensure column is initialized before pushing\n\t\t\t\tconst currentColumn = columns[columnIndex] || [];\n\t\t\t\tcolumns[columnIndex] = currentColumn;\n\t\t\t\tcurrentColumn.push({ event, end: adjustedEnd });\n\n\t\t\t\t// Calculate width and left position based on number of columns\n\t\t\t\tconst width = columnIndex === 0 ? 1 : 0.9;\n\t\t\t\tconst left = columnIndex === 0 ? 0 : columnIndex * 0.1;\n\n\t\t\t\tpositionedEvents.push({\n\t\t\t\t\tevent,\n\t\t\t\t\ttop,\n\t\t\t\t\theight,\n\t\t\t\t\tleft,\n\t\t\t\t\twidth,\n\t\t\t\t\tzIndex: 10 + columnIndex, // Higher columns get higher z-index\n\t\t\t\t});\n\t\t\t});\n\n\t\t\treturn positionedEvents;\n\t\t});\n\n\t\treturn result;\n\t}, [days, events]);\n\n\tconst handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n\t\te.stopPropagation();\n\t\tonEventSelect(event);\n\t};\n\n\tconst hasHolidays = holidays.length > 0;\n\tconst showAllDaySection = allDayEvents.length > 0 || hasHolidays;\n\tconst { currentTimePosition, currentTimeVisible } = useCurrentTimeIndicator(\n\t\tcurrentDate,\n\t\t\"week\",\n\t);\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t{days.map((day) => {\n\t\t\t\t\tconst dayHolidays = getHolidaysForDay(holidays, day);\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t{dayjs(day).format(\"ddd\")}\n\t\t\t\t\t\t\t\t{dayjs(day).format(\"D\")}\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{dayjs(day).format(\"D(ddd)\")}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{dayHolidays.length > 0 && (\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{dayHolidays.map((holiday) => (\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t
\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t
\n\n\t\t\t{showAllDaySection && (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t終日\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t{days.map((day, dayIndex) => {\n\t\t\t\t\t\t\tconst dayHolidays = getHolidaysForDay(holidays, day);\n\t\t\t\t\t\t\tconst dayAllDayEvents = allDayEvents.filter((event) => {\n\t\t\t\t\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\t\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\t\t\t\t\tconst dayTime = dayjs(day);\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\tdayTime.isSame(eventStart, \"day\") ||\n\t\t\t\t\t\t\t\t\t(dayTime.isAfter(eventStart, \"day\") &&\n\t\t\t\t\t\t\t\t\t\tdayTime.isBefore(eventEnd, \"day\")) ||\n\t\t\t\t\t\t\t\t\tdayTime.isSame(eventEnd, \"day\")\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{dayHolidays.map((holiday) => (\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t{dayAllDayEvents.map((event) => {\n\t\t\t\t\t\t\t\t\t\tconst eventStart = new Date(event.start);\n\t\t\t\t\t\t\t\t\t\tconst eventEnd = new Date(event.end);\n\t\t\t\t\t\t\t\t\t\tconst isFirstDay = dayjs(day).isSame(eventStart, \"day\");\n\t\t\t\t\t\t\t\t\t\tconst isLastDay = dayjs(day).isSame(eventEnd, \"day\");\n\n\t\t\t\t\t\t\t\t\t\t// Check if this is the first day in the current week view\n\t\t\t\t\t\t\t\t\t\tconst isFirstVisibleDay =\n\t\t\t\t\t\t\t\t\t\t\tdayIndex === 0 &&\n\t\t\t\t\t\t\t\t\t\t\tdayjs(eventStart).isBefore(weekStart, \"day\");\n\t\t\t\t\t\t\t\t\t\tconst shouldShowTitle = isFirstDay || isFirstVisibleDay;\n\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t handleEventClick(event, e)}\n\t\t\t\t\t\t\t\t\t\t\t\tevent={event}\n\t\t\t\t\t\t\t\t\t\t\t\tview=\"month\"\n\t\t\t\t\t\t\t\t\t\t\t\tisFirstDay={isFirstDay}\n\t\t\t\t\t\t\t\t\t\t\t\tisLastDay={isLastDay}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{/* Show title if it's the first day of the event or the first visible day in the week */}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{event.title}\n\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t)}\n\n\t\t\t 0 && \"max-h-[calc(100vh-350px)]\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t
\n\t\t\t\t\t{hours.map((hour, index) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{index > 0 && (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{`${dayjs(hour).format(\"HH\")}:00`}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t
\n\t\t\t\t\t))}\n\t\t\t\t\n\n\t\t\t\t{days.map((day, dayIndex) => (\n\t\t\t\t\t\n\t\t\t\t\t\t\tonRangeAdd?.({\n\t\t\t\t\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\t\t\t\t\tdate: day,\n\t\t\t\t\t\t\t\tstart: range.start,\n\t\t\t\t\t\t\t\tend: range.end,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpendingSelections={pendingRanges\n\t\t\t\t\t\t\t.filter((range) => dayjs(range.date).isSame(day, \"day\"))\n\t\t\t\t\t\t\t.map((range) => ({\n\t\t\t\t\t\t\t\tid: range.id,\n\t\t\t\t\t\t\t\t...timeRangeToSelectionStyle(range.start, range.end),\n\t\t\t\t\t\t\t}))}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"border-border/70 grid auto-cols-fr border-r last:border-r-0\",\n\t\t\t\t\t\t\tgetDayCellBackgroundClasses(day, holidays),\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tdata-today={dayjs(day).isToday() || undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{(processedDayEvents[dayIndex] ?? []).map((positionedEvent) => (\n\t\t\t\t\t\t\t// biome-ignore lint/a11y/noStaticElementInteractions: イベントクリックの伝播を止めるため\n\t\t\t\t\t\t\t e.stopPropagation()}\n\t\t\t\t\t\t\t\tonKeyDown={(e) => e.stopPropagation()}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t handleEventClick(positionedEvent.event, e)}\n\t\t\t\t\t\t\t\t\t\tshowTime\n\t\t\t\t\t\t\t\t\t\theight={positionedEvent.height}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t))}\n\n\t\t\t\t\t\t{currentTimeVisible && dayjs(day).isToday() && (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component" }, { "path": "registry/br-hr/blocks/event-calendar/hooks/use-current-time-indicator.ts", "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { EndHour, StartHour } from \"../constants\";\nimport dayjs from \"../dayjs\";\n\nexport function useCurrentTimeIndicator(\n\tcurrentDate: Date,\n\tview: \"day\" | \"week\",\n) {\n\tconst [currentTimePosition, setCurrentTimePosition] = useState(0);\n\tconst [currentTimeVisible, setCurrentTimeVisible] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst calculateTimePosition = () => {\n\t\t\tconst now = new Date();\n\t\t\tconst hours = now.getHours();\n\t\t\tconst minutes = now.getMinutes();\n\t\t\tconst totalMinutes = (hours - StartHour) * 60 + minutes;\n\t\t\tconst dayStartMinutes = 0; // 12am\n\t\t\tconst dayEndMinutes = (EndHour - StartHour) * 60; // 12am next day\n\n\t\t\t// Calculate position as percentage of day\n\t\t\tconst position =\n\t\t\t\t((totalMinutes - dayStartMinutes) / (dayEndMinutes - dayStartMinutes)) *\n\t\t\t\t100;\n\n\t\t\t// Check if current day is in view based on the calendar view\n\t\t\tlet isCurrentTimeVisible = false;\n\n\t\t\tif (view === \"day\") {\n\t\t\t\tisCurrentTimeVisible = dayjs(now).isSame(currentDate, \"day\");\n\t\t\t} else if (view === \"week\") {\n\t\t\t\tconst startOfWeekDate = dayjs(currentDate).startOf(\"week\");\n\t\t\t\tconst endOfWeekDate = dayjs(currentDate).endOf(\"week\");\n\t\t\t\tisCurrentTimeVisible = dayjs(now).isBetween(\n\t\t\t\t\tstartOfWeekDate,\n\t\t\t\t\tendOfWeekDate,\n\t\t\t\t\tnull,\n\t\t\t\t\t\"[]\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tsetCurrentTimePosition(position);\n\t\t\tsetCurrentTimeVisible(isCurrentTimeVisible);\n\t\t};\n\n\t\t// Calculate immediately\n\t\tcalculateTimePosition();\n\n\t\t// Update every minute\n\t\tconst interval = setInterval(calculateTimePosition, 60000);\n\n\t\treturn () => clearInterval(interval);\n\t}, [currentDate, view]);\n\n\treturn { currentTimePosition, currentTimeVisible };\n}\n", "type": "registry:hook" }, { "path": "registry/br-hr/blocks/event-calendar/hooks/use-event-visibility.ts", "content": "\"use client\";\n\nimport { useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\ninterface EventVisibilityOptions {\n\teventHeight: number;\n\teventGap: number;\n}\n\ninterface EventVisibilityResult {\n\tcontentRef: React.RefObject;\n\tcontentHeight: number | null;\n\tgetVisibleEventCount: (totalEvents: number) => number;\n}\n\n/**\n * Hook for calculating event visibility based on container height\n * Uses ResizeObserver for efficient updates\n */\nexport function useEventVisibility({\n\teventHeight,\n\teventGap,\n}: EventVisibilityOptions): EventVisibilityResult {\n\t// Use the standard pattern for React refs\n\tconst contentRef = useRef(null);\n\tconst observerRef = useRef(null);\n\tconst [contentHeight, setContentHeight] = useState(null);\n\n\t// Use layout effect for synchronous measurement before paint\n\tuseLayoutEffect(() => {\n\t\tif (!contentRef.current) return;\n\n\t\t// Function to update the content height\n\t\tconst updateHeight = () => {\n\t\t\tif (contentRef.current) {\n\t\t\t\tsetContentHeight(contentRef.current.clientHeight);\n\t\t\t}\n\t\t};\n\n\t\t// Initial measurement (synchronous)\n\t\tupdateHeight();\n\n\t\t// Create observer only once and reuse it\n\t\tif (!observerRef.current) {\n\t\t\tobserverRef.current = new ResizeObserver(() => {\n\t\t\t\t// Just call updateHeight when resize is detected\n\t\t\t\tupdateHeight();\n\t\t\t});\n\t\t}\n\n\t\t// Start observing the content container\n\t\tobserverRef.current.observe(contentRef.current);\n\n\t\t// Clean up function\n\t\treturn () => {\n\t\t\tif (observerRef.current) {\n\t\t\t\tobserverRef.current.disconnect();\n\t\t\t}\n\t\t};\n\t}, []);\n\n\t// Function to calculate visible events for a cell\n\tconst getVisibleEventCount = useMemo(() => {\n\t\treturn (totalEvents: number): number => {\n\t\t\tif (!contentHeight) return totalEvents;\n\n\t\t\t// Calculate how many events can fit in the container\n\t\t\tconst maxEvents = Math.floor(contentHeight / (eventHeight + eventGap));\n\n\t\t\t// If all events fit, show them all\n\t\t\tif (totalEvents <= maxEvents) {\n\t\t\t\treturn totalEvents;\n\t\t\t} else {\n\t\t\t\t// Otherwise, reserve space for \"more\" button by showing one less\n\t\t\t\treturn maxEvents > 0 ? maxEvents - 1 : 0;\n\t\t\t}\n\t\t};\n\t}, [contentHeight, eventHeight, eventGap]);\n\n\t// Use type assertion to satisfy TypeScript\n\treturn {\n\t\tcontentRef,\n\t\tcontentHeight,\n\t\tgetVisibleEventCount,\n\t} as EventVisibilityResult;\n}\n", "type": "registry:hook" }, { "path": "registry/br-hr/blocks/event-calendar/hooks/use-holidays.ts", "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport dayjs from \"../dayjs\";\nimport type { CalendarHoliday } from \"../types\";\n\ninterface GoogleHolidayResponse {\n\titems: {\n\t\tid: string;\n\t\tsummary: string;\n\t\tstart: { date: string };\n\t}[];\n}\n\ninterface UseHolidaysOptions {\n\tstart: Date;\n\tend: Date;\n}\n\nexport function useHolidays({ start, end }: UseHolidaysOptions) {\n\tconst [holidays, setHolidays] = useState([]);\n\tconst [isLoading, setIsLoading] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst controller = new AbortController();\n\n\t\tconst fetchHolidays = async () => {\n\t\t\tsetIsLoading(true);\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams({\n\t\t\t\t\ttimeMin: start.toISOString(),\n\t\t\t\t\ttimeMax: end.toISOString(),\n\t\t\t\t});\n\n\t\t\t\tconst response = await fetch(`/api/calendar/holidays?${params}`, {\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(\"Failed to fetch holidays\");\n\t\t\t\t}\n\n\t\t\t\tconst data = (await response.json()) as GoogleHolidayResponse;\n\t\t\t\tsetHolidays(\n\t\t\t\t\tdata.items.map((item) => ({\n\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\tname: item.summary,\n\t\t\t\t\t\tdate: dayjs(item.start.date).toDate(),\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof Error && error.name !== \"AbortError\") {\n\t\t\t\t\tsetHolidays([]);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetIsLoading(false);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfetchHolidays();\n\n\t\treturn () => controller.abort();\n\t}, [start, end]);\n\n\treturn { holidays, isLoading };\n}\n", "type": "registry:hook" }, { "path": "registry/br-hr/blocks/event-calendar/hooks/use-time-range-selection.ts", "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { EndHour, StartHour, WeekCellsHeight } from \"../constants\";\nimport dayjs from \"../dayjs\";\n\nconst MIN_SELECTION_MINUTES = 15;\nconst DRAG_THRESHOLD_PX = 5;\n\nexport interface TimeRangeSelectionState {\n\ttop: number;\n\theight: number;\n}\n\nexport interface PendingSelectionDisplay extends TimeRangeSelectionState {\n\tid: string;\n}\n\nexport type TimeRangeSelectionMode = \"single\" | \"multi\";\n\nexport interface UseTimeRangeSelectionOptions {\n\tmode?: TimeRangeSelectionMode;\n\tonEventCreate: (startTime: Date, endTime?: Date) => void;\n\tonRangeAdd?: (range: { start: Date; end: Date }) => void;\n\tpendingSelections?: PendingSelectionDisplay[];\n}\n\nfunction snapToQuarterHour(totalMinutes: number): number {\n\tconst clamped = Math.max(\n\t\tStartHour * 60,\n\t\tMath.min(EndHour * 60, totalMinutes),\n\t);\n\treturn Math.round(clamped / 15) * 15;\n}\n\nfunction minutesToDate(date: Date, totalMinutes: number): Date {\n\tconst result = new Date(date);\n\tresult.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0);\n\treturn result;\n}\n\nexport function minutesToSelectionStyle(\n\tstartMinutes: number,\n\tendMinutes: number,\n): TimeRangeSelectionState {\n\tconst minMinutes = Math.min(startMinutes, endMinutes);\n\tconst maxMinutes = Math.max(startMinutes, endMinutes);\n\tconst top = (minMinutes / 60 - StartHour) * WeekCellsHeight;\n\tconst height = Math.max(\n\t\tWeekCellsHeight / 4,\n\t\t((maxMinutes - minMinutes) / 60) * WeekCellsHeight,\n\t);\n\treturn { top, height };\n}\n\nexport function timeRangeToSelectionStyle(\n\tstart: Date,\n\tend: Date,\n): TimeRangeSelectionState {\n\tconst startMinutes = start.getHours() * 60 + start.getMinutes();\n\tconst endMinutes = end.getHours() * 60 + end.getMinutes();\n\treturn minutesToSelectionStyle(startMinutes, endMinutes);\n}\n\nexport function useTimeRangeSelection(\n\tdate: Date,\n\t{\n\t\tmode = \"single\",\n\t\tonEventCreate,\n\t\tonRangeAdd,\n\t\tpendingSelections = [],\n\t}: UseTimeRangeSelectionOptions,\n) {\n\tconst columnRef = useRef(null);\n\tconst [activeSelection, setActiveSelection] =\n\t\tuseState(null);\n\tconst dragStateRef = useRef<{\n\t\tpointerId: number;\n\t\tstartMinutes: number;\n\t\tendMinutes: number;\n\t\tstartY: number;\n\t\tdidDrag: boolean;\n\t} | null>(null);\n\n\tconst totalMinutesRange = (EndHour - StartHour) * 60;\n\n\tconst getMinutesFromClientY = useCallback(\n\t\t(clientY: number): number | null => {\n\t\t\tconst el = columnRef.current;\n\t\t\tif (!el) return null;\n\n\t\t\tconst rect = el.getBoundingClientRect();\n\t\t\tconst y = Math.max(0, Math.min(rect.height, clientY - rect.top));\n\t\t\tconst rawMinutes = (y / rect.height) * totalMinutesRange + StartHour * 60;\n\t\t\treturn snapToQuarterHour(rawMinutes);\n\t\t},\n\t\t[totalMinutesRange],\n\t);\n\n\tconst handlePointerDown = useCallback(\n\t\t(e: React.PointerEvent) => {\n\t\t\tif ((e.target as HTMLElement).closest(\"[data-calendar-event]\")) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst minutes = getMinutesFromClientY(e.clientY);\n\t\t\tif (minutes === null) return;\n\n\t\t\te.currentTarget.setPointerCapture(e.pointerId);\n\t\t\tdragStateRef.current = {\n\t\t\t\tpointerId: e.pointerId,\n\t\t\t\tstartMinutes: minutes,\n\t\t\t\tendMinutes: minutes,\n\t\t\t\tstartY: e.clientY,\n\t\t\t\tdidDrag: false,\n\t\t\t};\n\t\t\tsetActiveSelection(minutesToSelectionStyle(minutes, minutes));\n\t\t},\n\t\t[getMinutesFromClientY],\n\t);\n\n\tconst handlePointerMove = useCallback(\n\t\t(e: React.PointerEvent) => {\n\t\t\tconst state = dragStateRef.current;\n\t\t\tif (!state || state.pointerId !== e.pointerId) return;\n\n\t\t\tif (\n\t\t\t\t!state.didDrag &&\n\t\t\t\tMath.abs(e.clientY - state.startY) >= DRAG_THRESHOLD_PX\n\t\t\t) {\n\t\t\t\tstate.didDrag = true;\n\t\t\t}\n\n\t\t\tconst minutes = getMinutesFromClientY(e.clientY);\n\t\t\tif (minutes === null) return;\n\n\t\t\tstate.endMinutes = minutes;\n\t\t\tsetActiveSelection(\n\t\t\t\tminutesToSelectionStyle(state.startMinutes, state.endMinutes),\n\t\t\t);\n\t\t},\n\t\t[getMinutesFromClientY],\n\t);\n\n\tconst finishSelection = useCallback(\n\t\t(e: React.PointerEvent) => {\n\t\t\tconst state = dragStateRef.current;\n\t\t\tif (!state || state.pointerId !== e.pointerId) return;\n\n\t\t\tif (e.currentTarget.hasPointerCapture(e.pointerId)) {\n\t\t\t\te.currentTarget.releasePointerCapture(e.pointerId);\n\t\t\t}\n\n\t\t\tconst startMinutes = Math.min(state.startMinutes, state.endMinutes);\n\t\t\tconst endMinutes = Math.max(state.startMinutes, state.endMinutes);\n\t\t\tconst startTime = minutesToDate(date, startMinutes);\n\n\t\t\tconst endTime = (() => {\n\t\t\t\tif (state.didDrag) {\n\t\t\t\t\tconst durationMinutes = endMinutes - startMinutes;\n\t\t\t\t\treturn durationMinutes < MIN_SELECTION_MINUTES\n\t\t\t\t\t\t? dayjs(startTime).add(MIN_SELECTION_MINUTES, \"minute\").toDate()\n\t\t\t\t\t\t: minutesToDate(date, endMinutes);\n\t\t\t\t}\n\t\t\t\treturn dayjs(startTime).add(60, \"minute\").toDate();\n\t\t\t})();\n\n\t\t\tif (mode === \"multi\") {\n\t\t\t\tonRangeAdd?.({ start: startTime, end: endTime });\n\t\t\t} else if (state.didDrag) {\n\t\t\t\tonEventCreate(startTime, endTime);\n\t\t\t} else {\n\t\t\t\tonEventCreate(startTime);\n\t\t\t}\n\n\t\t\tdragStateRef.current = null;\n\t\t\tsetActiveSelection(null);\n\t\t},\n\t\t[date, mode, onEventCreate, onRangeAdd],\n\t);\n\n\tconst handlePointerUp = useCallback(\n\t\t(e: React.PointerEvent) => {\n\t\t\tfinishSelection(e);\n\t\t},\n\t\t[finishSelection],\n\t);\n\n\tconst handlePointerCancel = useCallback(\n\t\t(e: React.PointerEvent) => {\n\t\t\tconst state = dragStateRef.current;\n\t\t\tif (!state || state.pointerId !== e.pointerId) return;\n\n\t\t\tif (e.currentTarget.hasPointerCapture(e.pointerId)) {\n\t\t\t\te.currentTarget.releasePointerCapture(e.pointerId);\n\t\t\t}\n\n\t\t\tdragStateRef.current = null;\n\t\t\tsetActiveSelection(null);\n\t\t},\n\t\t[],\n\t);\n\n\treturn {\n\t\tcolumnRef,\n\t\tactiveSelection,\n\t\tpendingSelections,\n\t\thandlers: {\n\t\t\tonPointerDown: handlePointerDown,\n\t\t\tonPointerMove: handlePointerMove,\n\t\t\tonPointerUp: handlePointerUp,\n\t\t\tonPointerCancel: handlePointerCancel,\n\t\t},\n\t};\n}\n", "type": "registry:hook" }, { "path": "registry/br-hr/blocks/event-calendar/constants.ts", "content": "export const EventHeight = 24;\n\n// Vertical gap between events in pixels - controls spacing in month view\nexport const EventGap = 4;\n\n// Height of hour cells in week and day views - controls the scale of time display\nexport const WeekCellsHeight = 64;\n\n// Number of days to show in the agenda view\nexport const AgendaDaysToShow = 30;\n\n// Start and end hours for the week and day views\nexport const StartHour = 0;\nexport const EndHour = 24;\n\n// Default start and end times\nexport const DefaultStartHour = 9; // 9 AM\nexport const DefaultEndHour = 10; // 10 AM\n", "type": "registry:lib" }, { "path": "registry/br-hr/blocks/event-calendar/date-utils.ts", "content": "import dayjs from \"./dayjs\";\n\nexport function eachDayOfInterval(start: Date, end: Date): Date[] {\n\tconst days: Date[] = [];\n\tlet current = dayjs(start).startOf(\"day\");\n\tconst endDay = dayjs(end).startOf(\"day\");\n\n\twhile (current.isSameOrBefore(endDay, \"day\")) {\n\t\tdays.push(current.toDate());\n\t\tcurrent = current.add(1, \"day\");\n\t}\n\n\treturn days;\n}\n\nexport function eachHourOfInterval(start: Date, end: Date): Date[] {\n\tconst hours: Date[] = [];\n\tlet current = dayjs(start);\n\tconst endHour = dayjs(end);\n\n\twhile (current.isSameOrBefore(endHour)) {\n\t\thours.push(current.toDate());\n\t\tcurrent = current.add(1, \"hour\");\n\t}\n\n\treturn hours;\n}\n\nexport function areIntervalsOverlapping(\n\ta: { start: Date; end: Date },\n\tb: { start: Date; end: Date },\n): boolean {\n\treturn dayjs(a.start).isBefore(b.end) && dayjs(b.start).isBefore(a.end);\n}\n", "type": "registry:lib" }, { "path": "registry/br-hr/blocks/event-calendar/dayjs.ts", "content": "import dayjs from \"dayjs\";\nimport \"dayjs/locale/ja\";\nimport isBetween from \"dayjs/plugin/isBetween\";\nimport isSameOrAfter from \"dayjs/plugin/isSameOrAfter\";\nimport isSameOrBefore from \"dayjs/plugin/isSameOrBefore\";\nimport isToday from \"dayjs/plugin/isToday\";\nimport updateLocale from \"dayjs/plugin/updateLocale\";\n\ndayjs.extend(isBetween);\ndayjs.extend(isSameOrAfter);\ndayjs.extend(isSameOrBefore);\ndayjs.extend(isToday);\ndayjs.extend(updateLocale);\n\ndayjs.locale(\"ja\");\ndayjs.updateLocale(\"ja\", {\n\tweekStart: 0,\n});\n\nexport default dayjs;\n", "type": "registry:lib" }, { "path": "registry/br-hr/blocks/event-calendar/types.ts", "content": "export type CalendarView = \"month\" | \"week\" | \"day\" | \"agenda\";\n\nexport interface CalendarEvent {\n\tid: string;\n\ttitle: string;\n\tdescription?: string;\n\tstart: Date;\n\tend: Date;\n\tallDay?: boolean;\n\tcolor?: EventColor;\n\tlocation?: string;\n}\n\nexport type EventColor =\n\t| \"sky\"\n\t| \"amber\"\n\t| \"violet\"\n\t| \"rose\"\n\t| \"emerald\"\n\t| \"orange\";\n\nexport interface PendingTimeRange {\n\tid: string;\n\tdate: Date;\n\tstart: Date;\n\tend: Date;\n}\n\nexport interface CalendarHoliday {\n\tid: string;\n\tname: string;\n\tdate: Date;\n}\n", "type": "registry:lib" }, { "path": "registry/br-hr/blocks/event-calendar/utils.ts", "content": "import { cn } from \"@/lib/utils\";\nimport { AgendaDaysToShow } from \"./constants\";\nimport dayjs from \"./dayjs\";\nimport type {\n\tCalendarEvent,\n\tCalendarHoliday,\n\tCalendarView,\n\tEventColor,\n} from \"./types\";\n\n/**\n * Get CSS classes for event colors\n */\nexport function getEventColorClasses(color?: EventColor | string): string {\n\tconst eventColor = color || \"sky\";\n\n\tswitch (eventColor) {\n\t\tcase \"sky\":\n\t\t\treturn \"bg-sky-200/50 hover:bg-sky-200/40 text-sky-950/80 dark:bg-sky-400/25 dark:hover:bg-sky-400/20 dark:text-sky-200 shadow-sky-700/8\";\n\t\tcase \"amber\":\n\t\t\treturn \"bg-amber-200/50 hover:bg-amber-200/40 text-amber-950/80 dark:bg-amber-400/25 dark:hover:bg-amber-400/20 dark:text-amber-200 shadow-amber-700/8\";\n\t\tcase \"violet\":\n\t\t\treturn \"bg-violet-200/50 hover:bg-violet-200/40 text-violet-950/80 dark:bg-violet-400/25 dark:hover:bg-violet-400/20 dark:text-violet-200 shadow-violet-700/8\";\n\t\tcase \"rose\":\n\t\t\treturn \"bg-rose-200/50 hover:bg-rose-200/40 text-rose-950/80 dark:bg-rose-400/25 dark:hover:bg-rose-400/20 dark:text-rose-200 shadow-rose-700/8\";\n\t\tcase \"emerald\":\n\t\t\treturn \"bg-emerald-200/50 hover:bg-emerald-200/40 text-emerald-950/80 dark:bg-emerald-400/25 dark:hover:bg-emerald-400/20 dark:text-emerald-200 shadow-emerald-700/8\";\n\t\tcase \"orange\":\n\t\t\treturn \"bg-orange-200/50 hover:bg-orange-200/40 text-orange-950/80 dark:bg-orange-400/25 dark:hover:bg-orange-400/20 dark:text-orange-200 shadow-orange-700/8\";\n\t\tdefault:\n\t\t\treturn \"bg-sky-200/50 hover:bg-sky-200/40 text-sky-950/80 dark:bg-sky-400/25 dark:hover:bg-sky-400/20 dark:text-sky-200 shadow-sky-700/8\";\n\t}\n}\n\n/**\n * Get CSS classes for border radius based on event position in multi-day events\n */\nexport function getBorderRadiusClasses(\n\tisFirstDay: boolean,\n\tisLastDay: boolean,\n): string {\n\tif (isFirstDay && isLastDay) {\n\t\treturn \"rounded\"; // Both ends rounded\n\t} else if (isFirstDay) {\n\t\treturn \"rounded-l rounded-r-none\"; // Only left end rounded\n\t} else if (isLastDay) {\n\t\treturn \"rounded-r rounded-l-none\"; // Only right end rounded\n\t} else {\n\t\treturn \"rounded-none\"; // No rounded corners\n\t}\n}\n\n/**\n * Check if an event is a multi-day event\n */\nexport function isMultiDayEvent(event: CalendarEvent): boolean {\n\tconst eventStart = new Date(event.start);\n\tconst eventEnd = new Date(event.end);\n\treturn event.allDay || eventStart.getDate() !== eventEnd.getDate();\n}\n\n/**\n * Filter events for a specific day\n */\nexport function getEventsForDay(\n\tevents: CalendarEvent[],\n\tday: Date,\n): CalendarEvent[] {\n\treturn events\n\t\t.filter((event) => {\n\t\t\tconst eventStart = new Date(event.start);\n\t\t\treturn dayjs(day).isSame(eventStart, \"day\");\n\t\t})\n\t\t.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());\n}\n\n/**\n * Sort events with multi-day events first, then by start time\n */\nexport function sortEvents(events: CalendarEvent[]): CalendarEvent[] {\n\treturn [...events].sort((a, b) => {\n\t\tconst aIsMultiDay = isMultiDayEvent(a);\n\t\tconst bIsMultiDay = isMultiDayEvent(b);\n\n\t\tif (aIsMultiDay && !bIsMultiDay) return -1;\n\t\tif (!aIsMultiDay && bIsMultiDay) return 1;\n\n\t\treturn new Date(a.start).getTime() - new Date(b.start).getTime();\n\t});\n}\n\n/**\n * Get multi-day events that span across a specific day (but don't start on that day)\n */\nexport function getSpanningEventsForDay(\n\tevents: CalendarEvent[],\n\tday: Date,\n): CalendarEvent[] {\n\treturn events.filter((event) => {\n\t\tif (!isMultiDayEvent(event)) return false;\n\n\t\tconst eventStart = new Date(event.start);\n\t\tconst eventEnd = new Date(event.end);\n\t\tconst dayTime = dayjs(day);\n\n\t\t// Only include if it's not the start day but is either the end day or a middle day\n\t\treturn (\n\t\t\t!dayTime.isSame(eventStart, \"day\") &&\n\t\t\t(dayTime.isSame(eventEnd, \"day\") ||\n\t\t\t\t(dayTime.isAfter(eventStart, \"day\") &&\n\t\t\t\t\tdayTime.isBefore(eventEnd, \"day\")))\n\t\t);\n\t});\n}\n\n/**\n * Get all events visible on a specific day (starting, ending, or spanning)\n */\nexport function getAllEventsForDay(\n\tevents: CalendarEvent[],\n\tday: Date,\n): CalendarEvent[] {\n\treturn events.filter((event) => {\n\t\tconst eventStart = new Date(event.start);\n\t\tconst eventEnd = new Date(event.end);\n\t\tconst dayTime = dayjs(day);\n\n\t\treturn (\n\t\t\tdayTime.isSame(eventStart, \"day\") ||\n\t\t\tdayTime.isSame(eventEnd, \"day\") ||\n\t\t\t(dayTime.isAfter(eventStart, \"day\") && dayTime.isBefore(eventEnd, \"day\"))\n\t\t);\n\t});\n}\n\n/**\n * Get all events for a day (for agenda view)\n */\nexport function getAgendaEventsForDay(\n\tevents: CalendarEvent[],\n\tday: Date,\n): CalendarEvent[] {\n\treturn events\n\t\t.filter((event) => {\n\t\t\tconst eventStart = new Date(event.start);\n\t\t\tconst eventEnd = new Date(event.end);\n\t\t\tconst dayTime = dayjs(day);\n\n\t\t\treturn (\n\t\t\t\tdayTime.isSame(eventStart, \"day\") ||\n\t\t\t\tdayTime.isSame(eventEnd, \"day\") ||\n\t\t\t\t(dayTime.isAfter(eventStart, \"day\") &&\n\t\t\t\t\tdayTime.isBefore(eventEnd, \"day\"))\n\t\t\t);\n\t\t})\n\t\t.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());\n}\n\n/**\n * Add hours to a date\n */\nexport function addHoursToDate(date: Date, hours: number): Date {\n\treturn dayjs(date).add(hours, \"hour\").toDate();\n}\n\n/**\n * Get holidays for a specific day\n */\nexport function getHolidaysForDay(\n\tholidays: CalendarHoliday[],\n\tday: Date,\n): CalendarHoliday[] {\n\treturn holidays.filter((holiday) => dayjs(holiday.date).isSame(day, \"day\"));\n}\n\n/**\n * Check if a day is a holiday\n */\nexport function isHolidayDay(holidays: CalendarHoliday[], day: Date): boolean {\n\treturn getHolidaysForDay(holidays, day).length > 0;\n}\n\n/**\n * Get cell background classes (Sunday and holidays share red background)\n */\nexport function getDayCellBackgroundClasses(\n\tday: Date,\n\tholidays: CalendarHoliday[] = [],\n): string {\n\tconst dayOfWeek = dayjs(day).day();\n\tconst isHoliday = isHolidayDay(holidays, day);\n\n\treturn cn(\n\t\t(dayOfWeek === 0 || isHoliday) && \"bg-red-500/5 data-today:bg-red-500/10\",\n\t\tdayOfWeek === 6 && !isHoliday && \"bg-blue-500/5 data-today:bg-blue-500/10\",\n\t);\n}\n\n/**\n * Get header text classes (Sunday and holidays share red text)\n */\nexport function getDayHeaderTextClasses(\n\tday: Date,\n\tholidays: CalendarHoliday[] = [],\n): string {\n\tconst dayOfWeek = dayjs(day).day();\n\tconst isHoliday = isHolidayDay(holidays, day);\n\n\treturn cn(\n\t\t(dayOfWeek === 0 || isHoliday) && \"text-red-500\",\n\t\tdayOfWeek === 6 && !isHoliday && \"text-blue-500\",\n\t\tdayOfWeek !== 0 &&\n\t\t\tdayOfWeek !== 6 &&\n\t\t\t!isHoliday &&\n\t\t\t\"text-muted-foreground/70 data-today:text-foreground\",\n\t);\n}\n\n/**\n * Get the date range for fetching holidays based on the current view\n */\nexport function getHolidayFetchRange(\n\tcurrentDate: Date,\n\tview: CalendarView,\n): { start: Date; end: Date } {\n\tif (view === \"month\") {\n\t\tconst monthStart = dayjs(currentDate).startOf(\"month\");\n\t\tconst monthEnd = dayjs(currentDate).endOf(\"month\");\n\t\treturn {\n\t\t\tstart: monthStart.startOf(\"week\").toDate(),\n\t\t\tend: monthEnd.endOf(\"week\").toDate(),\n\t\t};\n\t}\n\n\tif (view === \"week\") {\n\t\treturn {\n\t\t\tstart: dayjs(currentDate).startOf(\"week\").toDate(),\n\t\t\tend: dayjs(currentDate).endOf(\"week\").toDate(),\n\t\t};\n\t}\n\n\tif (view === \"day\") {\n\t\treturn {\n\t\t\tstart: dayjs(currentDate).startOf(\"month\").toDate(),\n\t\t\tend: dayjs(currentDate).endOf(\"month\").toDate(),\n\t\t};\n\t}\n\n\treturn {\n\t\tstart: currentDate,\n\t\tend: dayjs(currentDate)\n\t\t\t.add(AgendaDaysToShow - 1, \"day\")\n\t\t\t.toDate(),\n\t};\n}\n", "type": "registry:lib" } ], "type": "registry:component" }