@@ -46,6 +46,7 @@ export interface CalendarViewProps {
4646 onViewChange ?: ( view : "month" | "week" | "day" ) => void
4747 onNavigate ?: ( date : Date ) => void
4848 onAddClick ?: ( ) => void
49+ onEventDrop ?: ( event : CalendarEvent , newStart : Date , newEnd ?: Date ) => void
4950 className ?: string
5051}
5152
@@ -59,6 +60,7 @@ function CalendarView({
5960 onViewChange,
6061 onNavigate,
6162 onAddClick,
63+ onEventDrop,
6264 className,
6365} : CalendarViewProps ) {
6466 const [ selectedView , setSelectedView ] = React . useState ( view )
@@ -228,6 +230,7 @@ function CalendarView({
228230 events = { events }
229231 onEventClick = { onEventClick }
230232 onDateClick = { onDateClick }
233+ onEventDrop = { onEventDrop }
231234 />
232235 ) }
233236 { selectedView === "week" && (
@@ -322,12 +325,70 @@ interface MonthViewProps {
322325 events : CalendarEvent [ ]
323326 onEventClick ?: ( event : CalendarEvent ) => void
324327 onDateClick ?: ( date : Date ) => void
328+ onEventDrop ?: ( event : CalendarEvent , newStart : Date , newEnd ?: Date ) => void
325329}
326330
327- function MonthView ( { date, events, onEventClick, onDateClick } : MonthViewProps ) {
331+ function MonthView ( { date, events, onEventClick, onDateClick, onEventDrop } : MonthViewProps ) {
328332 const days = getMonthDays ( date )
329333 const today = new Date ( )
330334 const weekDays = [ "Sun" , "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat" ]
335+ const [ draggedEventId , setDraggedEventId ] = React . useState < string | number | null > ( null )
336+ const [ dropTargetIndex , setDropTargetIndex ] = React . useState < number | null > ( null )
337+
338+ const handleDragStart = ( e : React . DragEvent , event : CalendarEvent ) => {
339+ setDraggedEventId ( event . id )
340+ e . dataTransfer . effectAllowed = "move"
341+ e . dataTransfer . setData ( "text/plain" , String ( event . id ) )
342+ }
343+
344+ const handleDragEnd = ( ) => {
345+ setDraggedEventId ( null )
346+ setDropTargetIndex ( null )
347+ }
348+
349+ const handleDragOver = ( e : React . DragEvent , index : number ) => {
350+ e . preventDefault ( )
351+ e . dataTransfer . dropEffect = "move"
352+ setDropTargetIndex ( index )
353+ }
354+
355+ const handleDragLeave = ( e : React . DragEvent ) => {
356+ // Only clear when actually leaving the cell, not when moving over child elements
357+ if ( ! e . currentTarget . contains ( e . relatedTarget as Node ) ) {
358+ setDropTargetIndex ( null )
359+ }
360+ }
361+
362+ const handleDrop = ( e : React . DragEvent , targetDay : Date ) => {
363+ e . preventDefault ( )
364+ setDropTargetIndex ( null )
365+ setDraggedEventId ( null )
366+
367+ if ( ! onEventDrop ) return
368+
369+ const eventId = e . dataTransfer . getData ( "text/plain" )
370+ const draggedEvent = events . find ( ( ev ) => String ( ev . id ) === eventId )
371+ if ( ! draggedEvent ) return
372+
373+ const oldStart = new Date ( draggedEvent . start )
374+ const oldStartDay = new Date ( oldStart )
375+ oldStartDay . setHours ( 0 , 0 , 0 , 0 )
376+
377+ const newTargetDay = new Date ( targetDay )
378+ newTargetDay . setHours ( 0 , 0 , 0 , 0 )
379+
380+ const deltaMs = newTargetDay . getTime ( ) - oldStartDay . getTime ( )
381+ if ( deltaMs === 0 ) return
382+
383+ const newStart = new Date ( oldStart . getTime ( ) + deltaMs )
384+
385+ let newEnd : Date | undefined
386+ if ( draggedEvent . end ) {
387+ newEnd = new Date ( new Date ( draggedEvent . end ) . getTime ( ) + deltaMs )
388+ }
389+
390+ onEventDrop ( draggedEvent , newStart , newEnd )
391+ }
331392
332393 return (
333394 < div className = "flex flex-col h-full" >
@@ -355,9 +416,13 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
355416 key = { index }
356417 className = { cn (
357418 "border-b border-r last:border-r-0 p-2 min-h-[100px] cursor-pointer hover:bg-accent/50" ,
358- ! isCurrentMonth && "bg-muted/30 text-muted-foreground"
419+ ! isCurrentMonth && "bg-muted/30 text-muted-foreground" ,
420+ dropTargetIndex === index && "ring-2 ring-primary"
359421 ) }
360422 onClick = { ( ) => onDateClick ?.( day ) }
423+ onDragOver = { ( e ) => handleDragOver ( e , index ) }
424+ onDragLeave = { handleDragLeave }
425+ onDrop = { ( e ) => handleDrop ( e , day ) }
361426 >
362427 < div
363428 className = { cn (
@@ -372,9 +437,13 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
372437 { dayEvents . slice ( 0 , 3 ) . map ( ( event ) => (
373438 < div
374439 key = { event . id }
440+ draggable = { ! ! onEventDrop }
441+ onDragStart = { ( e ) => handleDragStart ( e , event ) }
442+ onDragEnd = { handleDragEnd }
375443 className = { cn (
376444 "text-xs px-2 py-1 rounded truncate cursor-pointer hover:opacity-80" ,
377- event . color || DEFAULT_EVENT_COLOR
445+ event . color || DEFAULT_EVENT_COLOR ,
446+ draggedEventId === event . id && "opacity-50"
378447 ) }
379448 style = {
380449 event . color && event . color . startsWith ( "#" )
0 commit comments