---
title: "React Scheduler Drop Assignee on Event Example | Mobiscroll"
component: "Scheduler"
framework: "React"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/react/scheduler/drop-assignee-on-event-from-list"
---

# Drop Assignee on Event - React Scheduler demo

Drag team members from an external list onto existing meetings in the React Scheduler — with drop feedback and conflict validation. For React and Next.js.

- [Live demo](https://demo.mobiscroll.com/react/scheduler/drop-assignee-on-event-from-list)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/scheduler/drop-assignee-on-event-from-list)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/scheduler/drop-assignee-on-event-from-list)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/scheduler/drop-assignee-on-event-from-list.md), [jQuery](https://demo.mobiscroll.com/jquery/scheduler/drop-assignee-on-event-from-list.md), [Angular](https://demo.mobiscroll.com/angular/scheduler/drop-assignee-on-event-from-list.md), [Vue](https://demo.mobiscroll.com/vue/scheduler/drop-assignee-on-event-from-list.md)

## Description

Assigning people to meetings that are already booked across multiple rooms is a common scheduling challenge. Instead of dropping external items onto the calendar grid to create new events, the goal here is to drop them onto existing events, turning each meeting into an individual drop target where attendees can be assigned directly.

This is where the [`dropcontainer`](https://mobiscroll.com/docs/react/eventcalendar/drag-and-drop#dropcontainer) comes in. Each event rendered through the [`renderSchedulerEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderSchedulerEvent) function is initialized as a drop container, turning every meeting card into a valid drop zone. The external list of team members is set up using the draggable component, so each person can be grabbed and dropped onto any event on the scheduler. The [`onItemDrop`](https://mobiscroll.com/docs/react/eventcalendar/drag-and-drop#event-onItemDrop) event handles the assignment, while [`onItemDragEnter`](https://mobiscroll.com/docs/react/eventcalendar/drag-and-drop#event-onItemDragEnter) and [`onItemDragLeave`](https://mobiscroll.com/docs/react/eventcalendar/drag-and-drop#event-onItemDragLeave) provide real-time visual feedback.

Beyond simple assignment, the demo includes practical validation. Before accepting a drop, it checks whether the person is already assigned to the target meeting and whether they have a time conflict with another meeting in the same slot. Assignments can also be removed by clicking on an attendee chip, with an undo option.

**Looking for dropping external items onto the calendar grid instead?** [Check out the external drag & drop example →](https://demo.mobiscroll.com/react/scheduler/external-drag-drop-sortable-dragula)

## What this demo shows

- A weekly scheduler layout that displays multiple resources across the top, weekdays from Monday through Friday, an all-day row, and a scrollable time grid from 8:00 AM to 6:00 PM.
- **Scheduler layout** Each resource column contains sequential timed events, with default scheduler navigation controls above the calendar, including the month picker, previous and next arrows, and the Today button.
- **Events** Meetings are rendered as gray event cards with a colored stripe on the left and attendee chips inside the card showing the initials of assigned participants.
- **Team list** A separate list of team members is shown beside the scheduler, with each item displaying initials, name, and a status line that shows either no assignment or the meeting currently assigned to that person.
- **External drag and drop** Team members can be dragged from the side list and dropped onto existing meetings instead of onto empty scheduler cells.
- **Drop feedback** When dragging starts, meetings show a dashed outline and a placeholder message so each event becomes an obvious drop target.
- **Validation state** The hovered event indicates whether the drop is allowed, using green for a valid assignment and red when the assignment is rejected.
- **Assignment rules** The demo prevents assigning the same person to the same event more than once and blocks assignments when that person is already booked in an overlapping event.
- **Messages** Invalid drops show an error message, while successful drops show a confirmation message and add the person's initials to the event.
- **Chip actions** Assigned people can be removed directly from an event by clicking the close action on their chip.
- **Undo flow** Removing an assignment shows a snackbar with an Undo action, and restoring the assignment displays a confirmation toast.
- **Scrolling** The full view supports both vertical and horizontal scrolling.

## Best for

- **Room and team coordination** Scheduler setups where meetings are already scheduled and people need to be assigned to those meetings across multiple rooms or resources.
- **External resource assignment** Workflows that require dragging people, equipment, or other external items onto existing events instead of creating new events from the external list.
- **Conflict-aware planning** Scheduling interfaces where assignment conflicts, duplicate assignments, and overlapping bookings need immediate validation during drag and drop.
- **Operations dashboards** Resource planning views that benefit from a side list of assignable items and a scheduler that shows the impact of changes in context.

## 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
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`eventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventDelete) - option
- [`showEventTooltip`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-showEventTooltip) - option
- [`renderSchedulerEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderSchedulerEvent) - renderer
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/snackbar#event-onClose) - event

## Code

### JSX

```jsx
import { Draggable, Dropcontainer, Eventcalendar, Page, setOptions, Snackbar, Toast } from '@mobiscroll/react';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

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

const rooms = [
  { id: 1, name: 'Conference Room' },
  { id: 2, name: 'Board Room' },
  { id: 3, name: 'Meeting Room' },
  { id: 4, name: 'Training Room' },
];

const employees = [
  { id: 'emp1', name: 'Alice Martin', avatar: 'AM', color: '#e74c3c' },
  { id: 'emp2', name: 'Bob Johnson', avatar: 'BJ', color: '#3498db' },
  { id: 'emp3', name: 'Carol Smith', avatar: 'CS', color: '#2ecc71' },
  { id: 'emp4', name: 'David Lee', avatar: 'DL', color: '#f39c12' },
  { id: 'emp5', name: 'Eva Chen', avatar: 'EC', color: '#9b59b6' },
  { id: 'emp6', name: 'Frank Diaz', avatar: 'FD', color: '#1abc9c' },
  { id: 'emp7', name: 'Grace Kim', avatar: 'GK', color: '#e67e22' },
  { id: 'emp8', name: 'Henry Patel', avatar: 'HP', color: '#34495e' },
  { id: 'emp9', name: 'Ivy Torres', avatar: 'IT', color: '#e84393' },
];

function EmployeeItem({ employee, assignmentCount, onDragStart }) {
  const [el, setEl] = useState(null);

  const setElm = useCallback((elm) => {
    setEl(elm);
  }, []);

  return (
    <div className="mds-drop-on-events-employee-item mbsc-flex" ref={setElm} onPointerDown={onDragStart}>
      <div className="mds-drop-on-events-employee-avatar mbsc-flex" style={{ background: employee.color }}>
        {employee.avatar}
      </div>
      <div className="mds-drop-on-events-employee-info mbsc-flex">
        <div className="mds-drop-on-events-employee-name">{employee.name}</div>
        <div className="mds-drop-on-events-employee-count">
          {assignmentCount > 0 ? `${assignmentCount} meeting${assignmentCount > 1 ? 's' : ''}` : 'No assignments'}
        </div>
      </div>
      <Draggable dragData={employee} element={el} />
    </div>
  );
}

EmployeeItem.propTypes = {
  assignmentCount: PropTypes.number.isRequired,
  employee: PropTypes.object.isRequired,
  onDragStart: PropTypes.func.isRequired,
};

function SchedulerEvent({ data, findConflict, onAssign, onRemove, onToast }) {
  const [dropEl, setDropEl] = useState(null);
  const [dropState, setDropState] = useState('');

  const setDropElm = useCallback((elm) => {
    setDropEl(elm);
  }, []);

  const event = data.original;
  const attendees = useMemo(() => event.attendees || [], [event.attendees]);

  const handleItemDrop = useCallback(
    (args) => {
      const employee = args.data;
      setDropState('');

      if (attendees.some((a) => a.id === employee.id)) {
        onToast(`${employee.name} is already assigned`, 'danger');
        return;
      }

      const conflict = findConflict(employee.id, event.id);
      if (conflict) {
        onToast(`${employee.name} already has a ${conflict.title} on this timeslot`, 'danger');
        return;
      }

      onAssign(event.id, employee);
      onToast(`${employee.name} assigned to ${event.title}`, 'success');
    },
    [attendees, event, findConflict, onAssign, onToast],
  );

  const handleDragEnter = useCallback(
    (args) => {
      const employee = args.data;
      if (employee) {
        if (attendees.some((a) => a.id === employee.id)) {
          setDropState('mds-drop-conflict');
        } else {
          const conflict = findConflict(employee.id, event.id);
          setDropState(conflict ? 'mds-drop-conflict' : 'mds-drop-active');
        }
      } else {
        setDropState('mds-drop-active');
      }
    },
    [attendees, event, findConflict],
  );

  const handleDragLeave = useCallback(() => {
    setDropState('');
  }, []);

  return (
    <div
      ref={setDropElm}
      className={`mds-drop-on-events-custom-event mbsc-flex ${dropState}`}
      style={{ borderLeft: `4px solid ${event.color}` }}
    >
      <Dropcontainer element={dropEl} onItemDrop={handleItemDrop} onItemDragEnter={handleDragEnter} onItemDragLeave={handleDragLeave} />
      <div className="mds-drop-on-events-event-header mbsc-flex">
        <div className="mds-drop-on-events-event-title">{event.title}</div>
        <div className="mds-drop-on-events-event-time">
          {data.start} - {data.end}
        </div>
      </div>
      {attendees.length > 0 && (
        <div className="mds-drop-on-events-event-attendees mbsc-flex">
          {attendees.map((att) => (
            <span
              key={att.id}
              className="mds-drop-on-events-attendee-chip"
              style={{ background: att.color }}
              title={`${att.name} (click to remove)`}
              onClick={(e) => {
                e.stopPropagation();
                onRemove(event.id, att, event.title);
              }}
            >
              {att.avatar}
              <span className="mds-drop-on-events-attendee-remove">×</span>
            </span>
          ))}
        </div>
      )}
      <div className="mds-drop-on-events-event-drop-hint">Drop people to assign</div>
    </div>
  );
}

SchedulerEvent.propTypes = {
  data: PropTypes.object.isRequired,
  findConflict: PropTypes.func.isRequired,
  onAssign: PropTypes.func.isRequired,
  onRemove: PropTypes.func.isRequired,
  onToast: PropTypes.func.isRequired,
};

function App() {
  const [meetings, setMeetings] = useState(() => [
    {
      id: 'evt1',
      title: 'Sprint Planning',
      start: '2026-11-08T09:00',
      end: '2026-11-08T11:00',
      resource: 1,
      color: '#b52db9',
      attendees: [
        { id: 'emp1', name: 'Alice Martin', avatar: 'AM', color: '#e74c3c' },
        { id: 'emp2', name: 'Bob Johnson', avatar: 'BJ', color: '#3498db' },
      ],
    },
    {
      id: 'evt2',
      title: 'Budget Review',
      start: '2026-11-08T10:00',
      end: '2026-11-08T13:00',
      resource: 2,
      color: '#669ce2',
      attendees: [],
    },
    {
      id: 'evt3',
      title: 'Client Presentation',
      start: '2026-11-08T15:00',
      end: '2026-11-08T18:00',
      resource: 2,
      color: '#88bd42',
      attendees: [
        { id: 'emp3', name: 'Carol Smith', avatar: 'CS', color: '#2ecc71' },
        { id: 'emp4', name: 'David Lee', avatar: 'DL', color: '#f39c12' },
      ],
    },
    {
      id: 'evt4',
      title: 'Project Kickoff',
      start: '2026-11-08T09:00',
      end: '2026-11-08T11:00',
      resource: 3,
      color: '#b6962f',
      attendees: [],
    },
    {
      id: 'evt5',
      title: 'New Hire Orientation',
      start: '2026-11-08T13:00',
      end: '2026-11-08T16:00',
      resource: 4,
      color: '#c864f0',
      attendees: [],
    },
    {
      id: 'evt6',
      title: 'Design Review',
      start: '2026-11-08T13:00',
      end: '2026-11-08T15:00',
      resource: 1,
      color: '#c7682d',
      attendees: [],
    },
    {
      id: 'evt7',
      title: 'Product Demo',
      start: '2026-11-09T09:00',
      end: '2026-11-09T11:00',
      resource: 1,
      color: '#ad2b6c',
      attendees: [],
    },
    {
      id: 'evt8',
      title: 'Stakeholder Update',
      start: '2026-11-09T10:00',
      end: '2026-11-09T12:00',
      resource: 2,
      color: '#0f60ca',
      attendees: [],
    },
    {
      id: 'evt9',
      title: 'Code Review',
      start: '2026-11-09T10:00',
      end: '2026-11-09T12:00',
      resource: 3,
      color: '#56a823',
      attendees: [],
    },
    {
      id: 'evt10',
      title: 'Safety Training',
      start: '2026-11-09T13:00',
      end: '2026-11-09T16:00',
      resource: 4,
      color: '#aa36d8',
      attendees: [],
    },
    {
      id: 'evt11',
      title: 'Retrospective',
      start: '2026-11-10T09:00',
      end: '2026-11-10T11:00',
      resource: 1,
      color: '#c45f20',
      attendees: [],
    },
    {
      id: 'evt12',
      title: 'Board Briefing',
      start: '2026-11-10T13:00',
      end: '2026-11-10T16:00',
      resource: 2,
      color: '#1e58a5',
      attendees: [],
    },
    {
      id: 'evt13',
      title: 'Marketing Sync',
      start: '2026-11-10T10:00',
      end: '2026-11-10T12:00',
      resource: 3,
      color: '#549e27',
      attendees: [],
    },
    {
      id: 'evt14',
      title: 'API Workshop',
      start: '2026-11-10T13:00',
      end: '2026-11-10T16:00',
      resource: 4,
      color: '#7c1ca1',
      attendees: [],
    },
    {
      id: 'evt15',
      title: 'Architecture Review',
      start: '2026-11-11T09:00',
      end: '2026-11-11T11:30',
      resource: 1,
      color: '#a7511c',
      attendees: [],
    },
    {
      id: 'evt16',
      title: 'Quarterly Planning',
      start: '2026-11-11T13:00',
      end: '2026-11-11T16:00',
      resource: 2,
      color: '#13488d',
      attendees: [],
    },
    {
      id: 'evt17',
      title: 'Hiring Debrief',
      start: '2026-11-11T10:00',
      end: '2026-11-11T12:00',
      resource: 3,
      color: '#51ac19',
      attendees: [],
    },
    {
      id: 'evt18',
      title: 'Team Standup',
      start: '2026-11-07T09:00',
      end: '2026-11-07T11:00',
      resource: 1,
      color: '#e74c3c',
      attendees: [],
    },
    {
      id: 'evt19',
      title: 'Daily Scrum',
      start: '2026-11-07T10:00',
      end: '2026-11-07T13:00',
      resource: 2,
      color: '#3498db',
      attendees: [],
    },
    {
      id: 'evt20',
      title: 'Lunch Meeting',
      start: '2026-11-07T12:00',
      end: '2026-11-07T14:00',
      resource: 3,
      color: '#2ecc71',
      attendees: [],
    },
    {
      id: 'evt21',
      title: 'Weekly Sync',
      start: '2026-11-07T14:00',
      end: '2026-11-07T16:00',
      resource: 4,
      color: '#f39c12',
      attendees: [],
    },
    {
      id: 'evt22',
      title: 'Morning Briefing',
      start: '2026-11-06T09:00',
      end: '2026-11-06T11:00',
      resource: 1,
      color: '#9b59b6',
      attendees: [],
    },
    {
      id: 'evt23',
      title: 'Status Update',
      start: '2026-11-06T11:00',
      end: '2026-11-06T14:00',
      resource: 2,
      color: '#1abc9c',
      attendees: [],
    },
    {
      id: 'evt24',
      title: 'Client Call',
      start: '2026-11-06T13:00',
      end: '2026-11-06T15:00',
      resource: 3,
      color: '#e67e22',
      attendees: [],
    },
    {
      id: 'evt25',
      title: 'End of Day Review',
      start: '2026-11-06T16:00',
      end: '2026-11-06T18:00',
      resource: 4,
      color: '#34495e',
      attendees: [],
    },
  ]);

  const [isExternalDragging, setIsExternalDragging] = useState(false);
  const [toastMessage, setToastMessage] = useState('');
  const [toastColor, setToastColor] = useState('');
  const [isToastOpen, setIsToastOpen] = useState(false);
  const [snackbarMessage, setSnackbarMessage] = useState('');
  const [isSnackbarOpen, setIsSnackbarOpen] = useState(false);
  const [undoData, setUndoData] = useState(null);

  const meetingsRef = useRef(meetings);
  useEffect(() => {
    meetingsRef.current = meetings;
  }, [meetings]);

  const myView = useMemo(
    () => ({
      scheduler: {
        type: 'week',
        startDay: 1,
        endDay: 5,
        startTime: '08:00',
        endTime: '18:00',
        timeCellStep: 30,
        timeLabelStep: 30,
        virtualScroll: false,
      },
    }),
    [],
  );

  const findConflict = useCallback((empId, targetEventId) => {
    const allMeetings = meetingsRef.current;
    const target = allMeetings.find((m) => m.id === targetEventId);
    if (!target) return null;
    const targetStart = new Date(target.start).getTime();
    const targetEnd = new Date(target.end).getTime();
    for (const m of allMeetings) {
      if (m.id === targetEventId) continue;
      const attendees = m.attendees || [];
      if (!attendees.some((a) => a.id === empId)) continue;
      const mStart = new Date(m.start).getTime();
      const mEnd = new Date(m.end).getTime();
      if (mStart < targetEnd && mEnd > targetStart) return m;
    }
    return null;
  }, []);

  const handleAssign = useCallback((eventId, employee) => {
    setMeetings((prev) => prev.map((m) => (m.id === eventId ? { ...m, attendees: [...(m.attendees || []), employee] } : m)));
  }, []);

  const handleRemove = useCallback((eventId, employee, eventTitle) => {
    setMeetings((prev) =>
      prev.map((m) => (m.id === eventId ? { ...m, attendees: (m.attendees || []).filter((a) => a.id !== employee.id) } : m)),
    );
    setSnackbarMessage(`${employee.name} removed from ${eventTitle}`);
    setUndoData({ eventId, employee });
    setIsSnackbarOpen(true);
  }, []);

  const showToast = useCallback((message, color) => {
    setToastMessage(message);
    setToastColor(color);
    setIsToastOpen(true);
  }, []);

  const handleDragStart = useCallback(() => {
    const onMove = () => {
      setIsExternalDragging(true);
      document.removeEventListener('pointermove', onMove);
    };
    const onUp = () => {
      setIsExternalDragging(false);
      document.removeEventListener('pointermove', onMove);
      document.removeEventListener('pointerup', onUp);
    };
    document.addEventListener('pointermove', onMove);
    document.addEventListener('pointerup', onUp);
  }, []);

  const snackbarButton = useMemo(
    () => ({
      text: 'Undo',
      action: () => {
        if (!undoData) return;
        setMeetings((prev) =>
          prev.map((m) => (m.id === undoData.eventId ? { ...m, attendees: [...(m.attendees || []), undoData.employee] } : m)),
        );
        showToast('Assignment restored', 'success');
      },
    }),
    [undoData, showToast],
  );

  const renderEvent = useCallback(
    (data) => (
      <SchedulerEvent data={data} findConflict={findConflict} onAssign={handleAssign} onRemove={handleRemove} onToast={showToast} />
    ),
    [findConflict, handleAssign, handleRemove, showToast],
  );

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

  const handleSnackbarClose = useCallback(() => {
    setIsSnackbarOpen(false);
  }, []);

  return (
    <Page className={`mds-drop-on-events${isExternalDragging ? ' mds-drop-on-events-external-dragging' : ''}`}>
      <div className="mbsc-grid mbsc-no-padding">
        <div className="mbsc-row">
          <div className="mbsc-col-sm-3 mbsc-flex-col mds-drop-on-events-sidebar">
            <div className="mbsc-form-group-title">Team Members</div>
            <div className="mds-drop-on-events-employee-list mbsc-flex">
              {employees.map((emp) => (
                <EmployeeItem
                  key={emp.id}
                  employee={emp}
                  assignmentCount={meetings.filter((m) => (m.attendees || []).some((a) => a.id === emp.id)).length}
                  onDragStart={handleDragStart}
                />
              ))}
            </div>
          </div>
          <div className="mbsc-col-sm-9 mds-drop-on-events-calendar-wrapper">
            <Eventcalendar
              view={myView}
              data={meetings}
              resources={rooms}
              dragToCreate={false}
              dragToMove={false}
              dragToResize={false}
              clickToCreate={false}
              eventDelete={false}
              showEventTooltip={false}
              renderSchedulerEvent={renderEvent}
            />
          </div>
        </div>
      </div>
      <Toast message={toastMessage} color={toastColor} isOpen={isToastOpen} onClose={handleToastClose} />
      <Snackbar message={snackbarMessage} isOpen={isSnackbarOpen} onClose={handleSnackbarClose} button={snackbarButton} />
    </Page>
  );
}

export default App;
```

### TSX

```tsx
import {
  Draggable,
  Dropcontainer,
  Eventcalendar,
  MbscCalendarEvent,
  MbscCalendarEventData,
  MbscEventcalendarView,
  MbscItemDragEvent,
  Page,
  setOptions,
  Snackbar,
  Toast,
} from '@mobiscroll/react';
import { FC, RefObject, useCallback, useMemo, useRef, useState } from 'react';

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

interface Attendee {
  id: string;
  name: string;
  avatar: string;
  color: string;
}

interface Meeting extends MbscCalendarEvent {
  attendees: Attendee[];
}

const rooms = [
  { id: 1, name: 'Conference Room' },
  { id: 2, name: 'Board Room' },
  { id: 3, name: 'Meeting Room' },
  { id: 4, name: 'Training Room' },
];

const employees: Attendee[] = [
  { id: 'emp1', name: 'Alice Martin', avatar: 'AM', color: '#e74c3c' },
  { id: 'emp2', name: 'Bob Johnson', avatar: 'BJ', color: '#3498db' },
  { id: 'emp3', name: 'Carol Smith', avatar: 'CS', color: '#2ecc71' },
  { id: 'emp4', name: 'David Lee', avatar: 'DL', color: '#f39c12' },
  { id: 'emp5', name: 'Eva Chen', avatar: 'EC', color: '#9b59b6' },
  { id: 'emp6', name: 'Frank Diaz', avatar: 'FD', color: '#1abc9c' },
  { id: 'emp7', name: 'Grace Kim', avatar: 'GK', color: '#e67e22' },
  { id: 'emp8', name: 'Henry Patel', avatar: 'HP', color: '#34495e' },
  { id: 'emp9', name: 'Ivy Torres', avatar: 'IT', color: '#e84393' },
];

const initialMeetings: Meeting[] = [
  {
    id: 'evt1',
    title: 'Sprint Planning',
    start: '2026-11-08T09:00',
    end: '2026-11-08T11:00',
    resource: 1,
    color: '#b52db9',
    attendees: [
      { id: 'emp1', name: 'Alice Martin', avatar: 'AM', color: '#e74c3c' },
      { id: 'emp2', name: 'Bob Johnson', avatar: 'BJ', color: '#3498db' },
    ],
  },
  {
    id: 'evt2',
    title: 'Budget Review',
    start: '2026-11-08T10:00',
    end: '2026-11-08T13:00',
    resource: 2,
    color: '#669ce2',
    attendees: [],
  },
  {
    id: 'evt3',
    title: 'Client Presentation',
    start: '2026-11-08T15:00',
    end: '2026-11-08T18:00',
    resource: 2,
    color: '#88bd42',
    attendees: [
      { id: 'emp3', name: 'Carol Smith', avatar: 'CS', color: '#2ecc71' },
      { id: 'emp4', name: 'David Lee', avatar: 'DL', color: '#f39c12' },
    ],
  },
  {
    id: 'evt4',
    title: 'Project Kickoff',
    start: '2026-11-08T09:00',
    end: '2026-11-08T11:00',
    resource: 3,
    color: '#b6962f',
    attendees: [],
  },
  {
    id: 'evt5',
    title: 'New Hire Orientation',
    start: '2026-11-08T13:00',
    end: '2026-11-08T16:00',
    resource: 4,
    color: '#c864f0',
    attendees: [],
  },
  {
    id: 'evt6',
    title: 'Design Review',
    start: '2026-11-08T13:00',
    end: '2026-11-08T15:00',
    resource: 1,
    color: '#c7682d',
    attendees: [],
  },
  {
    id: 'evt7',
    title: 'Product Demo',
    start: '2026-11-09T09:00',
    end: '2026-11-09T11:00',
    resource: 1,
    color: '#ad2b6c',
    attendees: [],
  },
  {
    id: 'evt8',
    title: 'Stakeholder Update',
    start: '2026-11-09T10:00',
    end: '2026-11-09T12:00',
    resource: 2,
    color: '#0f60ca',
    attendees: [],
  },
  {
    id: 'evt9',
    title: 'Code Review',
    start: '2026-11-09T10:00',
    end: '2026-11-09T12:00',
    resource: 3,
    color: '#56a823',
    attendees: [],
  },
  {
    id: 'evt10',
    title: 'Safety Training',
    start: '2026-11-09T13:00',
    end: '2026-11-09T16:00',
    resource: 4,
    color: '#aa36d8',
    attendees: [],
  },
  {
    id: 'evt11',
    title: 'Retrospective',
    start: '2026-11-10T09:00',
    end: '2026-11-10T11:00',
    resource: 1,
    color: '#c45f20',
    attendees: [],
  },
  {
    id: 'evt12',
    title: 'Board Briefing',
    start: '2026-11-10T13:00',
    end: '2026-11-10T16:00',
    resource: 2,
    color: '#1e58a5',
    attendees: [],
  },
  {
    id: 'evt13',
    title: 'Marketing Sync',
    start: '2026-11-10T10:00',
    end: '2026-11-10T12:00',
    resource: 3,
    color: '#549e27',
    attendees: [],
  },
  {
    id: 'evt14',
    title: 'API Workshop',
    start: '2026-11-10T13:00',
    end: '2026-11-10T16:00',
    resource: 4,
    color: '#7c1ca1',
    attendees: [],
  },
  {
    id: 'evt15',
    title: 'Architecture Review',
    start: '2026-11-11T09:00',
    end: '2026-11-11T11:30',
    resource: 1,
    color: '#a7511c',
    attendees: [],
  },
  {
    id: 'evt16',
    title: 'Quarterly Planning',
    start: '2026-11-11T13:00',
    end: '2026-11-11T16:00',
    resource: 2,
    color: '#13488d',
    attendees: [],
  },
  {
    id: 'evt17',
    title: 'Hiring Debrief',
    start: '2026-11-11T10:00',
    end: '2026-11-11T12:00',
    resource: 3,
    color: '#51ac19',
    attendees: [],
  },
  {
    id: 'evt18',
    title: 'Team Standup',
    start: '2026-11-07T09:00',
    end: '2026-11-07T11:00',
    resource: 1,
    color: '#e74c3c',
    attendees: [],
  },
  {
    id: 'evt19',
    title: 'Daily Scrum',
    start: '2026-11-07T10:00',
    end: '2026-11-07T13:00',
    resource: 2,
    color: '#3498db',
    attendees: [],
  },
  {
    id: 'evt20',
    title: 'Lunch Meeting',
    start: '2026-11-07T12:00',
    end: '2026-11-07T14:00',
    resource: 3,
    color: '#2ecc71',
    attendees: [],
  },
  {
    id: 'evt21',
    title: 'Weekly Sync',
    start: '2026-11-07T14:00',
    end: '2026-11-07T16:00',
    resource: 4,
    color: '#f39c12',
    attendees: [],
  },
  {
    id: 'evt22',
    title: 'Morning Briefing',
    start: '2026-11-06T09:00',
    end: '2026-11-06T11:00',
    resource: 1,
    color: '#9b59b6',
    attendees: [],
  },
  {
    id: 'evt23',
    title: 'Status Update',
    start: '2026-11-06T11:00',
    end: '2026-11-06T14:00',
    resource: 2,
    color: '#1abc9c',
    attendees: [],
  },
  {
    id: 'evt24',
    title: 'Client Call',
    start: '2026-11-06T13:00',
    end: '2026-11-06T15:00',
    resource: 3,
    color: '#e67e22',
    attendees: [],
  },
  {
    id: 'evt25',
    title: 'End of Day Review',
    start: '2026-11-06T16:00',
    end: '2026-11-06T18:00',
    resource: 4,
    color: '#34495e',
    attendees: [],
  },
];

interface EmployeeItemProps {
  employee: Attendee;
  count: number;
  onPointerDown: () => void;
}

function EmployeeItem({ employee, count, onPointerDown }: EmployeeItemProps) {
  const [el, setEl] = useState<HTMLDivElement | null>(null);

  return (
    <div ref={setEl} className="mds-drop-on-events-employee-item mbsc-flex" id={'mds-emp-' + employee.id} onPointerDown={onPointerDown}>
      <div className="mds-drop-on-events-employee-avatar mbsc-flex" style={{ background: employee.color }}>
        {employee.avatar}
      </div>
      <div className="mds-drop-on-events-employee-info mbsc-flex">
        <div className="mds-drop-on-events-employee-name">{employee.name}</div>
        <div className="mds-drop-on-events-employee-count">
          {count > 0 ? count + ' meeting' + (count > 1 ? 's' : '') : 'No assignments'}
        </div>
      </div>
      <Draggable element={el} dragData={employee} />
    </div>
  );
}

interface SchedulerEventProps {
  data: MbscCalendarEventData;
  meetingsRef: RefObject<Meeting[]>;
  onDrop: (e: MbscItemDragEvent, eventId: string) => void;
  onFindConflict: (empId: string, targetEventId: string) => Meeting | null;
  onRemoveAttendee: (eventId: string, empId: string) => void;
}

function SchedulerEvent({ data, meetingsRef, onDrop, onFindConflict, onRemoveAttendee }: SchedulerEventProps) {
  const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
  const [dropClass, setDropClass] = useState('');
  const event = data.original as Meeting;
  const attendees: Attendee[] = event.attendees || [];

  const handleDrop = useCallback(
    (e: MbscItemDragEvent) => {
      setDropClass('');
      onDrop(e, event.id as string);
    },
    [onDrop, event.id],
  );

  const handleDragEnter = useCallback(
    (e: MbscItemDragEvent) => {
      const emp = e.data as Attendee;
      const meeting = meetingsRef.current.find((m) => m.id === event.id);
      if (!meeting) {
        setDropClass('mds-drop-active');
        return;
      }
      if (emp) {
        if (meeting.attendees.some((a) => a.id === emp.id) || onFindConflict(emp.id, event.id as string)) {
          setDropClass('mds-drop-conflict');
        } else {
          setDropClass('mds-drop-active');
        }
      } else {
        setDropClass('mds-drop-active');
      }
    },
    [event.id, meetingsRef, onFindConflict],
  );

  const handleDragLeave = useCallback(() => {
    setDropClass('');
  }, []);

  return (
    <div
      ref={setContainerEl}
      className={'mds-drop-on-events-custom-event mbsc-flex ' + dropClass}
      style={{ borderLeft: '4px solid ' + event.color }}
    >
      <Dropcontainer element={containerEl} onItemDrop={handleDrop} onItemDragEnter={handleDragEnter} onItemDragLeave={handleDragLeave}>
        <div className="mds-drop-on-events-event-header mbsc-flex">
          <div className="mds-drop-on-events-event-title">{event.title as string}</div>
          <div className="mds-drop-on-events-event-time">
            {data.start} - {data.end}
          </div>
        </div>
        {attendees.length > 0 && (
          <div className="mds-drop-on-events-event-attendees mbsc-flex">
            {attendees.map((att) => (
              <span
                key={att.id}
                className="mds-drop-on-events-attendee-chip"
                style={{ background: att.color }}
                title={att.name + ' (click to remove)'}
                onClick={(e) => {
                  e.stopPropagation();
                  onRemoveAttendee(event.id as string, att.id);
                }}
              >
                {att.avatar}
                <span className="mds-drop-on-events-attendee-remove">×</span>
              </span>
            ))}
          </div>
        )}
        <div className="mds-drop-on-events-event-drop-hint">Drop people to assign</div>
      </Dropcontainer>
    </div>
  );
}

const App: FC = () => {
  const meetingsRef = useRef<Meeting[]>(initialMeetings);
  const [myEvents, setMyEvents] = useState<Meeting[]>(initialMeetings);
  const [isExternalDragging, setIsExternalDragging] = useState(false);

  const [toastMessage, setToastMessage] = useState('');
  const [toastColor, setToastColor] = useState<'success' | 'danger'>('success');
  const [isToastOpen, setIsToastOpen] = useState(false);

  const [snackbarMessage, setSnackbarMessage] = useState('');
  const [snackbarButton, setSnackbarButton] = useState<{ text: string; action: () => void } | undefined>(undefined);
  const [isSnackbarOpen, setIsSnackbarOpen] = useState(false);

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      scheduler: {
        type: 'week',
        startDay: 1,
        endDay: 5,
        startTime: '08:00',
        endTime: '18:00',
        timeCellStep: 30,
        timeLabelStep: 30,
        virtualScroll: false,
      },
    }),
    [],
  );

  const getAssignmentCount = useCallback(
    (empId: string): number => myEvents.filter((m) => m.attendees.some((a) => a.id === empId)).length,
    [myEvents],
  );

  const findConflict = useCallback((empId: string, targetEventId: string): Meeting | null => {
    const target = meetingsRef.current.find((m) => m.id === targetEventId);
    if (!target) return null;
    const tStart = new Date(target.start as string).getTime();
    const tEnd = new Date(target.end as string).getTime();
    for (const m of meetingsRef.current) {
      if (m.id === targetEventId || !m.attendees.some((a) => a.id === empId)) continue;
      const mStart = new Date(m.start as string).getTime();
      const mEnd = new Date(m.end as string).getTime();
      if (mStart < tEnd && mEnd > tStart) return m;
    }
    return null;
  }, []);

  const handleDrop = useCallback(
    (dropEvent: MbscItemDragEvent, eventId: string) => {
      const employee = dropEvent.data as Attendee;
      const meetingIdx = meetingsRef.current.findIndex((m) => m.id === eventId);
      if (meetingIdx === -1) return;
      const meeting = meetingsRef.current[meetingIdx];

      if (meeting.attendees.some((a) => a.id === employee.id)) {
        setToastMessage(employee.name + ' is already assigned');
        setToastColor('danger');
        setIsToastOpen(true);
        return;
      }

      const conflict = findConflict(employee.id, eventId);
      if (conflict) {
        setToastMessage(employee.name + ' already has a ' + conflict.title + ' on this timeslot');
        setToastColor('danger');
        setIsToastOpen(true);
        return;
      }

      const updatedMeeting: Meeting = {
        ...meeting,
        attendees: [...meeting.attendees, { id: employee.id, name: employee.name, avatar: employee.avatar, color: employee.color }],
      };
      meetingsRef.current = [...meetingsRef.current.slice(0, meetingIdx), updatedMeeting, ...meetingsRef.current.slice(meetingIdx + 1)];
      setMyEvents(meetingsRef.current);

      setToastMessage(employee.name + ' assigned to ' + (meeting.title as string));
      setToastColor('success');
      setIsToastOpen(true);
    },
    [findConflict],
  );

  const handleRemoveAttendee = useCallback((eventId: string, empId: string) => {
    const meetingIdx = meetingsRef.current.findIndex((m) => m.id === eventId);
    if (meetingIdx === -1) return;
    const meeting = meetingsRef.current[meetingIdx];
    const idx = meeting.attendees.findIndex((a) => a.id === empId);
    if (idx === -1) return;

    const removedAtt = meeting.attendees[idx];
    const updatedMeeting: Meeting = {
      ...meeting,
      attendees: [...meeting.attendees.slice(0, idx), ...meeting.attendees.slice(idx + 1)],
    };
    meetingsRef.current = [...meetingsRef.current.slice(0, meetingIdx), updatedMeeting, ...meetingsRef.current.slice(meetingIdx + 1)];
    setMyEvents(meetingsRef.current);

    setSnackbarMessage(removedAtt.name + ' removed from ' + (meeting.title as string));
    setSnackbarButton({
      text: 'Undo',
      action: () => {
        const mIdx = meetingsRef.current.findIndex((m) => m.id === eventId);
        if (mIdx === -1) return;
        const m = meetingsRef.current[mIdx];
        const restored: Meeting = { ...m, attendees: [...m.attendees.slice(0, idx), removedAtt, ...m.attendees.slice(idx)] };
        meetingsRef.current = [...meetingsRef.current.slice(0, mIdx), restored, ...meetingsRef.current.slice(mIdx + 1)];
        setMyEvents(meetingsRef.current);
        setIsSnackbarOpen(false);
        setToastMessage('Assignment restored');
        setToastColor('success');
        setIsToastOpen(true);
      },
    });
    setIsSnackbarOpen(true);
  }, []);

  const handleEmployeePointerDown = useCallback(() => {
    const onMove = () => {
      setIsExternalDragging(true);
      document.removeEventListener('pointermove', onMove);
    };
    const onUp = () => {
      setIsExternalDragging(false);
      document.removeEventListener('pointermove', onMove);
      document.removeEventListener('pointerup', onUp);
    };
    document.addEventListener('pointermove', onMove);
    document.addEventListener('pointerup', onUp);
  }, []);

  const renderSchedulerEvent = useCallback(
    (data: MbscCalendarEventData) => (
      <SchedulerEvent
        data={data}
        meetingsRef={meetingsRef}
        onDrop={handleDrop}
        onFindConflict={findConflict}
        onRemoveAttendee={handleRemoveAttendee}
      />
    ),
    [handleDrop, findConflict, handleRemoveAttendee],
  );

  return (
    <Page className={'mds-drop-on-events' + (isExternalDragging ? ' mds-drop-on-events-external-dragging' : '')}>
      <div className="mbsc-grid mbsc-no-padding">
        <div className="mbsc-row">
          <div className="mbsc-col-sm-3 mbsc-flex-col mds-drop-on-events-sidebar">
            <div className="mbsc-form-group-title">Team Members</div>
            <div className="mds-drop-on-events-employee-list mbsc-flex">
              {employees.map((emp) => (
                <EmployeeItem key={emp.id} employee={emp} count={getAssignmentCount(emp.id)} onPointerDown={handleEmployeePointerDown} />
              ))}
            </div>
          </div>
          <div className="mbsc-col-sm-9 mds-drop-on-events-calendar-wrapper">
            <Eventcalendar
              view={myView}
              data={myEvents}
              resources={rooms}
              dragToCreate={false}
              dragToMove={false}
              dragToResize={false}
              clickToCreate={false}
              eventDelete={false}
              showEventTooltip={false}
              renderSchedulerEvent={renderSchedulerEvent}
            />
          </div>
        </div>
      </div>
      <Toast message={toastMessage} color={toastColor} isOpen={isToastOpen} onClose={() => setIsToastOpen(false)} />
      <Snackbar message={snackbarMessage} button={snackbarButton} isOpen={isSnackbarOpen} onClose={() => setIsSnackbarOpen(false)} />
    </Page>
  );
};

export default App;
```

### CSS

```css
.mds-drop-on-events,
.mds-drop-on-events .mbsc-grid,
.mds-drop-on-events .mbsc-row {
  height: 100%;
}
.mds-drop-on-events-sidebar {
  overflow-y: auto;
}
.mds-drop-on-events-employee-list {
  padding: 8px;
  flex-direction: column;
  gap: 4px;
}
.mds-drop-on-events-employee-item {
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  cursor: grab;
  user-select: none;
  background: rgba(128, 128, 128, 0.5);
  margin: 0 5px 5px 5px;
  touch-action: none;
  transition:
    background 0.15s,
    box-shadow 0.2s,
    transform 0.15s;
}
.mds-drop-on-events-employee-item:hover {
  background: rgba(128, 128, 128, 0.4);
  transform: translateY(-1px);
}
.mds-drop-on-events-employee-item:active {
  transform: translateY(0);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.mds-drop-on-events-employee-avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  flex-shrink: 0;
  letter-spacing: 0.5px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.mds-drop-on-events-employee-info {
  flex-direction: column;
  overflow: hidden;
}
.mds-drop-on-events-employee-name {
  font-size: 15px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mds-drop-on-events-employee-count {
  font-size: 13px;
  opacity: 0.55;
}
.mds-drop-on-events-calendar-wrapper {
  border-left: 1px solid rgba(0, 0, 0, 0.1);
}
/* Drag clone is appended to body, outside the root — keep unscoped */
.mds-drop-on-events-employee-item.mbsc-drag-clone {
  opacity: 0.8;
}
.mds-drop-on-events-custom-event {
  background: #cccccc;
  border-radius: 6px;
  padding: 6px 8px;
  height: 100%;
  box-sizing: border-box;
  flex-direction: column;
  gap: 4px;
  overflow: hidden;
  outline: 2px solid transparent;
  transition: background 0.15s;
  position: relative;
}
.mds-drop-on-events-event-header {
  flex-direction: column;
  gap: 1px;
}
.mds-drop-on-events-event-title {
  font-size: 14px;
  font-weight: 600;
  color: #181818;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mds-drop-on-events-event-time {
  font-size: 12px;
  color: #545454;
}
.mds-drop-on-events-event-attendees {
  flex-wrap: wrap;
  gap: 3px;
}
/* Drop hint - hidden by default, shown only during external drag */
.mds-drop-on-events-event-drop-hint {
  display: none;
  font-size: 11px;
  font-style: italic;
  color: #686868;
}
/* Show drop hints and dashed borders on events during external drag */
.mds-drop-on-events-external-dragging .mds-drop-on-events-event-drop-hint {
  display: block;
}
.mds-drop-on-events-external-dragging .mds-drop-on-events-custom-event {
  outline: 2px dashed #b9b9b9;
}
.mds-drop-on-events-attendee-chip {
  display: flex;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 9px;
  font-weight: 700;
  border: 2px solid rgba(255, 255, 255, 0.5);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  cursor: pointer;
  position: relative;
}
.mds-drop-on-events-attendee-remove {
  display: none;
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  line-height: 1;
}
.mds-drop-on-events-attendee-chip:hover .mds-drop-on-events-attendee-remove {
  display: flex;
}
.mds-drop-on-events-custom-event.mds-drop-active {
  cursor: copy;
  outline: 2px solid rgba(54, 133, 43, 0.8);
  background: rgba(180, 223, 173, 0.8);
}
.mds-drop-on-events-custom-event.mds-drop-conflict {
  cursor: not-allowed;
  outline: 2px solid rgba(145, 34, 34, 0.8);
  background: rgba(235, 194, 194, 0.8);
}
.mds-drop-on-events .mbsc-scheduler-event {
  min-height: 80px;
}
.mds-drop-on-events .mbsc-schedule-event-inner {
  height: 100%;
}
```

## Implementation notes

- Configure the scheduler with `type: 'week'`, `startDay: 1`, `endDay: 5`, `startTime: '08:00'`, `endTime: '18:00'`, and a list of room resources.
- Use `renderSchedulerEvent` (Angular: `schedulerEventTemplate`, Vue: `#schedulerEvent` slot) to render each event as a `Dropcontainer`; display current assignees as initials chips inside the card, and apply a dashed-outline and placeholder-text CSS state to all event cards while a `Draggable` drag is active.
- Build the team sidebar as a list of `Draggable` items, each showing initials, name, and a status line with a count of current meeting assignments or a "No assignments" label.
- In `onItemDragEnter` on each event's `Dropcontainer`, check for a duplicate assignment on that event and for a time conflict with any other event the person is already in; apply a green or red card highlight. Clear the highlight in `onItemDragLeave`.
- In `onItemDrop`, re-run the same validation: if invalid, show an error toast; if valid, add the person to the event's attendee list, show a confirmation toast, and update their sidebar assignment status.
- Make each attendee chip removable via a close icon; on removal show a `Snackbar` with an Undo action, and on undo restore the assignment and show a confirmation toast.

## 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: [Rota Planning](https://demo.mobiscroll.com/react/scheduler/24-hour-manufacturing-shift-rota-planning.md) - Build a 24-hour rotating crew rota with the React Scheduler — three crews, three shifts, and automatic conflict resolution when shifts move.
