React Scheduler Drop Assignee on Event

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 comes in. Each event rendered through the 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 event handles the assignment, while onItemDragEnter and 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 →
Get started with this example

Demo

Loading the live demo

Preview in full screen

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

Code

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({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
});

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;

Duplicate Session

You have signed in on a different machine. To keep everything in sync, you have been signed out of this session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.

Duplicate Session

We noticed you are logged in on a different machine and have closed your old session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.