---
name: aircall-blocks/migrate-dashboard/info-popup
description: >
  Migrate @dashboard/library InfoPopup (and the aw-web shared info-popup) to
  @aircall/ds HoverCard. Load when a file imports InfoPopup, InfoPopupTrigger,
  or InfoPopupContent from @dashboard/library or from the local shared info-popup.
type: sub-skill
library: aircall-blocks
requires:
  - aircall-blocks/setup
  - aircall-blocks/migrate-dashboard
sources:
  - "aircall/hydra:packages/ds/src/components/hover-card.tsx"
  - "aircall/hydra:packages/ds/src/index.ts"
---

This skill builds on aircall-blocks/migrate-dashboard.

## 1. Component mapping

| `@dashboard/library` / shared | `@aircall/ds` |
| --- | --- |
| `InfoPopup` (root) | `HoverCard` |
| `InfoPopupTrigger` | `HoverCardTrigger` |
| `InfoPopupContent` | `HoverCardContent` |

`HoverCard` is built on Base UI `PreviewCard`. The hover-grace period (cursor can
move from trigger into content without the card closing) is handled natively —
no manual timer cancellation needed.

## 2. Verified DS exports (`packages/ds/src/index.ts`)

```
HoverCard, HoverCardTrigger, HoverCardContent
```

## 3. Imports

```tsx
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@aircall/ds';
```

Icons (the trigger is usually an info icon):

```tsx
import { Info } from '@aircall/react-icons';
```

## 4. Prop mapping

### Root (`InfoPopup` → `HoverCard`)

| `InfoPopup` prop | `HoverCard` equivalent | Notes |
| --- | --- | --- |
| `side` | `<HoverCardContent side>` | Moved from root to content |
| `align` | `<HoverCardContent align>` | Moved from root to content |
| `mouseEnterDelay` | `<HoverCardTrigger delay>` | Default changes: 0 ms → 400 ms |
| `mouseLeaveDelay` | `<HoverCardTrigger closeDelay>` | Default changes: 100 ms → 300 ms |
| `open` | `<HoverCard open>` | Same |
| `defaultOpen` | `<HoverCard defaultOpen>` | Same |
| `onOpenChange` | `<HoverCard onOpenChange>` | Same |
| `data-test` | standard `data-test` on trigger or content | Pass directly to the element |

### Trigger (`InfoPopupTrigger` → `HoverCardTrigger`)

`InfoPopupTrigger` rendered children directly. `HoverCardTrigger` renders an `<a>`
by default — swap it for any element via the `render` prop:

```tsx
// info icon trigger
<HoverCardTrigger render={<span className="inline-flex cursor-help" />}>
  <Info size={16} />
</HoverCardTrigger>

// button trigger
<HoverCardTrigger render={<Button variant="ghost" size="sm" />}>
  Contact insights <Info />
</HoverCardTrigger>
```

### Content (`InfoPopupContent` → `HoverCardContent`)

`InfoPopupContent` accepted Tractor `BoxProps` (`maxW`, `p`, `backgroundColor`,
`borderRadius`, `boxShadow`). `HoverCardContent` is a plain `<div>` — use Tailwind:

| Old Tractor prop | Tailwind equivalent |
| --- | --- |
| `maxW="280px"` | `className="max-w-[280px]"` |
| `p="m"` | `className="p-4"` |
| `backgroundColor="surface-default"` | default `bg-popover` (no change needed) |
| `boxShadow={1}` | default `shadow-md` (no change needed) |
| `borderRadius="sm"` | default `rounded-md` (no change needed) |

## 5. Before / After examples

### 5a. Basic icon trigger

**Before (`@dashboard/library` / shared):**
```tsx
import { InfoPopup, InfoPopupTrigger, InfoPopupContent } from '@/components/shared/info-popup';
import { Icon } from '@aircall/tractor';
import { InformationOutlined } from '@aircall/icons';

<InfoPopup side="top" align="center">
  <InfoPopupTrigger>
    <Icon component={InformationOutlined} size={16} />
  </InfoPopupTrigger>
  <InfoPopupContent>
    <p>Contextual help text.</p>
  </InfoPopupContent>
</InfoPopup>
```

**After (`@aircall/ds`):**
```tsx
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@aircall/ds';
import { Info } from '@aircall/react-icons';

<HoverCard>
  <HoverCardTrigger render={<span className="inline-flex cursor-help" />}>
    <Info size={16} className="text-muted-foreground" />
  </HoverCardTrigger>
  <HoverCardContent side="top" align="center">
    <p>Contextual help text.</p>
  </HoverCardContent>
</HoverCard>
```

### 5b. With custom open delay (ContactInsightsWrapper pattern)

**Before:**
```tsx
<InfoPopup side="left" align="start" mouseEnterDelay={400}>
  <InfoPopupTrigger>
    <Flex gap="xxs" cursor="help">
      <Typography variant="supportingSemiboldS">{title}</Typography>
      <Icon component={InformationOutlined} size={16} />
    </Flex>
  </InfoPopupTrigger>
  <InfoPopupContent maxW="280px">
    <Flex flexDirection="column" gap="xs">
      <Typography variant="headingBoldXS">{heading}</Typography>
      <Typography variant="bodyRegularS">{body}</Typography>
    </Flex>
  </InfoPopupContent>
</InfoPopup>
```

**After:**
```tsx
<HoverCard>
  <HoverCardTrigger
    delay={400}
    closeDelay={100}
    render={<Button variant="ghost" size="sm" />}
  >
    {title}
    <Info />
  </HoverCardTrigger>
  <HoverCardContent side="left" align="start" className="max-w-[280px]">
    <div className="flex flex-col gap-2">
      <p className="text-xs font-bold">{heading}</p>
      <p className="text-xs text-muted-foreground">{body}</p>
    </div>
  </HoverCardContent>
</HoverCard>
```

## 6. Key behaviour differences

- **Hover grace** — moving the cursor from trigger into content keeps the card open
  natively. `InfoPopup` required manual timer cancellation.
- **Focus triggers** — `HoverCard` also opens on keyboard focus (Tab); `InfoPopup`
  was hover-only.
- **No provider needed** — neither `InfoPopup` nor `HoverCard` requires a provider.
- **Default delays** — `HoverCardTrigger` defaults to `delay=400` / `closeDelay=300`.
  If you need the original `InfoPopup` behaviour (instant open, 100 ms close), pass
  `delay={0} closeDelay={100}` explicitly.
