---
title: "React Timeline Resource Hover Tooltip Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/display-resource-information-on-hover"
---

# Resource Hover Tooltip - React Timeline demo

Hover over a resource row in the React Timeline to open a tooltip popup with scheduled hours — keeps the timeline view clean. For React and Next.JS.

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

## Description

Displaying information in a tooltip can come in handy when space is tight but information matters. When hovering over a resource cell, the [`onResourceHoverIn`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceHoverIn) and [`onResourceHoverOut`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceHoverOut) [lifecycle events](https://demo.mobiscroll.com/react/timeline/event-hooks) are triggered, which can be used to show/hide rich tooltips. Information and actions shown here won't clutter the main view, keep the timeline clean while still giving quick access to content that helps.

**Do you want to learn more about customizing the resource header?** [Take a look at this example →](https://demo.mobiscroll.com/react/scheduler/custom-resource-header-template)

## What this demo shows

- A desktop resource timeline with hours arranged horizontally and worker resources listed as rows on the left.
- **Header navigation** The month and year label opens date navigation, the previous and next arrows move between days, and the Today button returns to the current date.
- **Day view** The day strip shows the selected day, with the current date highlighted. The timeline displays hourly columns from 7 AM to 10 PM.
- **Resources** Each worker appears as a separate resource row with an avatar, name, and profession in the resource column.
- **Resource hover** Hovering over a resource row opens a custom popup with additional worker details.
- **Resource popup** The popup shows the worker avatar, name, hourly rate, scheduled hours for the selected day, estimated earnings, and a Pay action button.
- **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 are shown as colored cards with a left color stripe, a bold title, and the exact start and end time below the title.
- **Date positioning** Events are positioned by their assigned resource and exact date and time range.
- **Event interaction** Events highlight on hover and show drag and resize handles for repositioning or changing duration.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created by double-clicking the timeline or by clicking and dragging across a time range.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through time and resources.

## Best for

- **Workforce management** Showing employee schedules together with role, cost, and daily workload details.
- **Field service scheduling** Reviewing technician assignments while keeping profile details and pay-related actions close to the schedule.
- **Construction and maintenance planning** Coordinating resource-based work across a day timeline with quick access to worker information.
- **Healthcare staffing** Comparing staff assignments by time and resource while showing contextual staff details only when needed.
- **Payroll-aware scheduling** Displaying scheduled hours, hourly rates, estimated earnings, and payment actions alongside planned work.
- **Resource-heavy operations** Keeping the timeline readable while making worker metadata available from the resource column.

## 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
- [`getEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#method-getEvents) - method
- [`position`](https://mobiscroll.com/docs/react/popup/api#method-position) - method
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - 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
- [`eventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventDelete) - 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
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`onResourceHoverIn`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceHoverIn) - event
- [`onResourceHoverOut`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onResourceHoverOut) - event
- [`onPageChange`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onPageChange) - event
- [`display`](https://mobiscroll.com/docs/react/popup/api#opt-display) - option
- [`showOverlay`](https://mobiscroll.com/docs/react/popup/api#opt-showOverlay) - option
- [`touchUi`](https://mobiscroll.com/docs/react/popup/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/react/popup/api#opt-width) - option
- [`isOpen`](https://mobiscroll.com/docs/react/popup/api#opt-isOpen) - option
- [`onPosition`](https://mobiscroll.com/docs/react/popup/api#event-onPosition) - event
- [`onClose`](https://mobiscroll.com/docs/react/popup/api#event-onClose) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

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

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

function App() {
  const [resourceAvatar, setResourceAvatar] = useState('');
  const [resourceName, setResourceName] = useState('');
  const [resourceCost, setResourceCost] = useState('');
  const [resourceDate, setResourceDate] = useState('');
  const [resourceTotal, setResourceTotal] = useState('');
  const [currentResource, setCurrentResource] = useState(null);
  const [isTooltipOpen, setTooltipOpen] = useState(false);
  const [isToastOpen, setToastOpen] = useState(false);
  const [toastMessage, setToastMessage] = useState('');

  const hoveredResourceElmRef = useRef(null);
  const mySelectedDate = useRef(new Date());
  const calRef = useRef(null);
  const popupRef = useRef(null);
  const openTimer = useRef(null);
  const closeTimer = useRef(null);

  const myResources = useMemo(
    () => [
      {
        id: 'res1',
        name: 'Adam Miller',
        color: '#F39C12',
        profession: 'Mason',
        avatar: 'https://img.mobiscroll.com/demos/m1.png',
        cost: '15',
      },
      {
        id: 'res2',
        name: 'Emily Carter',
        color: '#76e083',
        profession: 'Electrician',
        avatar: 'https://img.mobiscroll.com/demos/f1.png',
        cost: '20',
      },
      {
        id: 'res3',
        name: 'James Brown',
        color: '#b13f49',
        profession: 'Carpenter',
        avatar: 'https://img.mobiscroll.com/demos/m2.png',
        cost: '18',
      },
      {
        id: 'res4',
        name: 'Daniel Wilson',
        color: '#e25dd2',
        profession: 'Welder',
        avatar: 'https://img.mobiscroll.com/demos/m3.png',
        cost: '22',
      },
      {
        id: 'res5',
        name: 'Benjamin Harris',
        color: '#7056ff',
        profession: 'Plumber',
        avatar: 'https://img.mobiscroll.com/demos/m4.png',
        cost: '20',
      },
      {
        id: 'res6',
        name: 'Olivia Anderson',
        color: '#56aaff',
        profession: 'Concrete Finisher',
        avatar: 'https://img.mobiscroll.com/demos/f2.png',
        cost: '15',
      },
      {
        id: 'res7',
        name: 'Emma Thompson',
        color: '#84852f',
        profession: 'Steelworker',
        avatar: 'https://img.mobiscroll.com/demos/f3.png',
        cost: '18',
      },
      {
        id: 'res8',
        name: 'Natalie Roberts',
        color: '#ff6e56',
        profession: 'Painter',
        avatar: 'https://img.mobiscroll.com/demos/f4.png',
        cost: '25',
      },
    ],
    [],
  );

  const myEvents = useMemo(
    () => [
      {
        start: '2026-10-25T12:00',
        end: '2026-10-25T15:00',
        title: 'Repoint Brick Facade',
        resource: 'res1',
      },
      {
        start: '2026-10-25T09:00',
        end: '2026-10-25T12:00',
        title: 'Install Custom Wood Trim',
        resource: 'res3',
      },
      {
        start: '2026-10-25T14:00',
        end: '2026-10-25T18:00',
        title: 'Repair Steel Stair Treads',
        resource: 'res4',
      },
      {
        start: '2026-10-25T10:00',
        end: '2026-10-25T13:00',
        title: 'Pour and Finish Driveway Slab',
        resource: 'res6',
      },
      {
        start: '2026-10-25T11:00',
        end: '2026-10-25T16:00',
        title: 'Paint Interior Drywall',
        resource: 'res8',
      },
      {
        start: '2026-10-26T08:00',
        end: '2026-10-26T11:00',
        title: 'Block Wall Construction',
        resource: 'res1',
      },
      {
        start: '2026-10-26T14:00',
        end: '2026-10-26T16:00',
        title: 'Task 2',
        resource: 'Paver Installation',
      },
      {
        start: '2026-10-26T12:00',
        end: '2026-10-26T17:00',
        title: 'Install ceiling fan',
        resource: 'res2',
      },
      {
        start: '2026-10-26T10:00',
        end: '2026-10-26T14:00',
        title: 'Roof Beam Replacement',
        resource: 'res3',
      },
      {
        start: '2026-10-26T07:00',
        end: '2026-10-26T12:00',
        title: 'Custom Metalworks Creation',
        resource: 'res4',
      },
      {
        start: '2026-10-26T14:00',
        end: '2026-10-26T17:00',
        title: 'Pipe Welding',
        resource: 'res4',
      },
      {
        start: '2026-10-10T08:00',
        end: '2026-10-11T20:00',
        title: 'Leak Detection & Repair',
        resource: 'res5',
      },
      {
        start: '2026-10-26T13:00',
        end: '2026-10-26T17:00',
        title: 'Faucet & Sink Fitting',
        resource: 'res5',
      },
      {
        start: '2026-10-26T18:00',
        end: '2026-10-26T20:00',
        title: 'Drainage System Setup',
        resource: 'res5',
      },
      {
        start: '2026-10-26T09:00',
        end: '2026-10-26T13:00',
        title: 'Surface Polishing',
        resource: 'res6',
      },
      {
        start: '2026-10-26T08:00',
        end: '2026-10-26T10:00',
        title: 'Structural Steel Inspections',
        resource: 'res7',
      },
      {
        start: '2026-10-26T13:00',
        end: '2026-10-26T16:00',
        title: 'Metal Structure Assembly',
        resource: 'res7',
      },
      {
        start: '2026-10-26T17:00',
        end: '2026-10-26T19:00',
        title: 'Heavy Steel Beam Placement',
        resource: 'res7',
      },
      {
        start: '2026-10-26T09:00',
        end: '2026-10-26T12:00',
        title: 'Exterior House Painting',
        resource: 'res8',
      },
      {
        start: '2026-10-26T15:00',
        end: '2026-10-26T18:00',
        title: 'Deck Staining & Sealing',
        resource: 'res8',
      },
      {
        start: '2026-10-27T12:00',
        end: '2026-10-27T15:00',
        title: 'Troubleshoot Faulty Breaker',
        resource: 'res2',
      },
      {
        start: '2026-10-27T10:00',
        end: '2026-10-27T13:00',
        title: 'Frame Interior Partitions',
        resource: 'res3',
      },
      {
        start: '2026-10-27T16:00',
        end: '2026-10-27T20:00',
        title: 'Weld Structural Beam Connections',
        resource: 'res4',
      },
      {
        start: '2026-10-27T12:00',
        end: '2026-10-27T16:00',
        title: 'Apply Smooth Trowel Finish to Basement Floor',
        resource: 'res6',
      },
    ],
    [],
  );

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

  const getTotalHoursForResource = useCallback(
    (resourceId) =>
      calRef.current
        .getEvents()
        .filter((e) => e.resource === resourceId)
        .reduce((sum, e) => {
          const start = new Date(e.start);
          const end = new Date(e.end);
          const hours = (end - start) / (1000 * 60 * 60);
          return sum + hours;
        }, 0),
    [],
  );

  // Handles both Mobiscroll hover and native mouse events
  const openTooltip = useCallback(
    (resource, target) => {
      clearTimeout(closeTimer.current);
      clearTimeout(openTimer.current);

      openTimer.current = setTimeout(() => {
        const totalHours = getTotalHoursForResource(resource.id);

        setCurrentResource(resource);
        hoveredResourceElmRef.current = target;

        setResourceAvatar(resource.avatar);
        setResourceName(resource.name);
        setResourceCost('$' + resource.cost);
        setResourceDate(formatDate('D DDD MMM YYYY', mySelectedDate.current));
        setResourceTotal(totalHours + 'h, $' + totalHours * resource.cost);
        target.classList.add('mds-resource-info-hover');

        popupRef.current.position();
        setTooltipOpen(true);

        openTimer.current = null;
      }, 100);
    },
    [mySelectedDate, getTotalHoursForResource],
  );

  const closeTooltip = useCallback(() => {
    clearTimeout(openTimer.current);
    clearTimeout(closeTimer.current);

    closeTimer.current = setTimeout(() => {
      setTooltipOpen(false);
      closeTimer.current = null;
    }, 200);
  }, []);

  // Mobiscroll resource hover events
  const handleResourceHoverIn = useCallback(
    (args) => {
      openTooltip(args.resource, args.target);
    },
    [openTooltip],
  );

  const handleResourceHoverOut = useCallback(
    (args) => {
      args.target.classList.remove('mds-resource-info-hover');
      closeTooltip();
    },
    [closeTooltip],
  );

  const handlePageChange = useCallback((args) => {
    mySelectedDate.current = args.firstDay;
  }, []);

  const handleTooltipPosition = useCallback((args, inst) => {
    const popupElm = args.target.querySelector('.mbsc-popup');
    const rect = hoveredResourceElmRef.current.getBoundingClientRect();

    popupElm.style.top = rect.top - 10 + 'px';

    if (inst.s.rtl) {
      popupElm.style.right = window.innerWidth - rect.left + 10 + 'px';
    } else {
      popupElm.style.left = rect.right + 10 + 'px';
    }
    return false; // Prevent default positioning
  }, []);

  // Native mouse events for the popup
  const handlePopupMouseEnter = useCallback(() => {
    clearTimeout(closeTimer.current);
  }, []);

  const handlePopupMouseLeave = useCallback(() => {
    closeTooltip();
  }, [closeTooltip]);

  const customResource = useCallback(
    (res) => (
      <div className="mbsc-flex">
        <img className="mds-resource-info-avatar" src={res.avatar} alt="Avatar" />
        <div className="mds-resource-info-details">
          <div className="mds-resource-info-title">{res.name}</div>
          <div className="mds-resource-info-profession">{res.profession}</div>
        </div>
      </div>
    ),
    [],
  );

  const handlePay = () => {
    setTooltipOpen(false);
    setToastMessage(currentResource.name + ' paid');
    setToastOpen(true);
  };

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

  const handleTooltipClose = useCallback(() => {
    setTooltipOpen(false);
  }, []);

  return (
    <>
      <Eventcalendar
        clickToCreate={true}
        dragToCreate={true}
        dragToMove={true}
        dragToResize={true}
        eventDelete={true}
        ref={calRef}
        data={myEvents}
        resources={myResources}
        view={myView}
        renderResource={customResource}
        onResourceHoverIn={handleResourceHoverIn}
        onResourceHoverOut={handleResourceHoverOut}
        onPageChange={handlePageChange}
      />
      <Popup
        ref={popupRef}
        display="anchored"
        showOverlay={false}
        touchUi={false}
        width={280}
        isOpen={isTooltipOpen}
        onPosition={handleTooltipPosition}
        onClose={handleTooltipClose}
      >
        <div onMouseEnter={handlePopupMouseEnter} onMouseLeave={handlePopupMouseLeave}>
          <div className="mbsc-flex mbsc-align-items-center mds-resource-info-header">
            <img className="mds-resource-info-avatar" src={resourceAvatar} alt="Avatar" />
            <div className="mds-resource-info-name mbsc-flex-1-0">{resourceName}</div>
            <Button color="success" variant="outline" className="mds-resource-info-pay mbsc-button-xs" onClick={handlePay}>
              Pay
            </Button>
          </div>
          <div className="mds-resource-info-cont">
            <div>
              <span className="mbsc-txt-muted">Hourly rate </span>
              <span className="mds-resource-info-detail">{resourceCost}</span>
            </div>
            <div>
              <span className="mbsc-txt-muted">{resourceDate}</span>
              <span className="mds-resource-info-detail">{resourceTotal}</span>
            </div>
          </div>
        </div>
      </Popup>
      <Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
    </>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  Eventcalendar,
  formatDate,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscPageChangeEvent,
  MbscPopupPositionEvent,
  MbscResource,
  MbscResourceHoverEvent,
  Popup,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useRef, useState } from 'react';

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

const App: FC = () => {
  const [resourceAvatar, setResourceAvatar] = useState<string>('');
  const [resourceName, setResourceName] = useState<string>('');
  const [resourceCost, setResourceCost] = useState<string>('');
  const [resourceDate, setResourceDate] = useState<string>('');
  const [resourceTotal, setResourceTotal] = useState<string>('');
  const [currentResource, setCurrentResource] = useState<MbscResource>();
  const [isTooltipOpen, setTooltipOpen] = useState<boolean>(false);
  const [isToastOpen, setToastOpen] = useState<boolean>(false);
  const [toastMessage, setToastMessage] = useState<string>('');

  const hoveredResourceElmRef = useRef<HTMLElement>(null);
  const mySelectedDate = useRef(new Date());
  const calInst = useRef<Eventcalendar>(null);
  const popupInst = useRef<Popup>(null);
  const openTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
  const closeTimer = useRef<ReturnType<typeof setTimeout>>(undefined);

  const myResources = useMemo<MbscResource[]>(
    () => [
      {
        id: 'res1',
        name: 'Adam Miller',
        color: '#F39C12',
        profession: 'Mason',
        avatar: 'https://img.mobiscroll.com/demos/m1.png',
        cost: '15',
      },
      {
        id: 'res2',
        name: 'Emily Carter',
        color: '#76e083',
        profession: 'Electrician',
        avatar: 'https://img.mobiscroll.com/demos/f1.png',
        cost: '20',
      },
      {
        id: 'res3',
        name: 'James Brown',
        color: '#b13f49',
        profession: 'Carpenter',
        avatar: 'https://img.mobiscroll.com/demos/m2.png',
        cost: '18',
      },
      {
        id: 'res4',
        name: 'Daniel Wilson',
        color: '#e25dd2',
        profession: 'Welder',
        avatar: 'https://img.mobiscroll.com/demos/m3.png',
        cost: '22',
      },
      {
        id: 'res5',
        name: 'Benjamin Harris',
        color: '#7056ff',
        profession: 'Plumber',
        avatar: 'https://img.mobiscroll.com/demos/m4.png',
        cost: '20',
      },
      {
        id: 'res6',
        name: 'Olivia Anderson',
        color: '#56aaff',
        profession: 'Concrete Finisher',
        avatar: 'https://img.mobiscroll.com/demos/f2.png',
        cost: '15',
      },
      {
        id: 'res7',
        name: 'Emma Thompson',
        color: '#84852f',
        profession: 'Steelworker',
        avatar: 'https://img.mobiscroll.com/demos/f3.png',
        cost: '18',
      },
      {
        id: 'res8',
        name: 'Natalie Roberts',
        color: '#ff6e56',
        profession: 'Painter',
        avatar: 'https://img.mobiscroll.com/demos/f4.png',
        cost: '25',
      },
    ],
    [],
  );

  const myEvents = useMemo<MbscCalendarEvent[]>(
    () => [
      {
        start: '2026-10-25T12:00',
        end: '2026-10-25T15:00',
        title: 'Repoint Brick Facade',
        resource: 'res1',
      },
      {
        start: '2026-10-25T09:00',
        end: '2026-10-25T12:00',
        title: 'Install Custom Wood Trim',
        resource: 'res3',
      },
      {
        start: '2026-10-25T14:00',
        end: '2026-10-25T18:00',
        title: 'Repair Steel Stair Treads',
        resource: 'res4',
      },
      {
        start: '2026-10-25T10:00',
        end: '2026-10-25T13:00',
        title: 'Pour and Finish Driveway Slab',
        resource: 'res6',
      },
      {
        start: '2026-10-25T11:00',
        end: '2026-10-25T16:00',
        title: 'Paint Interior Drywall',
        resource: 'res8',
      },
      {
        start: '2026-10-26T08:00',
        end: '2026-10-26T11:00',
        title: 'Block Wall Construction',
        resource: 'res1',
      },
      {
        start: '2026-10-26T14:00',
        end: '2026-10-26T16:00',
        title: 'Task 2',
        resource: 'Paver Installation',
      },
      {
        start: '2026-10-26T12:00',
        end: '2026-10-26T17:00',
        title: 'Install ceiling fan',
        resource: 'res2',
      },
      {
        start: '2026-10-26T10:00',
        end: '2026-10-26T14:00',
        title: 'Roof Beam Replacement',
        resource: 'res3',
      },
      {
        start: '2026-10-26T07:00',
        end: '2026-10-26T12:00',
        title: 'Custom Metalworks Creation',
        resource: 'res4',
      },
      {
        start: '2026-10-26T14:00',
        end: '2026-10-26T17:00',
        title: 'Pipe Welding',
        resource: 'res4',
      },
      {
        start: '2026-10-10T08:00',
        end: '2026-10-11T20:00',
        title: 'Leak Detection & Repair',
        resource: 'res5',
      },
      {
        start: '2026-10-26T13:00',
        end: '2026-10-26T17:00',
        title: 'Faucet & Sink Fitting',
        resource: 'res5',
      },
      {
        start: '2026-10-26T18:00',
        end: '2026-10-26T20:00',
        title: 'Drainage System Setup',
        resource: 'res5',
      },
      {
        start: '2026-10-26T09:00',
        end: '2026-10-26T13:00',
        title: 'Surface Polishing',
        resource: 'res6',
      },
      {
        start: '2026-10-26T08:00',
        end: '2026-10-26T10:00',
        title: 'Structural Steel Inspections',
        resource: 'res7',
      },
      {
        start: '2026-10-26T13:00',
        end: '2026-10-26T16:00',
        title: 'Metal Structure Assembly',
        resource: 'res7',
      },
      {
        start: '2026-10-26T17:00',
        end: '2026-10-26T19:00',
        title: 'Heavy Steel Beam Placement',
        resource: 'res7',
      },
      {
        start: '2026-10-26T09:00',
        end: '2026-10-26T12:00',
        title: 'Exterior House Painting',
        resource: 'res8',
      },
      {
        start: '2026-10-26T15:00',
        end: '2026-10-26T18:00',
        title: 'Deck Staining & Sealing',
        resource: 'res8',
      },
      {
        start: '2026-10-27T12:00',
        end: '2026-10-27T15:00',
        title: 'Troubleshoot Faulty Breaker',
        resource: 'res2',
      },
      {
        start: '2026-10-27T10:00',
        end: '2026-10-27T13:00',
        title: 'Frame Interior Partitions',
        resource: 'res3',
      },
      {
        start: '2026-10-27T16:00',
        end: '2026-10-27T20:00',
        title: 'Weld Structural Beam Connections',
        resource: 'res4',
      },
      {
        start: '2026-10-27T12:00',
        end: '2026-10-27T16:00',
        title: 'Apply Smooth Trowel Finish to Basement Floor',
        resource: 'res6',
      },
    ],
    [],
  );

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

  const getTotalHoursForResource = useCallback(
    (resourceId: string | number) =>
      calInst
        .current!.getEvents()
        .filter((e: MbscCalendarEvent) => e.resource === resourceId)
        .reduce((sum: number, e: MbscCalendarEvent) => {
          const start = new Date(e.start as Date);
          const end = new Date(e.end as Date);
          const hours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
          return sum + hours;
        }, 0),
    [],
  );

  // Handles both Mobiscroll hover and native mouse events
  const openTooltip = useCallback(
    (resource: MbscResource, target: HTMLElement) => {
      clearTimeout(closeTimer.current!);
      clearTimeout(openTimer.current!);

      openTimer.current = setTimeout(() => {
        const totalHours = getTotalHoursForResource(resource.id);

        setCurrentResource(resource);
        hoveredResourceElmRef.current = target;

        setResourceAvatar(resource.avatar);
        setResourceName(resource.name!);
        setResourceCost('$' + resource.cost);
        setResourceDate(formatDate('D DDD MMM YYYY', mySelectedDate.current));
        setResourceTotal(totalHours + 'h, $' + totalHours * resource.cost);

        popupInst.current!.position();
        setTooltipOpen(true);

        openTimer.current = undefined;
      }, 100);
    },
    [mySelectedDate, getTotalHoursForResource],
  );

  const closeTooltip = useCallback(() => {
    clearTimeout(openTimer.current);
    clearTimeout(closeTimer.current);

    closeTimer.current = setTimeout(() => {
      setTooltipOpen(false);
      closeTimer.current = undefined;
    }, 200);
  }, []);

  // Mobiscroll resource hover events
  const handleResourceHoverIn = useCallback(
    (args: MbscResourceHoverEvent) => {
      openTooltip(args.resource, args.target);
    },
    [openTooltip],
  );

  const handleResourceHoverOut = useCallback(
    (args: MbscResourceHoverEvent) => {
      args.target.classList.remove('mds-resource-info-hover');
      closeTooltip();
    },
    [closeTooltip],
  );

  const handlePageChange = useCallback((args: MbscPageChangeEvent) => {
    mySelectedDate.current = args.firstDay;
  }, []);

  const handleTooltipPosition = useCallback((args: MbscPopupPositionEvent, inst: Popup) => {
    const popupElm = args.target.querySelector('.mbsc-popup') as HTMLElement;
    const rect = hoveredResourceElmRef.current!.getBoundingClientRect();

    popupElm.style.top = rect.top - 10 + 'px';

    if (inst.s.rtl) {
      popupElm.style.right = window.innerWidth - rect.left + 10 + 'px';
    } else {
      popupElm.style.left = rect.right + 10 + 'px';
    }
    return false; // Prevent default positioning
  }, []);

  // Native mouse events for the popup
  const handlePopupMouseEnter = useCallback(() => {
    clearTimeout(closeTimer.current);
  }, []);

  const handlePopupMouseLeave = useCallback(() => {
    closeTooltip();
  }, [closeTooltip]);

  const customResource = useCallback(
    (res: MbscResource) => (
      <div className="mbsc-flex">
        <img className="mds-resource-info-avatar" src={res.avatar} alt="Avatar" />
        <div className="mds-resource-info-details">
          <div className="mds-resource-info-title">{res.name}</div>
          <div className="mds-resource-info-profession">{res.profession}</div>
        </div>
      </div>
    ),
    [],
  );

  const handlePay = () => {
    setTooltipOpen(false);
    setToastMessage(currentResource!.name + ' paid');
    setToastOpen(true);
  };

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

  const handleTooltipClose = useCallback(() => {
    setTooltipOpen(false);
  }, []);

  return (
    <>
      <Eventcalendar
        clickToCreate={true}
        dragToCreate={true}
        dragToMove={true}
        dragToResize={true}
        eventDelete={true}
        ref={calInst}
        data={myEvents}
        resources={myResources}
        view={myView}
        renderResource={customResource}
        onResourceHoverIn={handleResourceHoverIn}
        onResourceHoverOut={handleResourceHoverOut}
        onPageChange={handlePageChange}
      />
      <Popup
        ref={popupInst}
        display="anchored"
        showOverlay={false}
        touchUi={false}
        width={280}
        isOpen={isTooltipOpen}
        onPosition={handleTooltipPosition}
        onClose={handleTooltipClose}
      >
        <div onMouseEnter={handlePopupMouseEnter} onMouseLeave={handlePopupMouseLeave}>
          <div className="mbsc-flex mbsc-align-items-center mds-resource-info-header">
            <img className="mds-resource-info-avatar" src={resourceAvatar} alt="Avatar" />
            <div className="mds-resource-info-name mbsc-flex-1-0">{resourceName}</div>
            <Button color="success" variant="outline" className="mds-resource-info-pay mbsc-button-xs" onClick={handlePay}>
              Pay
            </Button>
          </div>
          <div className="mds-resource-info-cont">
            <div>
              <span className="mbsc-txt-muted">Hourly rate </span>
              <span className="mds-resource-info-detail">{resourceCost}</span>
            </div>
            <div>
              <span className="mbsc-txt-muted">{resourceDate}</span>
              <span className="mds-resource-info-detail">{resourceTotal}</span>
            </div>
          </div>
        </div>
      </Popup>
      <Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
    </>
  );
};

export default App;
```

### CSS

```css
.mds-resource-info-avatar {
  width: 40px;
  height: 40px;
}
.mds-resource-info-details {
  margin: 0 8px;
}
.mds-resource-info-title {
  line-height: 24px;
}
.mds-resource-info-profession {
  font-size: 12px;
  opacity: 0.5;
  line-height: 18px;
}
.mds-resource-info-hover.mbsc-timeline-resource {
  text-decoration: underline;
}
.mds-resource-info-header {
  height: 35px;
}
.mds-resource-info-name {
  font-size: 18px;
  padding: 16px 10px;
}
.mds-resource-info-pay.mbsc-button {
  margin: 0;
  line-height: 24px;
  padding: 0 10px;
  will-change: opacity;
}
.mds-resource-info-pay .mbsc-button-txt {
  font-size: 12px;
}
.mds-resource-info-cont {
  font-size: 14px;
  line-height: 22px;
  padding-top: 23px;
}
.mbsc-ltr .mds-resource-info-detail {
  float: right;
}
.mbsc-rtl .mds-resource-info-detail {
  float: left;
}
```

## Implementation notes

- Use `timeline: { type: 'day', startTime: '07:00', endTime: '22:00' }` - a single-day view showing 7am-10pm.
- Define 8 worker resources, each with `id`, `name`, `color`, `profession`, `avatar` (image URL), and `cost` ($/hour).
- Add ~24 task events across yesterday/today/tomorrow with `dyndatetime` offsets and hourly start/end times.
- **Custom resource row** (`renderResource` / `resourceTemplate` / `resource` slot): renders the worker's avatar image alongside their name and profession.
- **Hover tooltip** - implemented with debounced open/close timers to avoid flickering and to allow the cursor to travel from the resource row into the popup without dismissal:
  - `onResourceHoverIn`: cancel both timers, start a 100ms open timer. When it fires: compute total scheduled hours for the resource (see below), add a CSS highlight class (`mds-resource-info-hover`) to `args.target`, populate tooltip state (avatar, name, hourly rate, formatted date, total hours + total cost), call `popupRef.position()`, then open the popup.
  - `onResourceHoverOut`: remove the highlight class from `args.target`, cancel the open timer, start a 200ms close timer.
  - On the popup's wrapping element: `onMouseEnter` cancels the close timer (keeps popup open); `onMouseLeave` starts the close timer again.
- **Popup**: `display: 'anchored'`, `showOverlay: false`, `touchUi: false`, `width: 280`. Content: avatar + name + a "Pay" button (outline/success) in the header row; below that, hourly rate and a line showing the current day's total hours and total cost (`hours * rate`).
- **Custom popup positioning** (`onPosition`): read the hovered resource element's `getBoundingClientRect()`, set the popup 10px to the right of the element (`rect.right + 10`) and aligned to its top (`rect.top - 10`). For RTL, use `window.innerWidth - rect.left + 10` for the right offset. Return `false` to suppress Mobiscroll's default positioning.
- **Total hours**: use `calRef.getEvents()`, filter by resource id, sum `(end - start) / (1000 * 60 * 60)` across all events visible on the current day.
- **Date display**: track the visible date via `onPageChange` (`args.firstDay`); format it with `formatDate('D DDD MMM YYYY', date)` for the tooltip.
- **"Pay" button**: closes the tooltip and shows a `Toast` with `"<name> paid"`.

## 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 Properties](https://demo.mobiscroll.com/react/timeline/resource-data-structure.md) - Define resource properties on the React Timeline — set up nested groups, fixed rows, and per-resource drag, resize, and overlap controls.
