---
title: "React Timeline External Resource Drag & Drop Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/resource-management-with-dynamic-team-creation"
---

# External Resource Drag & Drop - React Timeline demo

Drag external resources onto a React Timeline to add team members, or drag them off to remove them — build crew scheduling and team management. For React and Next.JS.

- [Live demo](https://demo.mobiscroll.com/react/timeline/resource-management-with-dynamic-team-creation)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/timeline/resource-management-with-dynamic-team-creation)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/timeline/resource-management-with-dynamic-team-creation)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/resource-management-with-dynamic-team-creation.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/resource-management-with-dynamic-team-creation.md), [Angular](https://demo.mobiscroll.com/angular/timeline/resource-management-with-dynamic-team-creation.md), [Vue](https://demo.mobiscroll.com/vue/timeline/resource-management-with-dynamic-team-creation.md)

## Description

By combining the `resourceReorder` feature with external resource drag-and-drop, you can easily organize, group, and adjust resources by dragging them into or out of the timeline view.

**Dragging resources onto the timeline**

Resources can be created by dragging and dropping an external element onto the timeline. In order for that to work you will need to have two things set up:

- Enable the timeline to receive external resources by setting [`externalResourceDrop`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalResourceDrop) to `true`.
- Initialize the external resources (containers) as `draggable` components.

Use the `Draggable` component to specify a skeleton event through its `dragData` option and reference the draggable container in the `element` option. The `dragData` accepts a full event definition that will be added to the event calendar on drop. If omitted, a default event will be created.

Use the [`onResourceCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceCreate) for triggering a custom logic on drop like showing a toast.

**Dragging resources off the timeline)**

Additionally resource can be removed by dragging them out from the timeline and dropping them onto an external drop container. To activate this, you will need to:

- Enable the timeline to allow dragging events out by setting [`externalResourceDrag`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalResourceDrag) to `true`.
- Initialize the

[`dropcontainer`](https://mobiscroll.com/docs/react/eventcalendar/drag-and-drop#dropcontainer).

Resources can also be dropped on another timeline with `externalResourceDrop` enabled.

When a resource is dropped into an external drop container or another timeline, it will be deleted from the original calendar and the [`onResourceDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceDelete) will be fired.

You can also use the [`onResourceDragLeave`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceDragLeave) and [`onResourceDragEnter`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceDragEnter), and the drop container's `onItemDragEnter` and `onItemDragLeave` events to provide visual feedback or running custom logic during drag.

**Looking for external event drag & drop?** [Check out this example →](https://demo.mobiscroll.com/react/timeline/external-drag-drop-sortable-dragula)

## What this demo shows

- A desktop day timeline for dynamic resource management, with the selected day arranged horizontally and a hierarchical installer team resource tree arranged vertically on the left.
- **External technician list** An external list beside the timeline shows available technicians that can be dragged into installer teams.
- **Header navigation** The month and year label opens date navigation, while the previous and next arrows and the Today button move between days or return to the current day.
- **Day strip** The fixed strip below the header shows the selected day using the `DD DDD MMM YYYY` date format, with the current date highlighted.
- **Time grid** The timeline shows working hours from 7 AM to 5 PM with hourly columns.
- **Resources** The resource column shows expandable and collapsible installer teams with technician child resources.
- **Resource rendering** Technician rows display avatar initials, the technician name, and the job title or specialization.
- **Adding teams** The resource header includes an Add team action for creating new empty installer teams.
- **Empty teams** Empty teams display a "Drag Technicians here" placeholder to indicate valid drop targets.
- **Team assignment** Technicians can be dragged from the external list into installer teams.
- **Team reorganization** Technicians can be dragged between teams to update team composition.
- **Drag-and-drop feedback** Drop targets are highlighted while a technician is being dragged.
- **Confirmation feedback** A toast notification confirms when a technician is assigned to a team.
- **Current time** A vertical blue line with a time label marks the current time.
- **Hover behavior** Hovering over the time grid shows a time indicator that follows the cursor in 15-minute increments.
- **Event cards** Events appear as colored cards with a left color stripe, job name, and exact start and end time.
- **Event positioning** Events are placed by assigned resource, date, start time, and end time.
- **Event interaction** Events highlight on hover and expose drag and resize handles for moving events or changing their duration.
- **Event selection** Clicking an event selects and highlights it.
- **Scrolling** Horizontal and vertical scrolling support larger technician lists, multiple installer teams, and extended schedules.

## Best for

- **Construction workforce planning** Organizing installers into teams and assigning scheduled work from the same timeline interface.
- **Field service dispatching** Managing technician availability, team assignments, and time-based jobs in a resource timeline.
- **Installer team management** Creating empty teams, filling them by drag and drop, and reorganizing team composition as plans change.
- **Crew scheduling** Coordinating multiple teams and technicians across a single workday with visible job timing.
- **Operational scheduling tools** Building scheduling interfaces where resource grouping, external resource drag and drop, and timeline-based job placement need to work together.

## 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
- [`navigateToEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#method-navigateToEvent) - method
- [`dragBetweenResources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragBetweenResources) - option
- [`externalResourceDrop`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalResourceDrop) - option
- [`externalResourceDrag`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalResourceDrag) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`renderResourceHeader`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResourceHeader) - renderer
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`onResourceCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceCreate) - event
- [`onResourceDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceDelete) - event
- [`onResourceOrderUpdate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceOrderUpdate) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

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

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

function Installer(props) {
  const [draggable, setDraggable] = useState();

  const setDragElm = useCallback((elm) => {
    setDraggable(elm);
  }, []);

  const resource = props.data;

  return (
    <div className="mds-ext-res-item mbsc-font" ref={setDragElm}>
      <div className="mbsc-flex">
        <div className="mds-ext-res-avatar" style={{ background: resource.color }}>
          {resource.name[0]}
        </div>
        <div className="mds-ext-res-cont">
          <div className="mds-ext-res-name">{resource.name}</div>
          <div className="mds-ext-res-title">{resource.title}</div>
        </div>
        <Draggable dragData={resource} element={draggable} type="resource" />
      </div>
    </div>
  );
}

Installer.propTypes = {
  data: PropTypes.object.isRequired,
};

function App() {
  const [dropCont, setDropCont] = useState();
  const [shouldScroll, setShouldScroll] = useState(0);
  const [toastMessage, setToastMessage] = useState('');
  const [isToastOpen, setToastOpen] = useState(false);

  const timelineRef = useRef();
  const installerRef = useRef();

  const [installers, setInstallers] = useState([
    {
      id: 'it-1',
      name: 'Installer team 1',
      eventCreation: false,
      reorder: false,
      children: [
        {
          id: 1,
          name: 'Emily Carter',
          color: '#007acc',
          title: 'Electrician',
        },
        {
          id: 2,
          name: 'Michael Lawson',
          color: '#008000',
          title: 'Plumber',
        },
      ],
    },
    // Showing partial data. Download full source.
  ]);

  const [availableInstallers, setAvailableInstallers] = useState([
    {
      id: 9,
      name: 'Adam Miller',
      color: '#C70039',
      title: 'Mason',
    },
    {
      id: 10,
      name: 'Isabella Martinez',
      color: '#2ECC71',
      title: 'Surveyor',
    },
    {
      id: 11,
      name: 'Mark White',
      color: '#34495E',
      title: 'Glazier',
    },
    // Showing partial data. Download full source.
  ]);

  const tasks = useMemo(
    () => [
      {
        id: 1,
        start: '2026-10-27T07:00',
        end: '2026-10-27T13:00',
        title: 'Electrical wiring installation',
        resource: 1,
      },
      {
        id: 2,
        start: '2026-10-27T14:00',
        end: '2026-10-27T18:00',
        title: 'Power panel connection',
        resource: 1,
      },
      {
        id: 3,
        start: '2026-10-27T08:00',
        end: '2026-10-27T12:00',
        title: 'Water pipe fitting',
        resource: 2,
      },
      // Showing partial data. Download full source.
    ],
    [],
  );

  const myView = useMemo(
    () => ({
      timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' },
    }),
    [],
  );

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

  const handleResourceCreate = useCallback(
    (args) => {
      setAvailableInstallers(availableInstallers.filter((installer) => installer.id !== args.resource.id));
      setToastMessage(args.resource.name + ' added to ' + args.parent.name);
      setToastOpen(true);
    },
    [availableInstallers],
  );

  const handleResourceDelete = useCallback((args) => {
    setToastMessage(args.resource.name + ' removed from ' + args.parent.name);
    setToastOpen(true);
  }, []);

  const handleResourceOrderUpdate = useCallback((args) => {
    const parent = args.parent;
    const oldParent = args.oldParent;

    if (parent && oldParent) {
      setToastMessage(args.resource.name + ' moved to ' + args.parent.name);
      setToastOpen(true);
    }

    if (parent && parent.children) {
      // Remove placeholder resource
      parent.children = parent.children.filter((child) => !child.placeholder);
    }

    if (oldParent && !oldParent.children.length) {
      // Add placeholder resource
      oldParent.children.push({
        id: 'ph-' + oldParent.id,
        name: 'Drag Technicians here',
        placeholder: true,
        reorder: false,
      });
    }
  }, []);

  const handleItemDrop = useCallback(
    (args) => {
      if (args.data && args.dataType === 'resource') {
        setAvailableInstallers([...availableInstallers, args.data]);
        setShouldScroll((prevScroll) => prevScroll + 1);
      }
    },
    [availableInstallers],
  );

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

  const addNewTeam = useCallback(() => {
    const teamLength = installers.length + 1;
    const resourceId = 'it-' + teamLength;
    setInstallers([
      ...installers,
      {
        id: resourceId,
        eventCreation: false,
        reorder: false,
        name: 'Installer team ' + teamLength,
        children: [
          {
            id: 'ph-' + resourceId,
            name: 'Drag Technicians here',
            placeholder: true,
            reorder: false,
          },
        ],
      },
    ]);

    if (timelineRef && timelineRef.current) {
      timelineRef.current.navigateToEvent({
        start: new Date(),
        resource: resourceId,
      });
    }
  }, [installers]);

  const customResourceHeader = useCallback(
    () => (
      <div className="mbsc-flex mbsc-align-items-center">
        <div className="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
        <Button variant="outline" className="mds-ext-res-add" onClick={addNewTeam}>
          Add team
        </Button>
      </div>
    ),
    [addNewTeam],
  );

  const customResource = useCallback(
    (resource) =>
      resource.isParent || resource.placeholder ? (
        <div className={'mds-ext-res-name' + (resource.placeholder ? ' mds-ext-res-name-ph' : '')}>{resource.name}</div>
      ) : (
        <div className="mbsc-flex">
          <div className="mds-ext-res-avatar" style={{ background: resource.color }}>
            {resource.name[0]}
          </div>
          <div className="mds-ext-res-cont">
            <div className="mds-ext-res-name">{resource.name}</div>
            <div className="mds-ext-res-title">{resource.title}</div>
          </div>
        </div>
      ),
    [],
  );

  useEffect(() => {
    if (shouldScroll && installerRef.current) {
      installerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
  }, [installerRef, shouldScroll]);

  return (
    <Page className="mds-ext-res-drop">
      <div className="mbsc-grid mbsc-no-padding">
        <div className="mbsc-row">
          <div className="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont" ref={setDropElm}>
            <Dropcontainer onItemDrop={handleItemDrop} element={dropCont}>
              <div className="mds-ext-res-header">Available technicians</div>
              <div className="mds-ext-res-list">
                {availableInstallers.map((installer) => (
                  <div key={installer.id} ref={installerRef}>
                    <Installer data={installer} />
                  </div>
                ))}
              </div>
            </Dropcontainer>
          </div>
          <div className="mbsc-col-sm-9 mds-ext-res-drop-calendar">
            <Eventcalendar
              ref={timelineRef}
              data={tasks}
              dragBetweenResources={false}
              externalResourceDrop={true}
              externalResourceDrag={true}
              resources={installers}
              view={myView}
              renderResourceHeader={customResourceHeader}
              renderResource={customResource}
              onResourceCreate={handleResourceCreate}
              onResourceDelete={handleResourceDelete}
              onResourceOrderUpdate={handleResourceOrderUpdate}
            />
            <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
          </div>
        </div>
      </div>
    </Page>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  Draggable,
  Dropcontainer,
  Eventcalendar,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscItemDragEvent,
  MbscResource,
  MbscResourceCreateEvent,
  MbscResourceDeleteEvent,
  MbscResourceOrderEvent,
  Page,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

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

function Installer(props: { data: MbscResource }) {
  const [draggable, setDraggable] = useState<HTMLDivElement>();

  const setDragElm = useCallback((elm: HTMLDivElement) => {
    setDraggable(elm);
  }, []);

  const resource = props.data;

  return (
    <div className="mds-ext-res-item mbsc-font" ref={setDragElm}>
      <div className="mbsc-flex">
        <div className="mds-ext-res-avatar" style={{ background: resource.color }}>
          {resource.name && resource.name[0]}
        </div>
        <div className="mds-ext-res-cont">
          <div className="mds-ext-res-name">{resource.name}</div>
          <div className="mds-ext-res-title">{resource.title}</div>
        </div>
        <Draggable dragData={resource} element={draggable} type="resource" />
      </div>
    </div>
  );
}

function App() {
  const [dropCont, setDropCont] = useState<HTMLDivElement>();
  const [shouldScroll, setShouldScroll] = useState(0);
  const [toastMessage, setToastMessage] = useState('');
  const [isToastOpen, setToastOpen] = useState(false);

  const timelineRef = useRef<Eventcalendar>(null);
  const installerRef = useRef<HTMLDivElement>(null);

  const [installers, setInstallers] = useState<MbscResource[]>([
    {
      id: 'it-1',
      name: 'Installer team 1',
      eventCreation: false,
      reorder: false,
      children: [
        {
          id: 1,
          name: 'Emily Carter',
          color: '#007acc',
          title: 'Electrician',
        },
        {
          id: 2,
          name: 'Michael Lawson',
          color: '#008000',
          title: 'Plumber',
        },
      ],
    },
    // Showing partial data. Download full source.
  ]);

  const [availableInstallers, setAvailableInstallers] = useState<MbscResource[]>([
    {
      id: 9,
      name: 'Adam Miller',
      color: '#C70039',
      title: 'Mason',
    },
    {
      id: 10,
      name: 'Isabella Martinez',
      color: '#2ECC71',
      title: 'Surveyor',
    },
    {
      id: 11,
      name: 'Mark White',
      color: '#34495E',
      title: 'Glazier',
    },
    // Showing partial data. Download full source.
  ]);

  const tasks = useMemo<MbscCalendarEvent[]>(
    () => [
      {
        id: 1,
        start: '2026-10-27T07:00',
        end: '2026-10-27T13:00',
        title: 'Electrical wiring installation',
        resource: 1,
      },
      {
        id: 2,
        start: '2026-10-27T14:00',
        end: '2026-10-27T18:00',
        title: 'Power panel connection',
        resource: 1,
      },
      {
        id: 3,
        start: '2026-10-27T08:00',
        end: '2026-10-27T12:00',
        title: 'Water pipe fitting',
        resource: 2,
      },
      // Showing partial data. Download full source.
    ],
    [],
  );

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' },
    }),
    [],
  );

  const setDropElm = useCallback((elm: HTMLDivElement) => {
    setDropCont(elm);
  }, []);

  const handleResourceCreate = useCallback(
    (args: MbscResourceCreateEvent) => {
      setAvailableInstallers(availableInstallers.filter((installer) => installer.id !== args.resource.id));
      setToastMessage(args.resource.name + ' added to ' + args.parent?.name);
      setToastOpen(true);
    },
    [availableInstallers],
  );

  const handleResourceDelete = useCallback((args: MbscResourceDeleteEvent) => {
    setToastMessage(args.resource.name + ' removed from ' + args.parent?.name);
    setToastOpen(true);
  }, []);

  const handleResourceOrderUpdate = useCallback((args: MbscResourceOrderEvent) => {
    const parent = args.parent;
    const oldParent = args.oldParent;

    if (parent && oldParent) {
      setToastMessage(args.resource.name + ' moved to ' + args.parent?.name);
      setToastOpen(true);
    }

    if (parent && parent.children) {
      // Remove placeholder resource
      parent.children = parent.children.filter((child) => !child.placeholder);
    }

    if (oldParent && oldParent.children && !oldParent.children.length) {
      // Add placeholder resource
      oldParent.children.push({
        id: 'ph-' + oldParent.id,
        name: 'Drag Technicians here',
        placeholder: true,
        reorder: false,
      });
    }
  }, []);

  const handleItemDrop = useCallback(
    (args: MbscItemDragEvent<MbscResource>) => {
      if (args.data && args.dataType === 'resource') {
        setAvailableInstallers([...availableInstallers, args.data]);
        setShouldScroll((prevScroll) => prevScroll + 1);
      }
    },
    [availableInstallers],
  );

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

  const addNewTeam = useCallback(() => {
    const teamLength = installers.length + 1;
    const resourceId = 'it-' + teamLength;
    setInstallers([
      ...installers,
      {
        id: resourceId,
        eventCreation: false,
        reorder: false,
        name: 'Installer team ' + teamLength,
        children: [
          {
            id: 'ph-' + resourceId,
            name: 'Drag Technicians here',
            placeholder: true,
            reorder: false,
          },
        ],
      },
    ]);

    if (timelineRef && timelineRef.current) {
      timelineRef.current.navigateToEvent({
        start: new Date(),
        resource: resourceId,
      });
    }
  }, [installers]);

  const customResourceHeader = useCallback(
    () => (
      <div className="mbsc-flex mbsc-align-items-center">
        <div className="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
        <Button variant="outline" className="mds-ext-res-add" onClick={addNewTeam}>
          Add team
        </Button>
      </div>
    ),
    [addNewTeam],
  );

  const customResource = useCallback(
    (resource: MbscResource) =>
      resource.isParent || resource.placeholder ? (
        <div className={'mds-ext-res-name' + (resource.placeholder ? ' mds-ext-res-name-ph' : '')}>{resource.name}</div>
      ) : (
        <div className="mbsc-flex">
          <div className="mds-ext-res-avatar" style={{ background: resource.color }}>
            {resource.name && resource.name[0]}
          </div>
          <div className="mds-ext-res-cont">
            <div className="mds-ext-res-name">{resource.name}</div>
            <div className="mds-ext-res-title">{resource.title}</div>
          </div>
        </div>
      ),
    [],
  );

  useEffect(() => {
    if (shouldScroll && installerRef.current) {
      installerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
  }, [installerRef, shouldScroll]);

  return (
    <Page className="mds-ext-res-drop">
      <div className="mbsc-grid mbsc-no-padding">
        <div className="mbsc-row">
          <div className="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont" ref={setDropElm}>
            <Dropcontainer onItemDrop={handleItemDrop} element={dropCont}>
              <div className="mds-ext-res-header">Available technicians</div>
              <div className="mds-ext-res-list">
                {availableInstallers.map((installer) => (
                  <div key={installer.id} ref={installerRef}>
                    <Installer data={installer} />
                  </div>
                ))}
              </div>
            </Dropcontainer>
          </div>
          <div className="mbsc-col-sm-9 mds-ext-res-drop-calendar">
            <Eventcalendar
              ref={timelineRef}
              data={tasks}
              dragBetweenResources={false}
              externalResourceDrop={true}
              externalResourceDrag={true}
              resources={installers}
              view={myView}
              renderResourceHeader={customResourceHeader}
              renderResource={customResource}
              onResourceCreate={handleResourceCreate}
              onResourceDelete={handleResourceDelete}
              onResourceOrderUpdate={handleResourceOrderUpdate}
            />
            <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
          </div>
        </div>
      </div>
    </Page>
  );
}

export default App;
```

### CSS

```css
.mds-ext-res-drop-calendar {
  border-left: 1px solid #ccc;
}

.mds-ext-res-drop-calendar .mbsc-timeline-row {
  height: 50px;
}

.mds-ext-res-drop-calendar .mbsc-timeline-row-gutter {
  height: 8px;
}

.mds-ext-res-drop-calendar .mbsc-timeline-resource-col {
  width: 250px;
}

.mds-ext-res-drop-calendar .mbsc-timeline-parent {
  height: 32px;
}

.mds-ext-res-drop-calendar .mbsc-timeline-resource {
  align-items: center;
  align-content: center;
}

.mds-ext-res-drop-calendar .mds-ext-res-add {
  margin: 0;
}

.mds-ext-res-header {
  font-weight: 600;
  font-size: 0.875em;
  line-height: 38px;
  text-transform: uppercase;
  opacity: 0.6;
}

.mds-ext-res-name-ph {
  font-style: italic;
  opacity: 0.4;
}

.mds-ext-res-drop-cont .mds-ext-res-header {
  padding: 12px 16px;
  line-height: 1.5em;
}

.mds-ext-res-list {
  overflow: auto;
}

.mds-ext-res-item {
  background: rgba(128, 128, 128, 0.6);
  padding: 10px;
  margin: 16px;
  border-radius: 8px;
}

.mds-ext-res-item:first-child {
  margin-top: 0;
}

.mds-ext-res-cont {
  padding: 0 7px;
}

.mds-ext-res-name {
  font-size: 14px;
  font-weight: 600;
}

.mds-ext-res-title {
  padding-top: 2px;
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
}

.mds-ext-res-avatar {
  width: 36px;
  min-width: 36px;
  height: 36px;
  line-height: 36px;
  border-radius: 18px;
  text-align: center;
  color: #fff;
  font-size: 22px;
  font-weight: 400;
}

.mds-ext-res-drop,
.mds-ext-res-drop .mbsc-grid,
.mds-ext-res-drop .mbsc-row,
.mds-ext-res-drop-cont,
.mds-ext-res-drop-calendar {
  height: 100%;
}
```

## Implementation notes

- Use `type: 'day'` with `startTime: '07:00'`/`endTime: '18:00'` and `resourceReorder: true` in the view config to allow drag-reordering of resources within the timeline.
- Set `externalResourceDrop: true` to allow resources dragged from the sidebar to be added to the timeline, and `externalResourceDrag: true` to allow timeline resources to be dragged back out. Set `dragBetweenResources: false` to prevent events from being reassigned across team members.
- Define the timeline resources as a two-level tree of team groups (`eventCreation: false`, `reorder: false`) with installer leaf nodes (custom `name`, `color`, `title`). When a team has no members, give it a single placeholder child (`placeholder: true`, `reorder: false`) with a descriptive name like "Drag Technicians here".
- Build a separate `availableInstallers` state for the sidebar list. Render each available installer as an `Installer` card component that uses a ref callback to obtain its DOM element, then mounts a `Draggable` with `dragData` set to the resource object and `type: 'resource'`.
- Wrap the sidebar list in a `Dropcontainer` (with its DOM element passed via a ref callback to the `element` prop). In `onItemDrop`, check that `args.dataType === 'resource'` and add `args.data` back to `availableInstallers`. After adding, call `scrollIntoView` on the last card's ref to bring the returned installer into view.
- In `onResourceCreate`, remove the dropped installer from `availableInstallers` by ID, and show a `Toast` announcing that the person was added to `args.parent.name`.
- In `onResourceDelete`, show a `Toast` announcing the removal from `args.parent.name`.
- In `onResourceOrderUpdate`, show a `Toast` when `args.parent` and `args.oldParent` both exist (indicating a cross-team move). Remove the placeholder child from the destination team and, if the source team is now empty, push a new placeholder child onto its `children` array.
- Use `renderResourceHeader` (Angular: `resourceHeaderTemplate`, Vue: `#resourceHeader` slot) to render a "Set up teams" label alongside an "Add team" `Button`.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to handle three cases: parent/group nodes render just the team name; placeholder nodes render a styled placeholder label; leaf nodes render a colored avatar (first letter of the name), the installer's name, and their job title.
- In the Add team handler, append a new group to the installers array with a placeholder child, then call `navigateToEvent` on the calendar instance ref with the new resource ID to scroll the timeline to the newly added team.

## 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: [Resource Search and Filtering](https://demo.mobiscroll.com/react/timeline/resource-filtering-search.md) - Add live search and filter to a React Timeline resource header — multi-select by status or group, with a custom empty state and reset.
- Next: [Event Grouping & Bulk Actions](https://demo.mobiscroll.com/react/timeline/event-grouping-bulk-action.md) - Group React Timeline events into collapsible client/quarter blocks and bulk-drag groups — switch between assignee and work-type views on a full-year layout.
