# Lightbox

## Usage

`Lightbox` opens media in a native `<dialog>`. Bind `isOpen` with `v-model:is-open`. A single `media` object is one image or video. An array is a gallery: previous, next, and a counter stay mounted, and the ends disable instead of disappearing.

`useLightbox({ media })` owns the open state and the index. Bind `bindings` onto `Lightbox`, and call `open(index)` from a thumbnail.

`hasZoom` turns an image into a button. Double-click, Enter, Space, `+`, and `-` toggle between 1× and 2×. While zoomed, drag or the arrow keys pan; each arrow moves 50px. A drag does not also close the dialog. Video items keep the browser controls and do not zoom. `hasAutoPlay` starts a video when that item is shown.

Escape and the close button emit `update:isOpen` with `false`. A backdrop click on the dialog or the frame does the same.

```vue
<script setup lang="ts">
import { Lightbox, useLightbox } from '@astryx-vue/core/Lightbox'
import { Thumbnail } from '@astryx-vue/core/Thumbnail'

const photo = {
  src: '/template-assets/light-scene-horizontal-1.png',
  alt: 'Coastal shoreline with ocean waves',
  caption: 'A scenic coastline with waves rolling onto a sandy beach beneath a clear sky.',
}
const { bindings, open } = useLightbox({ media: photo })
</script>

<template>
  <Thumbnail :src="photo.src" :alt="photo.alt" :label="photo.alt" @click="open()" />
  <Lightbox v-bind="bindings" />
</template>
```

### Anatomy

- **Viewer overlay**: Full-viewport dialog overlay that contains the active media and controls.
- **Media**: Active image or video presented inside the viewer.
- **Close button**: Button that closes the viewer.
- **Previous button**: Gallery button that moves to the previous media item.
- **Next button**: Gallery button that moves to the next media item.
- **Caption**: Caller-provided caption displayed below the active media.
- **Counter**: Current position and total shown in gallery mode.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Write an `alt` that names the picture, and a `caption` that adds what the picture itself does not say. |
| Do | Open a gallery at the thumbnail the reader chose, with `open(index)`. |
| Do | Turn on `hasZoom` only for a still image that has detail worth inspecting. |
| Don't | Put `hasZoom` on a video. The controls stay native, and zoom is ignored. |
| Don't | Leave `alt` empty. The dialog name and the image name both come from it. |

## Examples

### Gallery

A thumbnail grid that opens a fullscreen gallery. Clicking any thumbnail opens the lightbox at that index. Previous and next browse the rest without closing.

```vue
<script setup lang="ts">
import { Grid } from '@astryx-vue/core/Grid'
import { Lightbox, useLightbox } from '@astryx-vue/core/Lightbox'
import { Thumbnail } from '@astryx-vue/core/Thumbnail'

const photos = [
  {
    src: '/template-assets/Neutral-Backpack.png',
    alt: 'Backpack',
    caption: 'A backpack displayed on a neutral background.',
  },
  {
    src: '/template-assets/building.png',
    alt: 'Modern building',
    caption: 'A modern building with a contemporary architectural design.',
  },
  {
    src: '/template-assets/light-scene-horizontal-1.png',
    alt: 'Coastal shoreline with ocean waves',
    caption: 'A scenic coastline with waves rolling onto a sandy beach beneath a clear sky.',
  },
  {
    src: '/template-assets/illustrative-vertical-1.png',
    alt: 'Illustrated lakeside landscape at sunset',
    caption: 'A stylized landscape illustration featuring pink clouds reflected over a calm lake at sunset.',
  },
]

const { bindings, open } = useLightbox({ media: photos })
</script>

<template>
  <Grid :columns="2" :gap="2" :width="136">
    <Thumbnail
      v-for="(photo, index) in photos"
      :key="photo.src"
      :src="photo.src"
      :alt="photo.alt"
      :label="photo.alt"
      @click="open(index)"
    />
  </Grid>
  <Lightbox v-bind="bindings" />
</template>
```

### Video

Opens a video with the browser controls. Zoom and pan stay off for video.

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

const isOpen = ref(false)
</script>

<template>
  <Button label="Play video" @click="isOpen = true" />
  <Lightbox
    :is-open="isOpen"
    :media="{
      src: '/template-assets/Nature-1.mp4',
      alt: 'Flower blooming in time-lapse',
      type: 'video',
      caption: 'A flower blooming in time-lapse',
    }"
    @update:is-open="isOpen = $event"
  />
</template>
```

### Zoom

Double-click the image to zoom in, then drag to pan. Double-click again, or close, to leave zoom.

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

const isOpen = ref(false)
</script>

<template>
  <Thumbnail
    src="/template-assets/light-scene-horizontal-1.png"
    alt="Coastal shoreline with ocean waves"
    label="Coastal shoreline with ocean waves"
    @click="isOpen = true"
  />
  <Lightbox
    :is-open="isOpen"
    has-zoom
    :media="{
      src: '/template-assets/light-scene-horizontal-1.png',
      alt: 'Coastal shoreline with ocean waves',
      caption: 'A scenic coastline. Double-click to zoom in and drag to pan.',
    }"
    @update:is-open="isOpen = $event"
  />
</template>
```

## Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `isOpen` | `boolean` | — | Whether the dialog is open. Bind with `v-model:is-open`. |
| `media` | `LightboxMedia \| readonly LightboxMedia[]` | — | One item, or a gallery. An array shows previous, next, and a counter. |
| `index` | `number` | — | Controlled gallery index. Bind with `v-model:index`. |
| `defaultIndex` | `number` | `0` | Initial index when `index` is omitted. |
| `hasZoom` | `boolean` | `false` | Double-click, Enter, Space, `+`, and `-` zoom images. Arrow keys pan while zoomed. |
| `hasAutoPlay` | `boolean` | `false` | Start video playback when a video item is shown. |

`LightboxMedia` is `{ src, alt, caption?, type? }`. `type` is `image` or `video`, and defaults to `image`.

## Events

| Name | Payload | Description |
| --- | --- | --- |
| `update:isOpen` | `boolean` | Close, Escape, or a backdrop click asks to change the open state. |
| `update:index` | `number` | Previous, next, or an arrow key while zoomed out asks for a new gallery index. |

## useLightbox

```ts
const { bindings, open, close, isOpen, index, getTriggerProps } = useLightbox({
  media,
  hasZoom: false,
  hasAutoPlay: false,
})
```

`bindings` is the set of props and update listeners for `v-bind`. `open(index)` sets the index and opens the dialog. `getTriggerProps(index)` returns `role="button"`, `tabindex="0"`, `aria-haspopup="dialog"`, and keyboard activation for a custom trigger.

## Theme

The dialog class is `astryx-lightbox`. Caption text uses the large type, `on-dark`, a max width of 600px, and padding of spacing 2 on the block start and spacing 3 on the inline sides. Close and navigation sit at spacing 3 from the frame.
