---
title: "React Scheduler 24-Hour Rotating Crew Shift Rota Example | Mobiscroll"
component: "Scheduler"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/scheduler/24-hour-manufacturing-shift-rota-planning"
---

# 24-Hour Rotating Crew Shift Rota - React Scheduler demo

Build a 24-hour rotating crew rota with the React Scheduler — three crews, three shifts, and automatic conflict resolution when shifts move. For React and Next.JS.

- [Live demo](https://demo.mobiscroll.com/react/scheduler/24-hour-manufacturing-shift-rota-planning)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/scheduler/24-hour-manufacturing-shift-rota-planning)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/scheduler/24-hour-manufacturing-shift-rota-planning)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/scheduler/24-hour-manufacturing-shift-rota-planning.md), [jQuery](https://demo.mobiscroll.com/jquery/scheduler/24-hour-manufacturing-shift-rota-planning.md), [Angular](https://demo.mobiscroll.com/angular/scheduler/24-hour-manufacturing-shift-rota-planning.md), [Vue](https://demo.mobiscroll.com/vue/scheduler/24-hour-manufacturing-shift-rota-planning.md)

## Description

The scheduler can be configured for industrial or service environments requiring 24/7 coverage, such as manufacturing lines or critical infrastructure maintenance. This setup uses three resources (Crew A, Crew B, Crew C) to demonstrate a cyclical, rotating roster, like the 3-Crew, 3-Shift Rota.

To visualize the continuous operation and manage shifts that span midnight, the scheduler view is configured with a custom, 24-hour cycle, starting at 06:00 and ending at 06:00 the following day. This is achieved using the custom day-offset feature added to the `startTime` (e.g., '20:00-1') and `endTime` (e.g., '08:00+1') properties under the [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) option, which initiates the view relative to the previous or next calendar day. Furthermore, to ensure compliance with 8-hour shift lengths and prevent invalid scheduling, the drag-and-drop functionality is constrained by setting the [`dragTimeStep`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragTimeStep) to 8 hours, requiring all events to be moved only in 8-hour increments and anchoring the alignment with the [`dragTimeStepBase: 'viewStart'`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragTimeStepBase) option. The drag-and-drop logic is implemented for automatic conflict resolution: when an event is dragged to a different time or resource column within the same day, it automatically updates and shifts any conflicting events (those sharing the same time slot or resource column) to maintain scheduling integrity and prevent overlaps.

## What this demo shows

- A desktop weekly scheduler for 24-hour shift planning, with a fixed week strip, repeated resources for each day, and a vertically scrollable time grid running from 6 AM to 6 AM the following day.
- **Header navigation** The month and year label in the top left opens date navigation, while the blue previous and next arrows and the Today button on the right make it easy to move between weeks and jump back to the current day.
- **Week strip**: The fixed strip below the header displays the days in the selected week, with the current date highlighted by a blue circle.
- **Resources**: Each day shows the same three resources: Crew A, Crew B, and Crew C.
- **Resource columns**: Each crew has its own column within the day column, keeping resources grouped by date.
- **Time grid**: The scheduler uses a custom 24-hour range from 6 AM to 6 AM the next day, with midnight marked by a stronger horizontal line.
- **Scrolling behavior**: The time grid scrolls vertically through the hours of the selected week.
- **Timed events**: Shifts appear as colored event cards with a colored stripe, bold shift title, and start and end time. Morning Shift runs from 6 AM to 2 PM, Afternoon Shift from 2 PM to 10 PM, and Night Shift from 10 PM to 6 AM.
- **Event overlap**: Shift events cannot overlap.
- **Shift coverage**: By default, each resource has a scheduled shift. Users can delete, create, or move shifts.
- **Missing shifts**: When a shift is deleted, the affected resource column is highlighted with a red background to show that no shift is scheduled for that resource.
- **Available shift slots**: When a resource has no shift, hovering over an available 8-hour shift period highlights the slot in green.
- **Shift repositioning**: Moving a shift can automatically update another shift on the same day to preserve coverage.
- **Current time**: A blue current-time line appears across the time grid.
- **Hover behavior**: Hovering over the time grid shows a time indicator that follows the cursor in 8-hour increments.
- **Event interaction**: Hovering over or selecting an event highlights it.
- **Toast messages**: Clicking an occupied time slot or trying to create a shift when every slot is already assigned shows an "Already assigned" toast at the bottom center of the scheduler. Deleting a shift shows a deletion toast with the shift name.

## Best for

- **Manufacturing and production facilities**: Schedule morning, afternoon, and night shifts across production lines, departments, or work crews while maintaining continuous coverage.
- **Warehousing and logistics operations**: Coordinate warehouse teams, loading crews, dispatch staff, and distribution personnel across multiple shifts.
- **Construction and field service teams**: Manage rotating crews, subcontractors, inspectors, and site personnel working across different schedules and locations.
- **Security and monitoring services**: Plan guard rotations, patrol schedules, control room operators, and emergency response teams around the clock.
- **Healthcare and care facilities**: Organize nurse rotations, support staff schedules, and departmental coverage across day, evening, and overnight shifts.
- **Hospitality and tourism businesses**: Schedule hotel staff, front desk teams, housekeeping crews, event staff, and service personnel.
- **Retail operations**: Manage store associates, supervisors, cashiers, and department teams while balancing staffing levels during peak and off-peak hours.
- **Transportation and aviation services**: Coordinate drivers, dispatchers, pilots, cabin crews, ground staff, and maintenance teams.
- **Customer support and contact centers**: Plan agent coverage across time zones, shifts, and support channels to meet service level requirements.
- **Public services and emergency organizations**: Schedule firefighters, police officers, emergency medical personnel, and municipal service teams.
- **Media, broadcasting, and operations centers**: Coordinate production crews, technical operators, studio staff, and live-event teams that require continuous coverage.
- **IT and DevOps teams**: Manage on-call rotations, support shifts, operations coverage, and incident response schedules for 24/7 systems.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-cssClass) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`colors`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-colors) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`dragTimeStep`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragTimeStep) - option
- [`eventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventDelete) - option
- [`eventOverlap`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventOverlap) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onCellHoverIn`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onCellHoverIn) - event
- [`onCellHoverOut`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onCellHoverOut) - event
- [`onEventCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreate) - event
- [`onEventClick`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventClick) - event
- [`onEventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDelete) - event
- [`onEventDragStart`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDragStart) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdateFailed) - event
- [`onEventUpdate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdate) - event
- [`groupBy`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-groupBy) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

```jsx
import { Eventcalendar, setOptions, Toast } from '@mobiscroll/react';
import { useCallback, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

const morningColor = '#4a8c4d';
const afternoonColor = '#f87c6b';
const nightColor = '#8567AD';

const shifts = {
  morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: morningColor },
  afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: afternoonColor },
  night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: nightColor },
};

const getShiftByHour = (hour) => {
  if (hour >= 6 && hour < 14) {
    return shifts.morning;
  }
  if (hour >= 14 && hour < 22) {
    return shifts.afternoon;
  }
  if (hour >= 22 || hour < 6) {
    return shifts.night;
  }
  return shifts.afternoon;
};

const calculateStart = (start) => {
  const d = new Date(start);
  const originalHour = d.getHours();
  const shift = getShiftByHour(originalHour);
  d.setHours(shift.startHour, 0, 0, 0);
  if (shift.startHour === 22 && originalHour < 6) {
    d.setDate(d.getDate() - 1);
  }
  return d;
};

const calculateEnd = (start) => {
  const d = new Date(start);
  const startHour = d.getHours();
  const shift = getShiftByHour(d.getHours());
  d.setHours(shift.endHour, 0, 0, 0);
  if (shift.nextDay && startHour === 22) {
    d.setDate(d.getDate() + 1);
  }
  return d;
};

const getTitle = (startHours) => getShiftByHour(startHours).title;

const getColor = (startHours) => getShiftByHour(startHours).color;

const initialEvents = [
  { id: 1, resource: 'A', title: 'Morning Shift', start: '2026-10-23T06:00', end: '2026-10-23T14:00', color: morningColor },
  {
    id: 2,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-23T14:00',
    end: '2026-10-23T22:00',
    color: afternoonColor,
  },
  { id: 3, resource: 'C', title: 'Night Shift', start: '2026-10-23T22:00', end: '2026-10-24T06:00', color: nightColor },
  { id: 4, resource: 'A', title: 'Morning Shift', start: '2026-10-24T06:00', end: '2026-10-24T14:00', color: morningColor },
  {
    id: 5,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-24T14:00',
    end: '2026-10-24T22:00',
    color: afternoonColor,
  },
  { id: 6, resource: 'C', title: 'Night Shift', start: '2026-10-24T22:00', end: '2026-10-25T06:00', color: nightColor },
  // Showing partial data. Download full source.
];

const resources = [
  { id: 'A', name: 'Crew A' },
  { id: 'B', name: 'Crew B' },
  { id: 'C', name: 'Crew C' },
];

const view = {
  scheduler: {
    type: 'week',
    allDay: false,
    startTime: '06:00',
    endTime: '06:00+1',
    timeCellStep: 120,
    timeLabelStep: 120,
  },
};

function App() {
  const [myEvents, setMyEvents] = useState(initialEvents);
  const [colors, setColors] = useState([]);
  const [redResources, setRedResources] = useState({});
  const [draggedEventStart, setDraggedEventStart] = useState(null);
  const [draggedEventEnd, setDraggedEventEnd] = useState(null);
  const [draggedEventResource, setDraggedEventResource] = useState(null);
  const [availableSlotOnHover, setAvailableSlotOnHover] = useState(null);
  const [isToastOpen, setToastOpen] = useState(false);
  const [toastMessage, setToastMessage] = useState('');

  const getAvailableSlots = useCallback(
    (resourceId, dayStart) => {
      const dayEnd = new Date(dayStart);
      dayEnd.setDate(dayEnd.getDate() + 1);
      dayEnd.setHours(6, 0, 0, 0);
      const dayEvents = myEvents.filter((e) => {
        const eStart = new Date(e.start);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const slots = { morning: true, afternoon: true, night: true };
      dayEvents.forEach((e) => {
        if (e.resource === resourceId) {
          slots.morning = slots.afternoon = slots.night = false;
        } else {
          const startHour = new Date(e.start).getHours();
          if (startHour === 6) {
            slots.morning = false;
          } else if (startHour === 14) {
            slots.afternoon = false;
          } else if (startHour === 22) {
            slots.night = false;
          }
        }
      });
      return slots;
    },
    [myEvents],
  );

  const clearColorsForResource = useCallback((currentColors, resourceId, date) => {
    const base = new Date(date);
    base.setHours(0, 0, 0, 0);
    const baseTime = +base;
    return currentColors.filter((c) => {
      if (c.resource === resourceId) {
        const cDate = new Date(c.start);
        cDate.setHours(0, 0, 0, 0);
        if (+cDate === baseTime) {
          const hour = new Date(c.start).getHours();
          if (hour === 6 || hour === 14 || hour === 22) {
            return false;
          }
        }
      }
      return true;
    });
  }, []);

  const handleCellHoverIn = useCallback(
    (args) => {
      const hoveredDate = new Date(args.date);
      const shift = getShiftByHour(hoveredDate.getHours());
      if (!shift) return;

      const dayStart = new Date(args.date);
      dayStart.setHours(6, 0, 0, 0);
      const availableSlots = getAvailableSlots(args.resource.id, dayStart);
      const slotKey = Object.keys(availableSlots).find((key) => availableSlots[key] && shifts[key].startHour === shift.startHour);
      if (slotKey) {
        const startTime = new Date(dayStart);
        startTime.setHours(shifts[slotKey].startHour, 0, 0, 0);
        const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1);
        const slot = {
          background: '#c1ffe180',
          cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
          start: +startTime + 1,
          end: endTime,
          resource: args.resource.id,
        };
        setAvailableSlotOnHover(slot);
        setColors((prev) => [...prev, slot]);
      }
    },
    [getAvailableSlots],
  );

  const handleCellHoverOut = useCallback(() => {
    if (availableSlotOnHover) {
      setColors((prev) => prev.filter((c) => c !== availableSlotOnHover));
      setAvailableSlotOnHover(null);
    }
  }, [availableSlotOnHover]);

  const handleEventCreate = useCallback(
    (args) => {
      const event = { ...args.event, id: Date.now() };
      const dayStart = new Date(event.start);
      dayStart.setHours(6, 0, 0, 0);
      const dayEnd = new Date(dayStart);
      dayEnd.setDate(dayEnd.getDate() + 1);
      dayEnd.setHours(6, 0, 0, 0);
      const dayEvents = myEvents.filter((e) => {
        const eStart = new Date(e.start);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const conflict = dayEvents.find((ev) => ev.resource === event.resource || +new Date(ev.start) === +new Date(event.start));
      if (conflict) {
        setToastMessage('Already assigned');
        setToastOpen(true);
        return false;
      }
      setColors((prev) => clearColorsForResource(prev, event.resource, event.start));
      const day = new Date(event.start);
      day.setHours(0, 0, 0, 0);
      setRedResources((prev) => ({ ...prev, [event.resource + day.toISOString()]: false }));
      setAvailableSlotOnHover(null);
      setMyEvents((prev) => [...prev, event]);
    },
    [myEvents, clearColorsForResource],
  );

  const handleEventClick = useCallback(() => {
    setToastMessage('Already assigned');
    setToastOpen(true);
  }, []);

  const handleEventDelete = useCallback((args) => {
    setMyEvents((prev) => prev.filter((e) => e.id !== args.event.id));
    const colorStart = new Date(args.event.start);
    colorStart.setHours(6, 0, 0, 0);
    const colorEnd = new Date(args.event.start);
    colorEnd.setDate(colorEnd.getDate() + 1);
    colorEnd.setHours(6, 0, 0, 0);
    setColors((prev) => [...prev, { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: args.event.resource }]);
    const resource = args.event.resource;
    const day = new Date(args.event.start);
    day.setHours(0, 0, 0, 0);
    setRedResources((prev) => ({ ...prev, [resource + day.toISOString()]: true }));
    setToastMessage(args.event.title + ' deleted');
    setToastOpen(true);
  }, []);

  const handleEventDragStart = useCallback((args) => {
    setDraggedEventStart(args.event.start);
    setDraggedEventEnd(args.event.end);
    setDraggedEventResource(args.event.resource);
  }, []);

  const handleEventUpdateFailed = useCallback(() => {
    setToastMessage('Already assigned');
    setToastOpen(true);
  }, []);

  const handleEventUpdate = useCallback(
    (args) => {
      const event = args.event;
      const dayStart = new Date(event.start);
      dayStart.setHours(6, 0, 0, 0);
      const dragStartDay = new Date(draggedEventStart);
      dragStartDay.setHours(6, 0, 0, 0);
      if (+dayStart !== +dragStartDay) {
        return false;
      }

      const dayEnd = new Date(dayStart);
      dayEnd.setDate(dayEnd.getDate() + 1);
      dayEnd.setHours(6, 0, 0, 0);
      const dayEvents = myEvents.filter((e) => {
        const eStart = new Date(e.start);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const conflicts = dayEvents.filter(
        (ev) => ev.id !== event.id && (ev.resource === event.resource || +new Date(ev.start) === +new Date(event.start)),
      );
      const evStart = new Date(event.start);
      event.title = getTitle(evStart.getHours());
      event.color = getColor(evStart.getHours());

      if (conflicts.length) {
        let shouldReturn = false;
        conflicts.forEach((sh, i) => {
          if (sh.resource === event.resource) {
            if (+event.start === +new Date(sh.start)) {
              shouldReturn = true;
            }
            sh.resource = draggedEventResource;
            conflicts[i] = sh;
          } else {
            sh.start = draggedEventStart;
            sh.end = draggedEventEnd;
            const start = new Date(draggedEventStart);
            sh.title = getTitle(start.getHours());
            sh.color = getColor(start.getHours());
            conflicts[i] = sh;
          }
        });
        if (shouldReturn) {
          return false;
        }
        setMyEvents((prev) =>
          prev.map((e) => {
            const updated = [...conflicts, event].find((u) => u.id === e.id);
            return updated || e;
          }),
        );
      } else {
        if (dayEvents.some((ev) => ev.id === event.id)) {
          setMyEvents((prev) => prev.map((e) => (e.id === event.id ? event : e)));
        }
      }

      setColors((prev) => clearColorsForResource(prev, event.resource, event.start));
      const day = new Date(event.start);
      day.setHours(0, 0, 0, 0);
      setRedResources((prev) => ({ ...prev, [event.resource + day.toISOString()]: false }));
      if (!conflicts.length && event.resource !== draggedEventResource) {
        const colorEnd = new Date(dayStart);
        colorEnd.setDate(colorEnd.getDate() + 1);
        colorEnd.setHours(6, 0, 0, 0);
        setColors((prev) => [...prev, { start: dayStart, resource: args.oldEvent.resource, background: '#ffdbd280', end: colorEnd }]);
        setRedResources((prev) => ({ ...prev, [args.oldEvent.resource + day.toISOString()]: true }));
      }
      setDraggedEventStart(null);
      setDraggedEventEnd(null);
      setDraggedEventResource(null);
    },
    [myEvents, draggedEventStart, draggedEventEnd, draggedEventResource, clearColorsForResource],
  );

  const extendDefaultEvent = useCallback((args) => {
    const start = new Date(args.start);
    const newStart = calculateStart(start);
    const newEnd = calculateEnd(start);
    const title = getTitle(newStart.getHours());
    const color = getColor(newStart.getHours());
    return {
      title: title,
      start: newStart,
      end: newEnd,
      color: color,
    };
  }, []);

  const customResource = useCallback(
    (res, day) => {
      const style = redResources[res.id + day.toISOString()]
        ? { color: '#53000080', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
        : {};
      return <div style={style}>{res.name}</div>;
    },
    [redResources],
  );

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  return (
    <>
      <Eventcalendar
        cssClass="mds-24-hour-manufacturing-calendar"
        data={myEvents}
        colors={colors}
        dragToMove={true}
        dragToCreate={false}
        clickToCreate="single"
        dragToResize={false}
        dragTimeStep={480}
        dragTimeStepBase="viewStart"
        eventDelete={true}
        eventOverlap={false}
        extendDefaultEvent={extendDefaultEvent}
        onCellHoverIn={handleCellHoverIn}
        onCellHoverOut={handleCellHoverOut}
        onEventCreate={handleEventCreate}
        onEventClick={handleEventClick}
        onEventDelete={handleEventDelete}
        onEventDragStart={handleEventDragStart}
        onEventUpdateFailed={handleEventUpdateFailed}
        onEventUpdate={handleEventUpdate}
        groupBy="date"
        resources={resources}
        renderResource={customResource}
        view={view}
      />
      <Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
    </>
  );
}

export default App;
```

### TSX

```tsx
import {
  Eventcalendar,
  MbscCalendarColor,
  MbscCalendarEvent,
  MbscCellHoverEvent,
  MbscDateType,
  MbscEventcalendarView,
  MbscEventCreateEvent,
  MbscEventDeleteEvent,
  MbscEventDragEvent,
  MbscEventUpdateEvent,
  MbscResource,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { FC, useCallback, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

interface Shift {
  startHour: number;
  endHour: number;
  title: string;
  color: string;
  nextDay?: boolean;
}

const morningColor = '#4a8c4d';
const afternoonColor = '#f87c6b';
const nightColor = '#8567AD';

const shifts: Record<string, Shift> = {
  morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: morningColor },
  afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: afternoonColor },
  night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: nightColor },
};

const getShiftByHour = (hour: number): Shift => {
  if (hour >= 6 && hour < 14) {
    return shifts.morning;
  }
  if (hour >= 14 && hour < 22) {
    return shifts.afternoon;
  }
  if (hour >= 22 || hour < 6) {
    return shifts.night;
  }
  return shifts.afternoon;
};

const calculateStart = (start: Date): Date => {
  const d = new Date(start);
  const originalHour = d.getHours();
  const shift = getShiftByHour(originalHour);
  d.setHours(shift.startHour, 0, 0, 0);
  if (shift.startHour === 22 && originalHour < 6) {
    d.setDate(d.getDate() - 1);
  }
  return d;
};

const calculateEnd = (start: Date): Date => {
  const d = new Date(start);
  const startHour = d.getHours();
  const shift = getShiftByHour(d.getHours());
  d.setHours(shift.endHour, 0, 0, 0);
  if (shift.nextDay && startHour === 22) {
    d.setDate(d.getDate() + 1);
  }
  return d;
};

const getTitle = (startHours: number): string => getShiftByHour(startHours).title;

const getColor = (startHours: number): string => getShiftByHour(startHours).color;

const initialEvents: MbscCalendarEvent[] = [
  { id: 1, resource: 'A', title: 'Morning Shift', start: '2026-10-23T06:00', end: '2026-10-23T14:00', color: morningColor },
  {
    id: 2,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-23T14:00',
    end: '2026-10-23T22:00',
    color: afternoonColor,
  },
  { id: 3, resource: 'C', title: 'Night Shift', start: '2026-10-23T22:00', end: '2026-10-24T06:00', color: nightColor },
  { id: 4, resource: 'A', title: 'Morning Shift', start: '2026-10-24T06:00', end: '2026-10-24T14:00', color: morningColor },
  {
    id: 5,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-24T14:00',
    end: '2026-10-24T22:00',
    color: afternoonColor,
  },
  { id: 6, resource: 'C', title: 'Night Shift', start: '2026-10-24T22:00', end: '2026-10-25T06:00', color: nightColor },
  // Showing partial data. Download full source.
];

const resources: MbscResource[] = [
  { id: 'A', name: 'Crew A' },
  { id: 'B', name: 'Crew B' },
  { id: 'C', name: 'Crew C' },
];

const view: MbscEventcalendarView = {
  scheduler: {
    type: 'week',
    allDay: false,
    startTime: '06:00',
    endTime: '06:00+1',
    timeCellStep: 120,
    timeLabelStep: 120,
  },
};

const App: FC = () => {
  const [myEvents, setMyEvents] = useState<MbscCalendarEvent[]>(initialEvents);
  const [colors, setColors] = useState<MbscCalendarColor[]>([]);
  const [redResources, setRedResources] = useState<Record<string, boolean>>({});
  const [draggedEventStart, setDraggedEventStart] = useState<Date | null>(null);
  const [draggedEventEnd, setDraggedEventEnd] = useState<Date | null>(null);
  const [draggedEventResource, setDraggedEventResource] = useState<string | null>(null);
  const [availableSlotOnHover, setAvailableSlotOnHover] = useState<MbscCalendarColor | null>(null);
  const [isToastOpen, setToastOpen] = useState<boolean>(false);
  const [toastMessage, setToastMessage] = useState<string>('');

  const getAvailableSlots = useCallback(
    (resourceId: string, dayStart: Date): Record<string, boolean> => {
      const dayEnd = new Date(dayStart);
      dayEnd.setDate(dayEnd.getDate() + 1);
      dayEnd.setHours(6, 0, 0, 0);
      const dayEvents = myEvents.filter((e) => {
        const eStart = new Date(e.start as Date);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const slots: Record<string, boolean> = { morning: true, afternoon: true, night: true };
      dayEvents.forEach((e) => {
        if (e.resource === resourceId) {
          slots.morning = slots.afternoon = slots.night = false;
        } else {
          const startHour = new Date(e.start as Date).getHours();
          if (startHour === 6) {
            slots.morning = false;
          } else if (startHour === 14) {
            slots.afternoon = false;
          } else if (startHour === 22) {
            slots.night = false;
          }
        }
      });
      return slots;
    },
    [myEvents],
  );

  const clearColorsForResource = useCallback((currentColors: MbscCalendarColor[], resourceId: string, date: Date): MbscCalendarColor[] => {
    const base = new Date(date);
    base.setHours(0, 0, 0, 0);
    const baseTime = +base;
    return currentColors.filter((c) => {
      if (c.resource === resourceId) {
        const cDate = new Date(c.start as Date);
        cDate.setHours(0, 0, 0, 0);
        if (+cDate === baseTime) {
          const hour = new Date(c.start as Date).getHours();
          if (hour === 6 || hour === 14 || hour === 22) {
            return false;
          }
        }
      }
      return true;
    });
  }, []);

  const handleCellHoverIn = useCallback(
    (args: MbscCellHoverEvent) => {
      const hoveredDate = new Date(args.date);
      const shift = getShiftByHour(hoveredDate.getHours());
      if (!shift) return;

      const dayStart = new Date(args.date);
      dayStart.setHours(6, 0, 0, 0);
      const availableSlots = getAvailableSlots(String(args.resource.id), dayStart);
      const slotKey = Object.keys(availableSlots).find((key) => availableSlots[key] && shifts[key].startHour === shift.startHour);
      if (slotKey) {
        const startTime = new Date(dayStart);
        startTime.setHours(shifts[slotKey].startHour, 0, 0, 0);
        const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1);
        const slot: MbscCalendarColor = {
          background: '#c1ffe180',
          cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
          start: startTime as MbscDateType,
          end: endTime,
          resource: String(args.resource.id),
        };
        setAvailableSlotOnHover(slot);
        setColors((prev) => [...prev, slot]);
      }
    },
    [getAvailableSlots],
  );

  const handleCellHoverOut = useCallback(() => {
    if (availableSlotOnHover) {
      setColors((prev) => prev.filter((c) => c !== availableSlotOnHover));
      setAvailableSlotOnHover(null);
    }
  }, [availableSlotOnHover]);

  const handleEventCreate = useCallback(
    (args: MbscEventCreateEvent) => {
      const event = { ...args.event, id: Date.now() };
      const dayStart = new Date(event.start as Date);
      dayStart.setHours(6, 0, 0, 0);
      const dayEnd = new Date(dayStart);
      dayEnd.setDate(dayEnd.getDate() + 1);
      dayEnd.setHours(6, 0, 0, 0);
      const dayEvents = myEvents.filter((e) => {
        const eStart = new Date(e.start as Date);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const conflict = dayEvents.find(
        (ev) => ev.resource === event.resource || +new Date(ev.start as Date) === +new Date(event.start as Date),
      );
      if (conflict) {
        setToastMessage('Already assigned');
        setToastOpen(true);
        return false;
      }
      setColors((prev) => clearColorsForResource(prev, String(event.resource), event.start as Date));
      const day = new Date(event.start as Date);
      day.setHours(0, 0, 0, 0);
      setRedResources((prev) => ({ ...prev, [String(event.resource) + day.toISOString()]: false }));
      setAvailableSlotOnHover(null);
      setMyEvents((prev) => [...prev, event]);
    },
    [myEvents, clearColorsForResource],
  );

  const handleEventClick = useCallback(() => {
    setToastMessage('Already assigned');
    setToastOpen(true);
  }, []);

  const handleEventDelete = useCallback((args: MbscEventDeleteEvent) => {
    setMyEvents((prev) => prev.filter((e) => e.id !== args.event.id));
    const colorStart = new Date(args.event.start as Date);
    colorStart.setHours(6, 0, 0, 0);
    const colorEnd = new Date(args.event.start as Date);
    colorEnd.setDate(colorEnd.getDate() + 1);
    colorEnd.setHours(6, 0, 0, 0);
    setColors((prev) => [...prev, { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: String(args.event.resource) }]);
    const resource = String(args.event.resource);
    const day = new Date(args.event.start as Date);
    day.setHours(0, 0, 0, 0);
    setRedResources((prev) => ({ ...prev, [resource + day.toISOString()]: true }));
    setToastMessage(args.event.title + ' deleted');
    setToastOpen(true);
  }, []);

  const handleEventDragStart = useCallback((args: MbscEventDragEvent) => {
    setDraggedEventStart(args.event.start as Date);
    setDraggedEventEnd(args.event.end as Date);
    setDraggedEventResource(String(args.event.resource));
  }, []);

  const handleEventUpdateFailed = useCallback(() => {
    setToastMessage('Already assigned');
    setToastOpen(true);
  }, []);

  const handleEventUpdate = useCallback(
    (args: MbscEventUpdateEvent) => {
      const event = args.event;
      const dayStart = new Date(event.start as Date);
      dayStart.setHours(6, 0, 0, 0);
      const dragStartDay = new Date(draggedEventStart!);
      dragStartDay.setHours(6, 0, 0, 0);
      if (+dayStart !== +dragStartDay) {
        return false;
      }

      const dayEnd = new Date(dayStart);
      dayEnd.setDate(dayEnd.getDate() + 1);
      dayEnd.setHours(6, 0, 0, 0);
      const dayEvents = myEvents.filter((e) => {
        const eStart = new Date(e.start as Date);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const conflicts = dayEvents.filter(
        (ev) => ev.id !== event.id && (ev.resource === event.resource || +new Date(ev.start as Date) === +new Date(event.start as Date)),
      );
      const evStart = new Date(event.start as Date);
      event.title = getTitle(evStart.getHours());
      event.color = getColor(evStart.getHours());

      if (conflicts.length) {
        let shouldReturn = false;
        conflicts.forEach((sh, i) => {
          if (sh.resource === event.resource) {
            if (+(event.start as Date) === +new Date(sh.start as Date)) {
              shouldReturn = true;
            }
            sh.resource = draggedEventResource!;
            conflicts[i] = sh;
          } else {
            sh.start = draggedEventStart!;
            sh.end = draggedEventEnd!;
            const start = new Date(draggedEventStart!);
            sh.title = getTitle(start.getHours());
            sh.color = getColor(start.getHours());
            conflicts[i] = sh;
          }
        });
        if (shouldReturn) {
          return false;
        }
        setMyEvents((prev) =>
          prev.map((e) => {
            const updated = [...conflicts, event].find((u) => u.id === e.id);
            return updated || e;
          }),
        );
      } else {
        if (dayEvents.some((ev) => ev.id === event.id)) {
          setMyEvents((prev) => prev.map((e) => (e.id === event.id ? event : e)));
        }
      }

      setColors((prev) => clearColorsForResource(prev, String(event.resource), event.start as Date));
      const day = new Date(event.start as Date);
      day.setHours(0, 0, 0, 0);
      setRedResources((prev) => ({ ...prev, [String(event.resource) + day.toISOString()]: false }));
      if (!conflicts.length && event.resource !== draggedEventResource) {
        const colorEnd = new Date(dayStart);
        colorEnd.setDate(colorEnd.getDate() + 1);
        colorEnd.setHours(6, 0, 0, 0);
        setColors((prev) => [
          ...prev,
          { start: dayStart, resource: String(args.oldEvent!.resource), background: '#ffdbd280', end: colorEnd },
        ]);
        setRedResources((prev) => ({ ...prev, [String(args.oldEvent!.resource) + day.toISOString()]: true }));
      }
      setDraggedEventStart(null);
      setDraggedEventEnd(null);
      setDraggedEventResource(null);
    },
    [myEvents, draggedEventStart, draggedEventEnd, draggedEventResource, clearColorsForResource],
  );

  const extendDefaultEvent = useCallback((args: { start: Date }): MbscCalendarEvent => {
    const start = new Date(args.start);
    const newStart = calculateStart(start);
    const newEnd = calculateEnd(start);
    const title = getTitle(newStart.getHours());
    const color = getColor(newStart.getHours());
    return {
      title: title,
      start: newStart,
      end: newEnd,
      color: color,
    };
  }, []);

  const customResource = useCallback(
    (res: MbscResource, day: Date) => {
      const style = redResources[res.id + day.toISOString()]
        ? { color: '#53000080', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
        : {};
      return <div style={style}>{res.name}</div>;
    },
    [redResources],
  );

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  return (
    <>
      <Eventcalendar
        cssClass="mds-24-hour-manufacturing-calendar"
        data={myEvents}
        colors={colors}
        dragToMove={true}
        dragToCreate={false}
        clickToCreate="single"
        dragToResize={false}
        dragTimeStep={480}
        dragTimeStepBase="viewStart"
        eventDelete={true}
        eventOverlap={false}
        extendDefaultEvent={extendDefaultEvent}
        onCellHoverIn={handleCellHoverIn}
        onCellHoverOut={handleCellHoverOut}
        onEventCreate={handleEventCreate}
        onEventClick={handleEventClick}
        onEventDelete={handleEventDelete}
        onEventDragStart={handleEventDragStart}
        onEventUpdateFailed={handleEventUpdateFailed}
        onEventUpdate={handleEventUpdate}
        groupBy="date"
        resources={resources}
        renderResource={customResource}
        view={view}
      />
      <Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
    </>
  );
};

export default App;
```

### CSS

```css
.mds-24-hour-manufacturing-calendar .mbsc-schedule-events {
  left: 0;
  right: 0;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-time-wrapper {
  height: 2.5em;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-item {
  height: 2.5em;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot {
  z-index: auto;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot .mbsc-schedule-color-text {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 35px;
  height: 35px;
  border: none;
  border-radius: 50%;
  background: linear-gradient(135deg, #33333380, #00000080);
  box-sizing: border-box;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot.mbsc-font-icon:before {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  top: 50%;
  left: 50%;
  color: #e0fff0;
  font-size: 12px;
  opacity: 0.8;
  transform: translate(-50%, -50%);
}
```

## Implementation notes

- Use `type: 'week'` with `allDay: false`, `startTime: '06:00'`, `endTime: '06:00+1'`, `timeCellStep: 120`, and `timeLabelStep: 120`. The `+1` suffix in `endTime` extends the view past midnight into the next calendar day, creating a continuous 24-hour column anchored at 06:00 per day.
- Define 3 resources (Crew A, B, C) and set `groupBy: 'date'` so all crews appear side by side under each day column.
- Define 3 shift objects each with `startHour`, `endHour`, `title`, and `color`: Morning (06-14, green), Afternoon (14-22, orange), Night (22-06 with `nextDay: true`, purple). Write a `getShiftByHour(hour)` helper that returns the matching shift for any given hour.
- Set `dragToMove: true`, `dragToCreate: false`, `dragToResize: false`, `clickToCreate: 'single'`, `eventDelete: true`, and `eventOverlap: false`. Set `dragTimeStep: 480` (8 hours in minutes) and `dragTimeStepBase: 'viewStart'` to snap all move operations to exact shift boundaries aligned to the 06:00 view start.
- Use `extendDefaultEvent` to snap click-created events to shift boundaries: call `getShiftByHour` on the clicked hour to determine the containing shift, then set `start`/`end` to that shift's boundaries (advancing the end date by one day for the Night shift), and derive `title` and `color` from the shift.
- In `onCellHoverIn`, determine the hovered shift via `getShiftByHour`, call `getAvailableSlots` to find which of the 3 shift slots are free for that resource and day, and if the hovered slot is free, push a color entry with a green background and a plus-icon `cssClass` to the `colors` array. Remove it in `onCellHoverOut`.
- In `onEventCreate`, define the day window as 06:00-06:00+1 and look for a conflict - another event on the same resource or at the same start hour. If found, show an "Already assigned" `Toast` and return `false`. On success, clear the hover-slot color for that resource/day and reset the red-resource flag.
- Store the original start, end, and resource in state via `onEventDragStart`. In `onEventUpdate`, first block cross-day moves (return `false` if the drop day's 06:00 boundary differs from the drag day's). Then find conflicts within the same day: if a conflicting event shares the same resource, reassign it to the dragged event's original resource; if it shares the same time slot, move it to the dragged event's original time and update its title and color. Return `false` if the exact drop position is already occupied. After resolving, update `colors` and the red-resource indicator for the source resource.
- In `onEventDelete`, remove the event from state, apply a red color overlay spanning 06:00-06:00+1 for that resource, set `redResources[resourceId + dayISO]` to `true`, and show a `Toast`.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) with a `(resource, day)` signature: apply a red background style to the crew name div when `redResources[res.id + day.toISOString()]` is `true` for that crew and day.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-react.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config react
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config react --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/react/getting-started/installation

## Related demos

- Previous: [Doctors Availability](https://demo.mobiscroll.com/react/scheduler/doctors-availability-calendar-planner.md) - Display per-doctor availability in React Scheduler — color-coded tracks mark open slots per doctor, automatically respecting working hours.
- Next: [Drop Assignee on Event](https://demo.mobiscroll.com/react/scheduler/drop-assignee-on-event-from-list.md) - Drag team members from an external list onto existing meetings in the React Scheduler — with visual drop feedback and conflict validation.
