---
title: "React Timeline Dual Synchronized Flight Scheduling Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/react/timeline/flight-scheduling-two-synchronized-timelines"
---

# Dual Synchronized Flight Scheduling - React Timeline demo

Use synchronized React Timeline views to build a flight scheduling UI — drag flights to assign them to aircraft, with fixed event times and conflict detection. For React and Next.js.

- [Live demo](https://demo.mobiscroll.com/react/timeline/flight-scheduling-two-synchronized-timelines)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/timeline/flight-scheduling-two-synchronized-timelines)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/timeline/flight-scheduling-two-synchronized-timelines)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/timeline/flight-scheduling-two-synchronized-timelines.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/flight-scheduling-two-synchronized-timelines.md), [Angular](https://demo.mobiscroll.com/angular/timeline/flight-scheduling-two-synchronized-timelines.md), [Vue](https://demo.mobiscroll.com/vue/timeline/flight-scheduling-two-synchronized-timelines.md)

## Description

For creating complex dashboards like for managing jet bookings sometimes two separate timeline instances are necessary.

Sticking two instances together with synchronized horizontal scrolling allows for a setup where the top timeline is a list of bookings with one flight per rows and the bottom timeline holds available aircrafts.

Only moving events from the top timeline is allowed and events are fixed in time, meaning the start and end cannot be changed. Things like `eventOverlap` is set to `false` to avoid overlapping events.

## What this demo shows

- Two stacked daily timeline views are synchronized horizontally so flights can be planned across two resource types on the same 24-hour time scale.
- **Header navigation** The month and year label in the top left opens date navigation, while the blue previous and next arrows and the Today button on the right make it easy to move between days and jump back to the current day.
- **Date strip** The 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 uses hourly columns across a 24-hour day.
- **Top timeline** The first timeline shows flight events against pilot resources.
- **Bottom timeline** The second timeline hides its own day and hour header while keeping the aircraft resources visible.
- **Aircraft resources** The bottom timeline uses expandable and collapsible resource groups with aircraft resources inside each group.
- **Custom resource rendering** Aircraft rows show an image, aircraft name, aircraft number, and a passenger-capacity indicator in the bottom-right corner.
- **Event cards** Events are rendered as blue cards with a left accent stripe, the flight route in bold, and the exact start and end time below the title.
- **Event positioning** Events are positioned by assigned resource and by start and end time.
- **Event movement** Events keep their original time range but can be moved between resources.
- **Cross-timeline drag and drop** Flights can be dragged from the first timeline to the second timeline to assign them to aircraft.
- **Scheduled state** After a flight is dropped onto the aircraft timeline, the event changes to green and a confirmation toast appears at the bottom center.
- **Drop restrictions** Scheduled flights cannot be dragged back to the first timeline.
- **Overlap prevention** Flights cannot overlap on the same aircraft resource during the same time period.
- **Conflict feedback** When a conflicting drop is attempted, the affected resource row shows a striped background and a toast message indicates the conflict.
- **Event interaction** Events are highlighted on hover.
- **Event selection** Clicking an event selects and highlights it.
- **Vertical scrolling** The resource area scrolls vertically so more resources can be displayed within each timeline.

## Best for

- **Airline operations** Planning flights across crew and aircraft resources on a shared time scale.
- **Flight scheduling** Assigning scheduled flights to available aircraft without changing their time range.
- **Crew management** Reviewing pilot-related flight assignments alongside aircraft availability.
- **Fleet planning** Managing aircraft availability, capacity, and scheduling conflicts in a timeline layout.
- **Transportation scheduling** Coordinating different resource types where assignments need to move between planning stages.
- **Conflict-aware planning** Preventing overlapping assignments on the same resource while giving immediate visual feedback.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-cssClass) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`eventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventDelete) - option
- [`externalDrag`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalDrag) - option
- [`onPageLoading`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onPageLoading) - event
- [`onPageLoaded`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onPageLoaded) - event
- [`onDestroy`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onDestroy) - event
- [`invalid`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-invalid) - option
- [`showControls`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-showControls) - option
- [`dragInTime`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragInTime) - option
- [`externalDrop`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalDrop) - option
- [`eventOverlap`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventOverlap) - option
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`selectedDate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-selectedDate) - option
- [`onSelectedDateChange`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onSelectedDateChange) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventCreateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreateFailed) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdateFailed) - event
- [`onEventDragStart`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDragStart) - event
- [`onEventDragEnd`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDragEnd) - event
- [`display`](https://mobiscroll.com/docs/react/notifications/toast#opt-display) - option
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

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

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

const bookings = [
  {
    start: '2026-11-04T06:00',
    end: '2026-11-04T10:00',
    title: 'Budapest - Ljubljana',
    resource: 1,
  },
  {
    start: '2026-11-04T15:00',
    end: '2026-11-04T18:00',
    title: 'Ljubljana - Berlin',
    resource: 1,
  },
  {
    start: '2026-11-04T04:00',
    end: '2026-11-04T12:00',
    title: 'Los Angeles - Dublin',
    resource: 2,
  },
  {
    start: '2026-11-04T18:00',
    end: '2026-11-04T23:00',
    title: 'Dublin - Bucharest',
    resource: 2,
  },
  {
    start: '2026-11-04T06:00',
    end: '2026-11-04T14:00',
    title: 'Chicago - Amsterdam',
    resource: 3,
  },
  {
    start: '2026-11-04T17:00',
    end: '2026-11-04T22:00',
    title: 'Amsterdam - Cairo',
    resource: 3,
  },
  {
    start: '2026-11-04T10:00',
    end: '2026-11-04T14:00',
    title: 'Hong Kong - Sydney',
    resource: 4,
  },
  {
    start: '2026-11-04T15:00',
    end: '2026-11-04T21:00',
    title: 'Sydney - Tokyo',
    resource: 4,
  },
  {
    start: '2026-11-04T04:00',
    end: '2026-11-04T12:00',
    title: 'Paris - Washington, D.C.',
    resource: 5,
  },
  {
    start: '2026-11-04T12:00',
    end: '2026-11-04T18:00',
    title: 'Washington, D.C. - Los-Angeles',
    resource: 5,
  },
  {
    start: '2026-11-04T03:00',
    end: '2026-11-04T11:00',
    title: 'Los Angeles - Dublin',
    resource: 6,
  },
  {
    start: '2026-11-04T13:00',
    end: '2026-11-04T18:00',
    title: 'Dublin - Rome',
    resource: 6,
  },
  {
    start: '2026-11-04T05:00',
    end: '2026-11-04T12:00',
    title: 'Barcelona - Dubai',
    resource: 7,
  },
  {
    start: '2026-11-04T13:00',
    end: '2026-11-04T20:00',
    title: 'Dubai - Tokyo',
    resource: 7,
  },
  {
    start: '2026-11-04T03:30',
    end: '2026-11-04T13:00',
    title: 'Rome - Toronto',
    resource: 8,
  },
  {
    start: '2026-11-04T16:00',
    end: '2026-11-04T19:00',
    title: 'Toronto - New-York',
    resource: 8,
  },
  {
    start: '2026-11-04T03:00',
    end: '2026-11-04T14:00',
    title: 'Vienna - Shanghai',
    resource: 9,
  },
  {
    start: '2026-11-04T16:00',
    end: '2026-11-04T23:30',
    title: 'Shanghai - Moscow',
    resource: 9,
  },
  {
    start: '2026-11-04T08:00',
    end: '2026-11-04T13:00',
    title: 'London - Cairo',
    resource: 10,
  },
  {
    start: '2026-11-04T15:30',
    end: '2026-11-04T19:30',
    title: 'Cairo - Sofia',
    resource: 10,
  },
  {
    start: '2026-11-04T02:00',
    end: '2026-11-04T13:00',
    title: 'Istanbul - New York',
    resource: 11,
  },
  {
    start: '2026-11-04T16:00',
    end: '2026-11-04T20:00',
    title: 'New York - Montreal',
    resource: 11,
  },
  {
    start: '2026-11-04T05:00',
    end: '2026-11-04T11:00',
    title: 'Seattle - Miami',
    resource: 12,
  },
  {
    start: '2026-11-04T12:00',
    end: '2026-11-04T21:00',
    title: 'Miami - Buenos-Aires',
    resource: 12,
  },
  {
    start: '2026-11-05T04:00',
    end: '2026-11-05T16:00',
    title: 'Hong Kong - Sydney',
    resource: 1,
  },
  {
    start: '2026-11-05T06:00',
    end: '2026-11-05T10:00',
    title: 'Los Angeles - Dublin',
    resource: 2,
  },
  {
    start: '2026-11-05T03:00',
    end: '2026-11-05T11:00',
    title: 'Budapest - Ljubljana',
    resource: 4,
  },
  {
    start: '2026-11-05T05:00',
    end: '2026-11-05T12:00',
    title: 'Los Angeles - Dublin',
    resource: 6,
  },
  {
    start: '2026-11-05T13:00',
    end: '2026-11-05T23:00',
    title: 'London - Cairo',
    resource: 7,
  },
  {
    start: '2026-11-05T03:00',
    end: '2026-11-05T14:00',
    title: 'Miami - Buenos-Aires',
    resource: 9,
  },
  {
    start: '2026-11-05T06:00',
    end: '2026-11-05T12:00',
    title: 'Barcelona - Dubai',
    resource: 10,
  },
  {
    start: '2026-11-05T08:00',
    end: '2026-11-05T22:00',
    title: 'Istanbul - New York',
    resource: 11,
  },
  {
    start: '2026-11-05T05:00',
    end: '2026-11-05T11:00',
    title: 'Vienna - Shanghai',
    resource: 12,
  },
];

const jets = [
  {
    id: 'Op 1',
    name: 'Prestige Air',
    eventCreation: false,
    children: [
      {
        id: 1,
        code: '#AF 7703',
        crew: 52,
        name: 'Red Bolt',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-1.jpg',
      },
      {
        id: 2,
        code: '#BQ 4718',
        crew: 47,
        name: 'Skyroar',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-2.jpg',
      },
      {
        id: 3,
        code: '#ZM 8430',
        crew: 24,
        name: 'Agile Raven',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-3.jpg',
      },
    ],
  },
  {
    id: 'Op 2',
    name: 'Elite Wings',
    eventCreation: false,
    children: [
      {
        id: 4,
        code: '#XG 5500',
        crew: 43,
        name: 'Monsterbolt',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-4.jpg',
      },
      {
        id: 5,
        code: '#FL 2531',
        crew: 22,
        name: 'Airrise',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-5.jpg',
      },
      {
        id: 6,
        code: '#PA 6487',
        crew: 84,
        name: 'Starblast',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-6.jpg',
      },
      {
        id: 7,
        code: '#PP 9812',
        crew: 28,
        name: 'Ebonfire',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-7.jpg',
      },
    ],
  },
  {
    id: 'Op 3',
    name: 'Luxury Skies',
    eventCreation: false,
    children: [
      {
        id: 8,
        code: '#DN 3191',
        crew: 36,
        name: 'Dark Bee',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-8.jpg',
      },
      {
        id: 9,
        code: '#ZW 2328',
        crew: 76,
        name: 'Keen Sparrow',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-9.jpg',
      },
      {
        id: 10,
        code: '#RX 9898',
        crew: 37,
        name: 'Jagged Bolt',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-10.jpg',
      },
    ],
  },
];

function App() {
  const reservations = useMemo(
    () => [
      {
        id: 1,
        name: 'Alison Reyes',
      },
      {
        id: 2,
        name: 'Shauna Perry',
      },
      {
        id: 3,
        name: 'Jan Whitney',
      },
      {
        id: 4,
        name: 'Freddie Durham',
      },
      {
        id: 5,
        name: 'William Dillon',
      },
      {
        id: 6,
        name: 'Tyrell Edwards',
      },
      {
        id: 7,
        name: 'Caitlyn Riddle',
      },
      {
        id: 8,
        name: 'Liam Mays',
      },
      {
        id: 9,
        name: 'Frank Medina',
      },
      {
        id: 10,
        name: 'Calvin Larsen',
      },
      {
        id: 11,
        name: 'Heather Walsh',
      },
      {
        id: 12,
        name: 'Conner Paul',
      },
    ],
    [],
  );

  const [flights, setFlights] = useState([
    {
      start: '2026-11-04T10:00',
      end: '2026-11-04T17:00',
      title: 'Barcelona - Dubai',
      resource: 4,
    },
    {
      start: '2026-11-04T07:30',
      end: '2026-11-04T16:00',
      title: 'Toronto - Rome',
      resource: 8,
    },
    {
      start: '2026-11-04T04:00',
      end: '2026-11-04T08:00',
      title: 'Ljubljana - Budapest',
      resource: 1,
    },
    {
      start: '2026-11-04T02:00',
      end: '2026-11-04T10:00',
      title: 'Paris - Washington, D.C.',
      resource: 2,
    },
    {
      start: '2026-11-04T12:00',
      end: '2026-11-04T23:00',
      title: 'New York - Istanbul',
      resource: 7,
    },
    {
      start: '2026-11-04T15:00',
      end: '2026-11-04T23:00',
      title: 'Moscow - Tokyo',
      resource: 3,
    },
  ]);

  const [invalid, setInvalid] = useState();
  const firstCalCont = useRef(null);
  const secondCalCont = useRef(null);
  const skipScroll1 = useRef(false);
  const skipScroll2 = useRef(false);
  const [mySelectedDate, setSelectedDate] = useState(new Date());
  const [isToastOpen, setToastOpen] = useState(false);
  const [toastText, setToastText] = useState();

  const myView = useMemo(
    () => ({
      timeline: {
        type: 'day',
        size: 1,
      },
    }),
    [],
  );

  const renderMyResource = useCallback((resource) => {
    if (!resource.children) {
      return (
        <div className="mbsc-flex mbsc-align-items-center mbsc-justify-content-start">
          <img className="md-drag-drop-bw-inst-avatar" src={resource.img} alt="Avatar" />
          <div className="md-drag-drop-bw-inst-details mbsc-flex-col mbsc-flex-1-0">
            <div>{resource.name}</div>
            <div className="md-aircraft-code mbsc-flex mbsc-justify-content-between">
              <div>{resource.code}</div>
              <div>
                {'\u{1F9D1}\u{1F3FC}\u{200D}\u{2708}\u{FE0F}'} {resource.crew}
              </div>
            </div>
          </div>
        </div>
      );
    }
    return <div>{resource.name}</div>;
  }, []);

  const handleScroll1 = useCallback((ev) => {
    if (secondCalCont.current && !skipScroll2.current) {
      skipScroll1.current = true;
      secondCalCont.current.scrollLeft = ev.target.scrollLeft;
    }
    skipScroll1.current = false;
  }, []);

  const handleScroll2 = useCallback((ev) => {
    if (firstCalCont.current && !skipScroll1.current) {
      skipScroll2.current = true;
      firstCalCont.current.scrollLeft = ev.target.scrollLeft;
    }
    skipScroll2.current = false;
  }, []);

  const handleFirstScroll = useCallback(() => {
    if (!firstCalCont.current) {
      firstCalCont.current = document.querySelector('.md-drag-drop-bw-inst-first .mbsc-timeline-grid-scroll');
      firstCalCont.current.addEventListener('scroll', handleScroll1);
    }
  }, [handleScroll1]);

  const handleSecondScroll = useCallback(() => {
    if (!secondCalCont.current) {
      secondCalCont.current = document.querySelector('.md-drag-drop-bw-inst-second .mbsc-timeline-grid-scroll');
      secondCalCont.current.addEventListener('scroll', handleScroll2);
    }
  }, [handleScroll2]);

  const detachFirstScroll = useCallback(() => {
    if (firstCalCont.current) {
      firstCalCont.current.removeEventListener('scroll', handleScroll1);
    }
  }, [handleScroll1]);

  const detachSecondScroll = useCallback(() => {
    if (secondCalCont.current) {
      secondCalCont.current.removeEventListener('scroll', handleScroll2);
    }
  }, [handleScroll2]);

  const handlePageLoading = useCallback((args) => {
    if (secondCalCont.current) {
      setTimeout(() => {
        setSelectedDate(args.firstDay);
      }, 100);
    }
  }, []);

  const handleSelectedDateChange = useCallback((args) => {
    setSelectedDate(args.date);
  }, []);

  const handleEventCreated = useCallback((args) => {
    setFlights((current) => [...current, args.event]);
    setToastText('Flight scheduled');
    setToastOpen(true);
  }, []);

  const handleEventDeleted = useCallback((args) => {
    setFlights((current) => [...current.filter((flight) => flight.id !== args.event.id)]);
  }, []);

  const handleEventCreateUpdateFailed = useCallback(() => {
    setToastText("There's already a flight on this date");
    setToastOpen(true);
  }, []);

  const handleEventDragStart = useCallback(
    (args) => {
      const unavailable = {};
      const invalid = {
        cssClass: 'md-drag-drop-invalid',
        recurring: { repeat: 'daily' },
        resource: [],
      };
      // Filter unavailable jets
      flights.forEach((flight) => {
        if (
          flight.id !== args.event.id &&
          new Date(flight.start) < new Date(args.event.end) &&
          new Date(flight.end) > new Date(args.event.start)
        ) {
          unavailable[flight.resource] = true;
        }
      });
      flights.forEach((flight) => {
        if (unavailable[flight.resource]) {
          flight.cssClass = 'md-drag-drop-faded';
        }
      });
      jets.forEach((group) => {
        group.children.forEach((jet) => {
          if (unavailable[jet.id]) {
            invalid.resource.push(jet.id);
          }
        });
      });
      setInvalid([invalid]);
    },
    [flights],
  );

  const handleEventDragEnd = useCallback(() => {
    flights.forEach((flight) => {
      delete flight.cssClass;
    });
    setInvalid([]);
  }, [flights]);

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

  return (
    <div className="mbsc-flex-col md-drag-drop-bw-inst-cont">
      <Eventcalendar
        className="md-drag-drop-bw-inst-first"
        data={bookings}
        resources={reservations}
        view={myView}
        dragToMove={false}
        dragToCreate={false}
        dragToResize={false}
        eventDelete={true}
        externalDrag={true}
        onPageLoading={handlePageLoading}
        onPageLoaded={handleFirstScroll}
        onDestroy={detachFirstScroll}
      />
      <Eventcalendar
        className="md-drag-drop-bw-inst-second"
        invalid={invalid}
        data={flights}
        resources={jets}
        view={myView}
        showControls={false}
        dragToCreate={false}
        dragToMove={true}
        dragToResize={false}
        dragInTime={false}
        externalDrop={true}
        eventDelete={true}
        eventOverlap={false}
        renderResource={renderMyResource}
        selectedDate={mySelectedDate}
        onSelectedDateChange={handleSelectedDateChange}
        onEventCreated={handleEventCreated}
        onEventCreateFailed={handleEventCreateUpdateFailed}
        onEventDeleted={handleEventDeleted}
        onEventUpdateFailed={handleEventCreateUpdateFailed}
        onEventDragStart={handleEventDragStart}
        onEventDragEnd={handleEventDragEnd}
        onPageLoaded={handleSecondScroll}
        onDestroy={detachSecondScroll}
      />
      <Toast display="center" message={toastText} isOpen={isToastOpen} onClose={handleCloseToast} />
    </div>
  );
}

export default App;
```

### TSX

```tsx
import {
  Eventcalendar,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventDragEvent,
  MbscPageLoadingEvent,
  MbscResource,
  MbscSelectedDateChangeEvent,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useRef, useState } from 'react';

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

const bookings: MbscCalendarEvent[] = [
  {
    start: '2026-11-04T06:00',
    end: '2026-11-04T10:00',
    title: 'Budapest - Ljubljana',
    resource: 1,
  },
  {
    start: '2026-11-04T15:00',
    end: '2026-11-04T18:00',
    title: 'Ljubljana - Berlin',
    resource: 1,
  },
  {
    start: '2026-11-04T04:00',
    end: '2026-11-04T12:00',
    title: 'Los Angeles - Dublin',
    resource: 2,
  },
  {
    start: '2026-11-04T18:00',
    end: '2026-11-04T23:00',
    title: 'Dublin - Bucharest',
    resource: 2,
  },
  {
    start: '2026-11-04T06:00',
    end: '2026-11-04T14:00',
    title: 'Chicago - Amsterdam',
    resource: 3,
  },
  {
    start: '2026-11-04T17:00',
    end: '2026-11-04T22:00',
    title: 'Amsterdam - Cairo',
    resource: 3,
  },
  {
    start: '2026-11-04T10:00',
    end: '2026-11-04T14:00',
    title: 'Hong Kong - Sydney',
    resource: 4,
  },
  {
    start: '2026-11-04T15:00',
    end: '2026-11-04T21:00',
    title: 'Sydney - Tokyo',
    resource: 4,
  },
  {
    start: '2026-11-04T04:00',
    end: '2026-11-04T12:00',
    title: 'Paris - Washington, D.C.',
    resource: 5,
  },
  {
    start: '2026-11-04T12:00',
    end: '2026-11-04T18:00',
    title: 'Washington, D.C. - Los-Angeles',
    resource: 5,
  },
  {
    start: '2026-11-04T03:00',
    end: '2026-11-04T11:00',
    title: 'Los Angeles - Dublin',
    resource: 6,
  },
  {
    start: '2026-11-04T13:00',
    end: '2026-11-04T18:00',
    title: 'Dublin - Rome',
    resource: 6,
  },
  {
    start: '2026-11-04T05:00',
    end: '2026-11-04T12:00',
    title: 'Barcelona - Dubai',
    resource: 7,
  },
  {
    start: '2026-11-04T13:00',
    end: '2026-11-04T20:00',
    title: 'Dubai - Tokyo',
    resource: 7,
  },
  {
    start: '2026-11-04T03:30',
    end: '2026-11-04T13:00',
    title: 'Rome - Toronto',
    resource: 8,
  },
  {
    start: '2026-11-04T16:00',
    end: '2026-11-04T19:00',
    title: 'Toronto - New-York',
    resource: 8,
  },
  {
    start: '2026-11-04T03:00',
    end: '2026-11-04T14:00',
    title: 'Vienna - Shanghai',
    resource: 9,
  },
  {
    start: '2026-11-04T16:00',
    end: '2026-11-04T23:30',
    title: 'Shanghai - Moscow',
    resource: 9,
  },
  {
    start: '2026-11-04T08:00',
    end: '2026-11-04T13:00',
    title: 'London - Cairo',
    resource: 10,
  },
  {
    start: '2026-11-04T15:30',
    end: '2026-11-04T19:30',
    title: 'Cairo - Sofia',
    resource: 10,
  },
  {
    start: '2026-11-04T02:00',
    end: '2026-11-04T13:00',
    title: 'Istanbul - New York',
    resource: 11,
  },
  {
    start: '2026-11-04T16:00',
    end: '2026-11-04T20:00',
    title: 'New York - Montreal',
    resource: 11,
  },
  {
    start: '2026-11-04T05:00',
    end: '2026-11-04T11:00',
    title: 'Seattle - Miami',
    resource: 12,
  },
  {
    start: '2026-11-04T12:00',
    end: '2026-11-04T21:00',
    title: 'Miami - Buenos-Aires',
    resource: 12,
  },
  {
    start: '2026-11-05T04:00',
    end: '2026-11-05T16:00',
    title: 'Hong Kong - Sydney',
    resource: 1,
  },
  {
    start: '2026-11-05T06:00',
    end: '2026-11-05T10:00',
    title: 'Los Angeles - Dublin',
    resource: 2,
  },
  {
    start: '2026-11-05T03:00',
    end: '2026-11-05T11:00',
    title: 'Budapest - Ljubljana',
    resource: 4,
  },
  {
    start: '2026-11-05T05:00',
    end: '2026-11-05T12:00',
    title: 'Los Angeles - Dublin',
    resource: 6,
  },
  {
    start: '2026-11-05T13:00',
    end: '2026-11-05T23:00',
    title: 'London - Cairo',
    resource: 7,
  },
  {
    start: '2026-11-05T03:00',
    end: '2026-11-05T14:00',
    title: 'Miami - Buenos-Aires',
    resource: 9,
  },
  {
    start: '2026-11-05T06:00',
    end: '2026-11-05T12:00',
    title: 'Barcelona - Dubai',
    resource: 10,
  },
  {
    start: '2026-11-05T08:00',
    end: '2026-11-05T22:00',
    title: 'Istanbul - New York',
    resource: 11,
  },
  {
    start: '2026-11-05T05:00',
    end: '2026-11-05T11:00',
    title: 'Vienna - Shanghai',
    resource: 12,
  },
];

const jets: MbscResource[] = [
  {
    id: 'Op 1',
    name: 'Prestige Air',
    eventCreation: false,
    children: [
      {
        id: 1,
        code: '#AF 7703',
        crew: 52,
        name: 'Red Bolt',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-1.jpg',
      },
      {
        id: 2,
        code: '#BQ 4718',
        crew: 47,
        name: 'Skyroar',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-2.jpg',
      },
      {
        id: 3,
        code: '#ZM 8430',
        crew: 24,
        name: 'Agile Raven',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-3.jpg',
      },
    ],
  },
  {
    id: 'Op 2',
    name: 'Elite Wings',
    eventCreation: false,
    children: [
      {
        id: 4,
        code: '#XG 5500',
        crew: 43,
        name: 'Monsterbolt',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-4.jpg',
      },
      {
        id: 5,
        code: '#FL 2531',
        crew: 22,
        name: 'Airrise',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-5.jpg',
      },
      {
        id: 6,
        code: '#PA 6487',
        crew: 84,
        name: 'Starblast',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-6.jpg',
      },
      {
        id: 7,
        code: '#PP 9812',
        crew: 28,
        name: 'Ebonfire',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-7.jpg',
      },
    ],
  },
  {
    id: 'Op 3',
    name: 'Luxury Skies',
    eventCreation: false,
    children: [
      {
        id: 8,
        code: '#DN 3191',
        crew: 36,
        name: 'Dark Bee',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-8.jpg',
      },
      {
        id: 9,
        code: '#ZW 2328',
        crew: 76,
        name: 'Keen Sparrow',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-9.jpg',
      },
      {
        id: 10,
        code: '#RX 9898',
        crew: 37,
        name: 'Jagged Bolt',
        color: '#1dab2f',
        img: 'https://img.mobiscroll.com/demos/jet-10.jpg',
      },
    ],
  },
];

const App: FC = () => {
  const reservations = useMemo<MbscResource[]>(
    () => [
      {
        id: 1,
        name: 'Alison Reyes',
      },
      {
        id: 2,
        name: 'Shauna Perry',
      },
      {
        id: 3,
        name: 'Jan Whitney',
      },
      {
        id: 4,
        name: 'Freddie Durham',
      },
      {
        id: 5,
        name: 'William Dillon',
      },
      {
        id: 6,
        name: 'Tyrell Edwards',
      },
      {
        id: 7,
        name: 'Caitlyn Riddle',
      },
      {
        id: 8,
        name: 'Liam Mays',
      },
      {
        id: 9,
        name: 'Frank Medina',
      },
      {
        id: 10,
        name: 'Calvin Larsen',
      },
      {
        id: 11,
        name: 'Heather Walsh',
      },
      {
        id: 12,
        name: 'Conner Paul',
      },
    ],
    [],
  );

  const [flights, setFlights] = useState<MbscCalendarEvent[]>([
    {
      start: '2026-11-04T10:00',
      end: '2026-11-04T17:00',
      title: 'Barcelona - Dubai',
      resource: 4,
    },
    {
      start: '2026-11-04T07:30',
      end: '2026-11-04T16:00',
      title: 'Toronto - Rome',
      resource: 8,
    },
    {
      start: '2026-11-04T04:00',
      end: '2026-11-04T08:00',
      title: 'Ljubljana - Budapest',
      resource: 1,
    },
    {
      start: '2026-11-04T02:00',
      end: '2026-11-04T10:00',
      title: 'Paris - Washington, D.C.',
      resource: 2,
    },
    {
      start: '2026-11-04T12:00',
      end: '2026-11-04T23:00',
      title: 'New York - Istanbul',
      resource: 7,
    },
    {
      start: '2026-11-04T15:00',
      end: '2026-11-04T23:00',
      title: 'Moscow - Tokyo',
      resource: 3,
    },
  ]);

  const [invalid, setInvalid] = useState<MbscCalendarEvent[]>();
  const firstCalCont = useRef<HTMLElement | null>(null);
  const secondCalCont = useRef<HTMLElement | null>(null);
  const skipScroll1 = useRef<boolean>(false);
  const skipScroll2 = useRef<boolean>(false);
  const [mySelectedDate, setSelectedDate] = useState<Date>(new Date());
  const [isToastOpen, setToastOpen] = useState<boolean>(false);
  const [toastText, setToastText] = useState<string>('');

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      timeline: {
        type: 'day',
        size: 1,
      },
    }),
    [],
  );

  const renderMyResource = (resource: MbscResource) => {
    if (!resource.children) {
      return (
        <div className="mbsc-flex mbsc-align-items-center mbsc-justify-content-start">
          <img className="md-drag-drop-bw-inst-avatar" src={resource.img} alt="Avatar" />
          <div className="md-drag-drop-bw-inst-details mbsc-flex-col mbsc-flex-1-0">
            <div>{resource.name}</div>
            <div className="md-aircraft-code mbsc-flex mbsc-justify-content-between">
              <div>{resource.code}</div>
              <div>
                {'\u{1F9D1}\u{1F3FC}\u{200D}\u{2708}\u{FE0F}'} {resource.crew}
              </div>
            </div>
          </div>
        </div>
      );
    }
    return <div>{resource.name}</div>;
  };

  const handleScroll1 = useCallback((ev: Event) => {
    if (secondCalCont.current && !skipScroll2.current) {
      skipScroll1.current = true;
      secondCalCont.current.scrollLeft = (ev.target as HTMLDivElement).scrollLeft;
    }
    skipScroll1.current = false;
  }, []);

  const handleScroll2 = useCallback((ev: Event) => {
    if (firstCalCont.current && !skipScroll1.current) {
      skipScroll2.current = true;
      firstCalCont.current.scrollLeft = (ev.target as HTMLDivElement).scrollLeft;
    }
    skipScroll2.current = false;
  }, []);

  const handleFirstScroll = useCallback(() => {
    if (!firstCalCont.current) {
      firstCalCont.current = document.querySelector('.md-drag-drop-bw-inst-first .mbsc-timeline-grid-scroll');
      firstCalCont.current!.addEventListener('scroll', handleScroll1);
    }
  }, [handleScroll1]);

  const handleSecondScroll = useCallback(() => {
    if (!secondCalCont.current) {
      secondCalCont.current = document.querySelector('.md-drag-drop-bw-inst-second .mbsc-timeline-grid-scroll');
      secondCalCont.current!.addEventListener('scroll', handleScroll2);
    }
  }, [handleScroll2]);

  const detachFirstScroll = useCallback(() => {
    if (firstCalCont.current) {
      firstCalCont.current.removeEventListener('scroll', handleScroll1);
    }
  }, [handleScroll1]);

  const detachSecondScroll = useCallback(() => {
    if (secondCalCont.current) {
      secondCalCont.current.removeEventListener('scroll', handleScroll2);
    }
  }, [handleScroll2]);

  const handlePageLoading = useCallback((args: MbscPageLoadingEvent) => {
    if (secondCalCont.current) {
      setTimeout(() => {
        setSelectedDate(args.firstDay);
      }, 100);
    }
  }, []);

  const handleSelectedDateChange = useCallback((args: MbscSelectedDateChangeEvent) => {
    setSelectedDate(new Date(args.date as string));
  }, []);

  const handleEventCreated = useCallback((args: MbscEventCreatedEvent) => {
    setFlights((current) => [...current, args.event]);
    setToastText('Flight scheduled');
    setToastOpen(true);
  }, []);

  const handleEventDeleted = useCallback((args: MbscEventDeletedEvent) => {
    setFlights((current) => [...current.filter((flight) => flight.id !== args.event.id)]);
  }, []);

  const handleEventCreateUpdateFailed = useCallback(() => {
    setToastText("There's already a flight on this date");
    setToastOpen(true);
  }, []);

  const handleEventDragStart = useCallback(
    (args: MbscEventDragEvent) => {
      const unavailable: { [key: string]: boolean } = {};
      const invalid: MbscCalendarEvent = {
        cssClass: 'md-drag-drop-invalid',
        recurring: { repeat: 'daily' },
        resource: [],
      };
      // Filter unavailable jets
      flights.forEach((flight) => {
        if (
          flight.id !== args.event.id &&
          new Date(flight.start as string) < new Date(args.event.end as string) &&
          new Date(flight.end as string) > new Date(args.event.start as string)
        ) {
          unavailable[flight.resource as string] = true;
        }
      });
      flights.forEach((flight) => {
        if (unavailable[flight.resource as string]) {
          flight.cssClass = 'md-drag-drop-faded';
        }
      });
      jets.forEach((group) => {
        group.children!.forEach((jet) => {
          if (unavailable[jet.id]) {
            (invalid.resource as Array<number | string>).push(jet.id);
          }
        });
      });
      setInvalid([invalid]);
    },
    [flights],
  );

  const handleEventDragEnd = useCallback(() => {
    flights.forEach((flight) => {
      delete flight.cssClass;
    });
    setInvalid([]);
  }, [flights]);

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

  return (
    <div className="mbsc-flex-col md-drag-drop-bw-inst-cont">
      <Eventcalendar
        className="md-drag-drop-bw-inst-first"
        data={bookings}
        resources={reservations}
        view={myView}
        dragToMove={false}
        dragToCreate={false}
        dragToResize={false}
        eventDelete={true}
        externalDrag={true}
        onPageLoading={handlePageLoading}
        onPageLoaded={handleFirstScroll}
        onDestroy={detachFirstScroll}
      />
      <Eventcalendar
        className="md-drag-drop-bw-inst-second"
        invalid={invalid}
        data={flights}
        resources={jets}
        view={myView}
        showControls={false}
        dragToCreate={false}
        dragToMove={true}
        dragToResize={false}
        dragInTime={false}
        externalDrop={true}
        eventDelete={true}
        eventOverlap={false}
        renderResource={renderMyResource}
        selectedDate={mySelectedDate}
        onSelectedDateChange={handleSelectedDateChange}
        onEventCreated={handleEventCreated}
        onEventCreateFailed={handleEventCreateUpdateFailed}
        onEventDeleted={handleEventDeleted}
        onEventUpdateFailed={handleEventCreateUpdateFailed}
        onEventDragStart={handleEventDragStart}
        onEventDragEnd={handleEventDragEnd}
        onPageLoaded={handleSecondScroll}
        onDestroy={detachSecondScroll}
      />
      <Toast display="center" message={toastText} isOpen={isToastOpen} onClose={handleCloseToast} />
    </div>
  );
};
export default App;
```

### CSS

```css
.md-drag-drop-bw-inst-cont .mbsc-eventcalendar {
  height: 500px;
}

.md-drag-drop-bw-inst-cont .mbsc-timeline-parent {
  height: 32px;
}

.md-drag-drop-bw-inst-cont .mbsc-timeline-resource {
  padding-left: 5px !important;
}

.md-drag-drop-bw-inst-cont .mbsc-timeline-row-gutter {
  height: 6px;
}

.md-drag-drop-bw-inst-cont .mbsc-timeline-resource-depth-step {
  width: 0;
}

.md-drag-drop-bw-inst-second .mbsc-timeline-header,
.md-drag-drop-bw-inst-second .mbsc-timeline-header-sticky {
  display: none;
}

.md-aircraft-code {
  color: #939995;
}

.md-drag-drop-bw-inst-cont {
  min-height: 500px;
  height: 100%;
}

.md-drag-drop-bw-inst-first {
  border-bottom: 5px solid #ccc;
}

.md-drag-drop-bw-inst-avatar {
  max-height: 55px;
  max-width: 55px;
  border-bottom-right-radius: 4px;
  border-top-right-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-left-radius: 4px;
}

.md-drag-drop-bw-inst-details {
  margin-left: 16px;
  font-size: 12px;
}

.md-drag-drop-bw-inst-title {
  font-size: 12px;
  margin-top: 5px;
}

.md-drag-drop-faded {
  opacity: 0.3;
}

.md-drag-drop-invalid.mbsc-schedule-invalid {
  background: repeating-linear-gradient(-45deg, #f3f3f3, #f3f3f3 11px, #e5e5e5 11px, #e5e5e5 22px);
}
```

## Implementation notes

- Render two Eventcalendar instances stacked vertically: the top shows passenger bookings (one row per passenger; drag-out and delete only) and the bottom shows jet aircraft grouped by operator.
- Use `type: 'day'` with `size: 1` on both calendars so they show the same single day.
- On the top calendar, set `externalDrag: true` so bookings can be dragged out onto the bottom calendar. Set `eventDelete: true` to allow deleting bookings directly. Disable `dragToMove`, `dragToCreate`, and `dragToResize`.
- On the bottom calendar, set `externalDrop: true` to accept dropped bookings, `dragToMove: true` to allow reassignment, `dragInTime: false` to prevent time changes, and `eventDelete: true`. Disable `dragToCreate` and `dragToResize`. Set `eventOverlap: false` to prevent double-booking a jet.
- Set `showControls: false` on the bottom calendar so only the top calendar shows the navigation header. Bind `selectedDate` on the bottom calendar and update it via `onPageLoading` on the top calendar to keep both on the same date. Wire `onSelectedDateChange` on the bottom calendar to propagate manual navigation back.
- Synchronize horizontal scrolling: in `onPageLoaded` for each calendar, locate the `.mbsc-timeline-grid-scroll` scroll container and attach a scroll listener. Each listener copies `scrollLeft` to the other container. Use two boolean skip-flags to prevent infinite scroll loops. Remove the listeners in `onDestroy`.
- Define the bottom calendar's resources as a two-level tree: operator groups with `eventCreation: false` and individual jet leaf nodes with custom `img`, `code`, and `crew` properties.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) on the bottom calendar to show a jet photo, name, tail code, and crew count for leaf nodes, and just the operator name for group nodes.
- Use `onEventDragStart` on the bottom calendar to compute availability: find all scheduled flights that overlap the dragged booking's time window, collect their jet resource IDs, and set them as `invalid` with `recurring: { repeat: 'daily' }`. Also apply a fade CSS class to the overlapping flights to visually grey them out.
- Use `onEventDragEnd` to clear the `invalid` array and remove the fade class from all flights.
- Use `onEventCreated` and `onEventDeleted` to update the scheduled flights state. Use `onEventCreateFailed` and `onEventUpdateFailed` to show a `Toast` when a booking cannot be placed due to a conflict.

## 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: [Task Checklist in Events](https://demo.mobiscroll.com/react/timeline/check-list-tasks-within-events.md) - Render a task checklist inside each React Timeline event card — row heights expand as the list grows, and a popup let users add new subtasks.
- Next: [Task Progress Tracking](https://demo.mobiscroll.com/react/timeline/show-task-progress-on-event.md) - Show task completion progress on the React Timeline events with an inline hover slider for live updates and a progress field in the add/edit dialog.
