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

# Resource Hover Tooltip - Vue Timeline demo

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

- [Live demo](https://demo.mobiscroll.com/vue/timeline/display-resource-information-on-hover)
- [View SFC JS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue/tree/main/src/demos/eventcalendar/timeline/display-resource-information-on-hover)
- [View SFC TS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue-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), [React](https://demo.mobiscroll.com/react/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 [`resource-hover-in`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onResourceHoverIn) and [`resource-hover-out`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onResourceHoverOut) [lifecycle events](https://demo.mobiscroll.com/vue/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/vue/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/vue/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-themeVariant) - option
- [`getEvents`](https://mobiscroll.com/docs/vue/eventcalendar/api#method-getEvents) - method
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToResize) - option
- [`eventDelete`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-eventDelete) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-resources) - option
- [`resource-hover-in`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onResourceHoverIn) - event
- [`resource-hover-out`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onResourceHoverOut) - event
- [`page-change`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onPageChange) - event
- [`display`](https://mobiscroll.com/docs/vue/popup/api#opt-display) - option
- [`anchor`](https://mobiscroll.com/docs/vue/popup/api#opt-anchor) - option
- [`showOverlay`](https://mobiscroll.com/docs/vue/popup/api#opt-showOverlay) - option
- [`touchUi`](https://mobiscroll.com/docs/vue/popup/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/vue/popup/api#opt-width) - option
- [`isOpen`](https://mobiscroll.com/docs/vue/popup/api#opt-isOpen) - option
- [`close`](https://mobiscroll.com/docs/vue/popup/api#event-onClose) - event
- [`cssClass`](https://mobiscroll.com/docs/vue/forms/button#opt-cssClass) - option
- [`close`](https://mobiscroll.com/docs/vue/notifications/toast#event-onClose) - event

## Code

### SFC JS

```vue
<script setup>
import {
  formatDate,
  MbscButton,
  MbscEventcalendar,
  MbscPopup,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import { ref } from 'vue'

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

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

const resourceAvatar = ref('')
const resourceName = ref('')
const resourceCost = ref('')
const resourceDate = ref('')
const resourceTotal = ref('')
const currentResource = ref(null)
const tooltipAnchor = ref(null)
const isTooltipOpen = ref(false)
const isToastOpen = ref(false)
const toastMessage = ref('')

const tooltipRef = ref(null)
const calRef = ref(null)
const hoveredResourceElmRef = ref(null)
const mySelectedDate = ref(new Date())
const openTimer = ref(null)
const closeTimer = ref(null)

const myView = {
  timeline: {
    type: 'day',
    startTime: '07:00',
    endTime: '22:00'
  }
}

function getTotalHoursForResource(resourceId) {
  return calRef.value.instance
    .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
function openTooltip(resource, target) {
  clearTimeout(closeTimer.value)
  clearTimeout(openTimer.value)

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

    currentResource.value = resource
    hoveredResourceElmRef.value = target

    resourceAvatar.value = resource.avatar
    resourceName.value = resource.name
    resourceCost.value = '$' + resource.cost
    resourceDate.value = formatDate('D DDD MMM YYYY', mySelectedDate.value)
    resourceTotal.value = totalHours + 'h, $' + totalHours * resource.cost
    target.classList.add('mds-resource-info-hover')

    tooltipAnchor.value = target
    isTooltipOpen.value = true

    openTimer.value = null
  }, 100)
}

function closeTooltip() {
  clearTimeout(openTimer.value)
  clearTimeout(closeTimer.value)

  closeTimer.value = setTimeout(() => {
    isTooltipOpen.value = false
    closeTimer.value = null
  }, 200)
}

// Mobiscroll resource hover events
function handleResourceHoverIn(args) {
  openTooltip(args.resource, args.target)
}

function handleResourceHoverOut(args) {
  args.target.classList.remove('mds-resource-info-hover')
  closeTooltip()
}

function handlePageChange(args) {
  mySelectedDate.value = args.firstDay
}

// Native mouse events for the popup
function handlePopupMouseEnter() {
  clearTimeout(closeTimer.value)
}

function handlePopupMouseLeave() {
  closeTooltip()
}

function handlePay() {
  isTooltipOpen.value = false
  toastMessage.value = currentResource.value.name + ' paid'
  isToastOpen.value = true
}
</script>

<template>
  <MbscEventcalendar :clickToCreate="true" :dragToCreate="true" :dragToMove="true" :dragToResize="true" :eventDelete="true"
    ref="calRef"
    :view="myView"
    :data="myEvents"
    :resources="myResources"
    @resource-hover-in="handleResourceHoverIn"
    @resource-hover-out="handleResourceHoverOut"
    @page-change="handlePageChange"
  >
    <template #resource="resource">
      <div class="mbsc-flex">
        <img class="mds-resource-info-avatar" :src="resource.avatar" alt="Avatar" />
        <div class="mds-resource-info-details">
          <div class="mds-resource-info-title">{{ resource.name }}</div>
          <div class="mds-resource-info-profession">{{ resource.profession }}</div>
        </div>
      </div>
    </template>
  </MbscEventcalendar>
  <MbscPopup
    ref="tooltipRef"
    display="anchored"
    :anchor="tooltipAnchor"
    :showOverlay="false"
    :touchUi="false"
    :width="280"
    :isOpen="isTooltipOpen"
    @close="isTooltipOpen = false"
  >
    <div @mouseenter="handlePopupMouseEnter" @mouseleave="handlePopupMouseLeave">
      <div class="mbsc-flex mbsc-align-items-center mds-resource-info-header">
        <img class="mds-resource-info-avatar" :src="resourceAvatar" alt="Avatar" />
        <div class="mds-resource-info-name mbsc-flex-1-0">{{ resourceName }}</div>
        <MbscButton
          color="success"
          variant="outline"
          cssClass="mds-resource-info-pay mbsc-button-xs"
          @click="handlePay"
        >
          Pay
        </MbscButton>
      </div>
      <div class="mds-resource-info-cont">
        <div>
          <span class="mbsc-txt-muted">Hourly rate </span>
          <span class="mds-resource-info-detail">{{ resourceCost }}</span>
        </div>
        <div>
          <span class="mbsc-txt-muted">{{ resourceDate }}</span>
          <span class="mds-resource-info-detail">{{ resourceTotal }}</span>
        </div>
      </div>
    </div>
  </MbscPopup>
  <MbscToast :message="toastMessage" @close="isToastOpen = false" />
</template>

<style>
.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;
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import {
  formatDate,
  MbscButton,
  MbscEventcalendar,
  MbscPopup,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import type {
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscPageChangeEvent,
  MbscResource,
  MbscResourceHoverEvent
} from '@mobiscroll/vue'
import { ref } from 'vue'

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

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

const resourceAvatar = ref<string>('')
const resourceName = ref<string | undefined>('')
const resourceCost = ref<string>('')
const resourceDate = ref<string>('')
const resourceTotal = ref<string>('')
const currentResource = ref<MbscResource>()
const tooltipAnchor = ref<HTMLElement>()
const isTooltipOpen = ref<boolean>(false)
const isToastOpen = ref<boolean>(false)
const toastMessage = ref<string>('')

const tooltipRef = ref<typeof MbscPopup>()
const calRef = ref<typeof MbscEventcalendar>()
const hoveredResourceElmRef = ref<HTMLElement>()
const mySelectedDate = ref<Date>(new Date())
const openTimer = ref<ReturnType<typeof setTimeout>>()
const closeTimer = ref<ReturnType<typeof setTimeout>>()

const myView: MbscEventcalendarView = {
  timeline: {
    type: 'day',
    startTime: '07:00',
    endTime: '22:00'
  }
}

function getTotalHoursForResource(resourceId: string | number) {
  return calRef
    .value!.instance.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
function openTooltip(resource: MbscResource, target: HTMLElement) {
  clearTimeout(closeTimer.value)
  clearTimeout(openTimer.value)

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

    currentResource.value = resource
    hoveredResourceElmRef.value = target

    resourceAvatar.value = resource.avatar
    resourceName.value = resource.name
    resourceCost.value = '$' + resource.cost
    resourceDate.value = formatDate('D DDD MMM YYYY', mySelectedDate.value)
    resourceTotal.value = totalHours + 'h, $' + totalHours * resource.cost
    target.classList.add('mds-resource-info-hover')

    tooltipAnchor.value = target
    isTooltipOpen.value = true

    openTimer.value = undefined
  }, 100)
}

function closeTooltip() {
  clearTimeout(openTimer.value)
  clearTimeout(closeTimer.value)

  closeTimer.value = setTimeout(() => {
    isTooltipOpen.value = false
    closeTimer.value = undefined
  }, 200)
}

// Mobiscroll resource hover events
function handleResourceHoverIn(args: MbscResourceHoverEvent) {
  openTooltip(args.resource, args.target)
}

function handleResourceHoverOut(resource: MbscResourceHoverEvent) {
  resource.target.classList.remove('mds-resource-info-hover')
  closeTooltip()
}

function handlePageChange(args: MbscPageChangeEvent) {
  mySelectedDate.value = args.firstDay
}

// Native mouse events for the popup
function handlePopupMouseEnter() {
  clearTimeout(closeTimer.value)
}

function handlePopupMouseLeave() {
  closeTooltip()
}

function handlePay() {
  isTooltipOpen.value = false
  toastMessage.value = currentResource.value!.name + ' paid'
  isToastOpen.value = true
}

function handleTooltipClose() {
  isTooltipOpen.value = false
}
</script>

<template>
  <MbscEventcalendar :clickToCreate="true" :dragToCreate="true" :dragToMove="true" :dragToResize="true" :eventDelete="true"
    ref="calRef"
    :view="myView"
    :data="myEvents"
    :resources="myResources"
    @resource-hover-in="handleResourceHoverIn"
    @resource-hover-out="handleResourceHoverOut"
    @page-change="handlePageChange"
  >
    <template #resource="resource">
      <div class="mbsc-flex">
        <img class="mds-resource-info-avatar" :src="resource.avatar" alt="Avatar" />
        <div class="mds-resource-info-details">
          <div class="mds-resource-info-title">{{ resource.name }}</div>
          <div class="mds-resource-info-profession">{{ resource.profession }}</div>
        </div>
      </div>
    </template>
  </MbscEventcalendar>
  <MbscPopup
    ref="tooltipRef"
    display="anchored"
    :anchor="tooltipAnchor"
    :showOverlay="false"
    :touchUi="false"
    :width="280"
    :isOpen="isTooltipOpen"
    @close="isTooltipOpen = false"
  >
    <div @mouseenter="handlePopupMouseEnter" @mouseleave="handlePopupMouseLeave">
      <div class="mbsc-flex mbsc-align-items-center mds-resource-info-header">
        <img class="mds-resource-info-avatar" :src="resourceAvatar" alt="Avatar" />
        <div class="mds-resource-info-name mbsc-flex-1-0">{{ resourceName }}</div>
        <MbscButton
          color="success"
          variant="outline"
          cssClass="mds-resource-info-pay mbsc-button-xs"
          @click="handlePay"
        >
          Pay
        </MbscButton>
      </div>
      <div class="mds-resource-info-cont">
        <div>
          <span class="mbsc-txt-muted">Hourly rate </span>
          <span class="mds-resource-info-detail">{{ resourceCost }}</span>
        </div>
        <div>
          <span class="mbsc-txt-muted">{{ resourceDate }}</span>
          <span class="mds-resource-info-detail">{{ resourceTotal }}</span>
        </div>
      </div>
    </div>
  </MbscPopup>
  <MbscToast :message="toastMessage" @close="isToastOpen = false" />
</template>

<style>
.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;
}
</style>
```

## 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-vue.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 vue
   ```

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 vue --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/vue/getting-started/installation

## Related demos

- Previous: [Resource Properties](https://demo.mobiscroll.com/vue/timeline/resource-data-structure.md) - Define resource properties on the Vue Timeline — set up nested groups, fixed rows, and per-resource drag, resize, and overlap controls.
