# useContainerReveal

## Usage

A headless hover/focus reveal primitive. Gives a container a scoped trigger that reveals (or conceals) content inside it when the container is hovered or receives keyboard focus: the classic "row actions appear on hover" pattern. The reveal is CSS-only: no hover state lives in Vue and hovering never triggers a re-render. The caller authors no CSS for the reveal itself; the hook hands out the container and content classes, and a nested container shadows its ancestor, so nested containers never leak hover/focus into one another. Accessible by construction: revealed content is visually hidden at rest with position and opacity (never `display: none`), so it stays mounted, keeps its place in the tab order, and is announced to assistive technology; it reveals on `:focus-within` so keyboard users see it when tabbing in, stays visible on touch (never gated behind hover on coarse pointers), and honors `prefers-reduced-motion`.

`containerProps()` returns `class` and `style` for the container; `contentProps()` returns the `class` for each revealed child. Bind them with `:class` and `:style`.

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

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Destructure `containerProps` and `contentProps`; bind `containerProps()` on the container (alongside your own classes) and `contentProps()` on the content to reveal. |
| Do | Use for secondary affordances: reveal-on-hover row actions (edit/copy/remove on list or table rows) and overlay controls on a card or media tile (e.g. `Thumbnail`'s remove button). |
| Do | Gate the reveal with `isEnabled` when a consumer prop decides whether content is revealed on hover or always shown; it can change at any time. |
| Do | Pass `isLayoutPreserved` for absolutely-positioned or overlay content to reserve its box and avoid layout shift when it appears. |
| Do | Set a `hoverDelay` (100-250ms) on rows in a long list, so a cursor travelling across the list does not light up every row it passes; keyboard and touch still reveal immediately. |
| Do | Reach for `forceState` when something other than the pointer owns the interaction (a drag, a scroll or motion gate, an open row menu), and `forceVisibility` when just one element should ignore the container. |
| Don't | Reach past the API into the hook's private custom properties (`--_reveal-opacity` and friends) to suppress a reveal; use `forceState` / `forceVisibility`, which survive a rename. |
| Don't | Use it to hide content that must always be discoverable; keep essential actions visible instead of gating them behind hover. |

## Examples

Common configurations, variations, and states.

### useContainerReveal — Reveal-on-hover Row Actions

File rows keep their edit and delete actions hidden at rest and reveal them on hover or keyboard focus via useContainerReveal; the actions stay mounted and in the tab order.

```vue
<script setup lang="ts">
import { useContainerReveal } from '@astryx-vue/core'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { ref } from 'vue'

const files = ref([
  { name: 'report.pdf', meta: 'Edited 2 hours ago' },
  { name: 'budget.xlsx', meta: 'Edited 2 hours ago' },
  { name: 'notes.txt', meta: 'Edited 2 hours ago' },
])

const reveal = useContainerReveal()
const container = reveal.containerProps({ hoverDelay: 150 })
const content = reveal.contentProps()

function remove(name: string): void {
  files.value = files.value.filter(file => file.name !== name)
}
</script>

<template>
  <ul class="reveal-preview">
    <li
      v-for="file in files"
      :key="file.name"
      class="reveal-preview__row"
      :class="container.class"
      :style="container.style"
    >
      <span class="reveal-preview__label">
        <span class="reveal-preview__name">{{ file.name }}</span>
        <span class="reveal-preview__meta">{{ file.meta }}</span>
      </span>
      <span class="reveal-preview__actions" :class="content.class">
        <IconButton label="Edit" size="sm" variant="ghost">
          <Icon icon="i-heroicons-pencil-square" />
        </IconButton>
        <IconButton label="Delete" size="sm" variant="ghost" @click="remove(file.name)">
          <Icon icon="i-heroicons-trash" />
        </IconButton>
      </span>
    </li>
  </ul>
</template>

<style scoped>
.reveal-preview {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  max-width: 420px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.reveal-preview__row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 44px;
  padding: 6px 10px;
  border-radius: 8px;
}

.reveal-preview__row:hover {
  background: var(--astryx-color-background-muted);
}

.reveal-preview__label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.reveal-preview__name {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--astryx-color-text-primary);
}

.reveal-preview__meta {
  font-size: 0.75rem;
  color: var(--astryx-color-text-secondary);
}

.reveal-preview__actions {
  display: flex;
  align-items: center;
  gap: 2px;
  top: 0;
  bottom: 0;
  right: 10px;
}
</style>
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `UseContainerRevealOptions` | `{}` | Configuration object for the reveal container. Optional. |
| `options.isEnabled` | `MaybeRefOrGetter<boolean>` | `true` | When false the hook is inert: the container gets no classes and the content getters return nothing, so content is always shown. Read on every call, so a component can flip it after mount (e.g. `revealOn === "hover"`). |
| `containerProps(options?).hoverDelay` | `number` | — | Milliseconds the pointer must dwell before the reveal starts: a hover-intent gate like Tooltip's and HoverCard's delay, so a cursor sweeping across a list leaves nothing painted behind it. Keyboard and coarse pointers never wait. |
| `containerProps(options?).forceState` | `'active' \| 'inactive'` | — | Pins the trigger state when the caller owns it: a motion gate, a scroll, or a row whose menu is open. `'inactive'` still yields to keyboard focus and coarse pointers. |
| `contentProps(options?).isRevealInverted` | `boolean` | `false` | Conceal on hover instead of reveal on hover. Pointer only; keyboard focus and touch still show the content. |
| `contentProps(options?).isLayoutPreserved` | `boolean` | `false` | Reserves the layout box while hidden (opacity only) and avoids layout shift. |
| `contentProps(options?).forceVisibility` | `'shown' \| 'hidden'` | — | Pins this one element's appearance whatever the container is doing. `'hidden'` yields to focus. |

## Returns

| Name | Type | Description |
| --- | --- | --- |
| `containerProps` | `(options?: ContainerRevealOptions) => { class?: string \| Record<string, boolean>, style?: CSSProperties }` | Bind onto the container whose hover and focus-within drive the reveal. Accepts `hoverDelay` and `forceState`. Returns an empty object when the hook is disabled. |
| `contentProps` | `(options?: ContentRevealOptions) => { class?: string \| Record<string, boolean> }` | Bind onto each revealed or concealed child. Accepts `isRevealInverted`, `isLayoutPreserved`, and `forceVisibility`. Returns an empty object when the hook is disabled. |
