# useHoverCard

## Usage

Headless hook for hover-triggered floating cards. Builds on `useLayer` with hover/focus intent detection, configurable delays, safe hover behavior, and accessible `aria-describedby` linking. Use for rich previews on hover when you need full control over the trigger or rendered content.

```ts
import { useHoverCard } from '@astryx-vue/core/HoverCard'
```

Bind `triggerRef` to the element that reveals the card. The hook attaches the pointer, focus, and touch handlers and keeps the card open while the pointer travels into it. Render the surface yourself and spread `contentProps` on it:

```vue
<script setup lang="ts">
import { useHoverCard } from '@astryx-vue/core/HoverCard'

const card = useHoverCard({ label: 'Alex Morgan' })
</script>

<template>
  <button :ref="card.triggerRef" type="button">
    Hover profile
  </button>
  <Teleport to="body">
    <div
      v-show="card.isOpen"
      :ref="card.contentRef"
      v-bind="card.contentProps"
      :style="card.contentStyle"
    >
      Alex Morgan
    </div>
  </Teleport>
</template>
```

With a `label` the card is exposed as a named `role="dialog"` and the trigger advertises it with `aria-haspopup` / `aria-controls` / `aria-expanded`. Without one it falls back to `role="group"` and an `aria-describedby` link. `contentProps` carries the role, the label, and the handlers that keep the card open.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use for rich content previews such as user profiles, entity summaries, and link previews. |
| Do | Prefer the `HoverCard` component for standard trigger-content pairs; use the hook for custom trigger patterns. |
| Don't | Use for simple text hints: use `Tooltip` or `useTooltip` instead. |

## Examples

Custom profile preview built with `useHoverCard`, with direct control over the trigger and the rendered content.

### useHoverCard — Profile Preview

Custom profile preview using useHoverCard with direct trigger and render control.

```vue
<script setup lang="ts">
import { useHoverCard } from '@astryx-vue/core/HoverCard'

const card = useHoverCard({ label: 'Alex Morgan', delay: 150 })
const { isOpen, contentStyle, contentProps } = card
</script>

<template>
  <div class="hook-hover-card">
    <button :ref="card.triggerRef" type="button" class="hook-hover-card__trigger">
      Hover profile
    </button>

    <Teleport to="body">
      <div
        v-show="isOpen"
        :ref="card.contentRef"
        v-bind="contentProps"
        class="hook-hover-card__surface"
        :style="contentStyle"
      >
        <p class="hook-hover-card__name">
          Alex Morgan
        </p>
        <p class="hook-hover-card__role">
          Staff designer · Product systems
        </p>
        <p class="hook-hover-card__bio">
          Owns interaction patterns for overlays and navigation.
        </p>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.hook-hover-card {
  display: flex;
  justify-content: center;
}

.hook-hover-card__trigger {
  padding: 8px 16px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-control, 8px);
  background: var(--astryx-color-background-surface, #fff);
  color: var(--astryx-color-text-primary, #171717);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.hook-hover-card__surface {
  position: fixed;
  z-index: 1000;
  box-sizing: border-box;
  width: 16rem;
  padding: 14px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-container, 12px);
  background: var(--astryx-color-background-surface, #fff);
  box-shadow: 0 4px 8px oklch(0 0 0 / 10%);
  color: var(--astryx-color-text-primary, #171717);
  font-size: 0.875rem;
}

.hook-hover-card__name {
  margin: 0;
  font-weight: 600;
}

.hook-hover-card__role {
  margin: 2px 0 8px;
  color: var(--astryx-color-text-secondary, #525252);
  font-size: 0.75rem;
}

.hook-hover-card__bio {
  margin: 0;
  line-height: 1.5;
}
</style>
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `placement` | `'above' \| 'below' \| 'start' \| 'end' \| MaybeRefOrGetter` | `'above'` | Side of the trigger the card is placed on. Logical: `start` / `end` mirror under RTL. Flips when there is no room. |
| `alignment` | `'start' \| 'center' \| 'end' \| MaybeRefOrGetter` | `'center'` | Alignment along the placement axis. |
| `delay` | `number \| MaybeRefOrGetter` | `300` | Delay before showing the hover card on hover, in milliseconds. |
| `hideDelay` | `number \| MaybeRefOrGetter` | `200` | Delay before hiding after the pointer or focus leaves, so the pointer can travel into the card. |
| `focusTrigger` | `'auto' \| 'always' \| 'never' \| MaybeRefOrGetter` | `'auto'` | When focus opens the card. `auto` only attaches focus listeners to naturally focusable elements. |
| `touchTrigger` | `'auto' \| 'tap' \| 'none' \| MaybeRefOrGetter` | `'auto'` | What a tap does where there is no hover. `auto` opens on tap unless the trigger performs an action of its own; `tap` always opens; `none` never opens on touch. |
| `isEnabled` | `boolean \| MaybeRefOrGetter` | `true` | Whether the hover and focus triggers are live. |
| `label` | `string \| undefined \| MaybeRefOrGetter` | — | Accessible name for the card. When provided the card is exposed as a named `role="dialog"`; when omitted it falls back to `role="group"`, which may validly be unnamed. |
| `isOpen` | `boolean \| undefined \| MaybeRefOrGetter` | — | Controlled open state. `true` force-shows, `false` force-hides, `undefined` lets hover and focus manage visibility. |
| `isDefaultOpen` | `boolean` | `false` | Whether the card is shown on mount. |
| `onShow` | `() => void` | — | Called when the card becomes visible. |
| `onHide` | `() => void` | — | Called when the card is hidden. |

## Returns

| Field | Type | Description |
| --- | --- | --- |
| `isOpen` | `Ref<boolean>` | Whether the card is showing. |
| `triggerRef` | `Ref` | Trigger element. Bind it with `:ref`; the hook attaches the handlers and ARIA relationships. |
| `contentRef` | `Ref` | Card container. |
| `id` | `string` | Unique ID of the card. |
| `anchorId` | `string` | CSS identifier for the anchor element, for advanced positioning cases. |
| `describedBy` | `string` | ID to compose into `aria-describedby`. The hook already writes it onto `triggerRef`; it is here for hand-written ARIA. |
| `contentStyle` | `ComputedRef` | Inline coordinates for the card. |
| `resolvedPlacement` | `ComputedRef<'above' \| 'below' \| 'start' \| 'end'>` | Placement actually in effect, after flipping for available space. |
| `isDialog` | `ComputedRef<boolean>` | Whether the card is exposed as a named dialog rather than an unnamed group. |
| `contentProps` | `ComputedRef<Record<string, unknown>>` | Attributes for the card: `id`, `role`, `aria-label`, and the pointer/focus handlers that keep it open. |
| `show` | `() => void` | Imperatively show the card immediately. |
| `hide` | `() => void` | Imperatively hide the card immediately. |
