# API reference

Complete API reference

## Entry points

| Import | What it is |
| --- | --- |
| `@ogdakke/fluidbox` | The plain JavaScript API: `createFluidbox`, `createItemList`, `imageItem`, the event classes and every type. Safe to import on the server, with no side effects. |
| `@ogdakke/fluidbox/elements` | The custom elements, `<fluidbox-gallery>` and its children, defined on import. Browser only. |
| `@ogdakke/fluidbox/react` | The React binding: the `Fluidbox` parts and the `useFluidbox` and `useFluidboxSheet` hooks. |
| `@ogdakke/fluidbox/solid` | The Solid binding: the `Fluidbox` parts and the `useFluidbox` and `useFluidboxSheet` primitives. |
| `@ogdakke/fluidbox/vue` | The Vue binding: the `Fluidbox` components and the `useFluidbox` and `useFluidboxSheet` composables. |
| `@ogdakke/fluidbox/angular` | The Angular binding: standalone `fluidbox*` directives and `FluidboxModule`. |
| `@ogdakke/fluidbox/filmstrip` | The filmstrip add-on. Importing it turns on the `filmstrip` option and the bindings' `Filmstrip` part. Its stylesheet is `@ogdakke/fluidbox/filmstrip.css`. |
| `@ogdakke/fluidbox/sheet` | The sheet add-on. Importing it turns on the `sheet` option and the bindings' `Sheet` part. Its stylesheet is `@ogdakke/fluidbox/sheet.css`. |
| `@ogdakke/fluidbox/kit` | The binding kit: what the framework bindings build their parts from. Each binding uses only this and the plain API, never the custom elements. |
| `@ogdakke/fluidbox/styles.css` | The viewer's layout and default look. |
| `@ogdakke/fluidbox/sheet.css` | The sheet's layout and variables. |
| `@ogdakke/fluidbox/filmstrip.css` | The filmstrip's layout and variables. |

```ts
import {
  createFluidbox,
  createItemList,
  idOf,
  itemLabel,
  imageItem,
  FluidboxItemChangeEvent,
  FluidboxSheetEvent,
  FluidboxToggleEvent,
} from "@ogdakke/fluidbox";
```

## createFluidbox(options)

```ts
/**
 * Create a fluidbox: a viewer at the end of `<body>`, and the commands, state and
 * events that drive it. Browser only. Throws a TypeError for an array of items
 * with an empty or duplicate id.
 */
function createFluidbox<TItem extends FluidboxItem = FluidboxItem>(
  options: FluidboxOptions<TItem>,
): Fluidbox<TItem>;

/** Every option but `items`, `renderers` and `origins` can change later through `update`. */
interface FluidboxOptions<TItem extends FluidboxItem = FluidboxItem> {
  readonly items: FluidboxItems<TItem>;
  /** Your strings for the viewer, over the defaults. */
  readonly labels?: Partial<FluidboxLabels>;
  /**
   * Pinch, double-tap, wheel and keyboard zoom of images. `step` is the scale of a
   * double-click or double-tap, and `max` the largest scale for pinch, wheel and
   * keyboard. The `+` and `-` keys zoom by 1.5. `false` turns zoom off.
   * @default { step: 2, max: 4 }
   */
  readonly zoom?: false | { readonly step?: number; readonly max?: number };
  /** Draw `custom` items, by the name in their `render`. Read when a slide mounts. */
  readonly renderers?: Readonly<Record<string, FluidboxRenderer<TItem>>>;
  /**
   * Go round: past the last item comes the first, and before the first the
   * last, by swipe, key, button and filmstrip alike. A single item never loops.
   * @default false
   */
  readonly loop?: boolean;
  /** Draw your own controls and captions into the viewer. */
  readonly viewer?: FluidboxViewerRenderer<TItem>;
  /**
   * The default Previous, Next and Close buttons, or some of them.
   * @default true without a `viewer`, else false
   */
  readonly controls?: boolean | readonly FluidboxControlName[];
  /**
   * A thumbnail strip. Needs `import "@ogdakke/fluidbox/filmstrip"` and its
   * stylesheet. Without the import, it throws an Error that names it.
   */
  readonly filmstrip?: boolean | FluidboxFilmstripOptions<TItem>;
  /**
   * A sheet over the photo. Needs `import "@ogdakke/fluidbox/sheet"` and its
   * stylesheet. Without the import, it throws an Error that names it.
   */
  readonly sheet?: boolean | FluidboxSheetOptions<TItem>;
  /** Find origins the viewer can't see, as in a virtualized grid. Read on each use. */
  readonly origins?: FluidboxOriginAdapter<TItem>;
  /**
   * When the viewer's code loads.
   * @default "hover"
   */
  readonly loading?: FluidboxLoading;
}

type FluidboxControlName = "previous" | "next" | "close";
```

## Fluidbox

```ts
/**
 * Commands resolve true once they happened and their motion settled, and false
 * when they were prevented, superseded by a later command, aborted, or had
 * nothing to do. An unknown id or an index out of range rejects with a
 * RangeError.
 */
interface Fluidbox<TItem extends FluidboxItem = FluidboxItem> extends EventTarget {
  readonly state: FluidboxState<TItem>;
  /** The items it was made with, or the last `setItems`, as a list. */
  readonly items: FluidboxItemList<TItem>;
  /** The sheet, when the `sheet` option is on. */
  readonly sheet: FluidboxSheet | null;
  /**
   * Open the viewer on an item, or move to it while open. Without a target, it
   * opens the current item, or the first if there is none.
   */
  open(target?: FluidboxTarget<TItem>, options?: FluidboxCommandOptions): Promise<boolean>;
  close(options?: FluidboxCommandOptions): Promise<boolean>;
  next(options?: FluidboxCommandOptions): Promise<boolean>;
  previous(options?: FluidboxCommandOptions): Promise<boolean>;
  /**
   * Change options without closing the viewer. Labels and zoom apply in place, so
   * a zoomed photo zooms out when zoom turns off. A change of `controls`, `viewer`
   * or the labels redraws the viewer's layer, and a change of `filmstrip` or
   * `sheet` mounts that add-on again, with the sheet at rest. It compares option
   * objects member by member and functions by identity, so a fresh object with the
   * same members changes nothing. Turning an add-on on without its import
   * throws.
   */
  update(options: FluidboxUpdateOptions<TItem>): void;
  /**
   * Replace the items. An open viewer stays on the same id, else on the item now at
   * its index, or the last. It closes when the list is empty.
   */
  setItems(items: FluidboxItems<TItem>): Promise<boolean>;
  /**
   * Start loading an item a press is likely to open, while closed. Until the
   * viewer's code has loaded, this only loads the code. Call the result to drop
   * the work. Throws a RangeError for an unknown target.
   */
  preload(target: FluidboxTarget<TItem>): () => void;
  /**
   * Load the viewer's code now, as on any signal of your own that a viewer is
   * about to open. Resolves to true once it has loaded, or false if it failed or
   * the fluidbox was destroyed. A failed load tries again on the next call.
   */
  load(): Promise<boolean>;
  /**
   * Make an element a trigger that opens the viewer on an item. The item, and any
   * in `returns`, close back onto it. It sets `data-fluidbox-trigger`,
   * `aria-haspopup="dialog"` and `aria-expanded` on the element, loads the viewer's
   * code as the `loading` option says, preloads on a primary-button `pointerdown`,
   * and drops that work 250 ms after the pointer lifts without a click. A click
   * opens the item the trigger shows, its target or the item of `returns` it last
   * closed onto, unless the click was prevented.
   * Returns a function that unbinds.
   */
  bind(
    element: HTMLElement,
    target: FluidboxTarget<TItem>,
    options?: FluidboxBindOptions<TItem>,
  ): () => void;
  /**
   * Make an element on a trigger, such as a badge or a caption, a thumbnail
   * overlay: it gets `--fluidbox-closed` and `data-fluidbox-phase` as the
   * viewer opens and closes, and with `travel` it flies with the photo.
   */
  bindOverlay(
    element: HTMLElement,
    target: FluidboxTarget<TItem>,
    options?: FluidboxOverlayOptions<TItem>,
  ): () => void;
  /**
   * Call `listener` after every state change, not on every animation frame.
   * Returns a function that unsubscribes.
   */
  subscribe(listener: (state: FluidboxState<TItem>) => void): () => void;
  /**
   * Close at once, remove the viewer and release every binding. Calling it again
   * does nothing. After it, commands resolve false, and `setItems`, `preload`,
   * `bind`, `bindOverlay` and `subscribe` throw an InvalidStateError.
   */
  destroy(): void;

  addEventListener<K extends keyof FluidboxEventMap<TItem>>(
    type: K,
    listener: (event: FluidboxEventMap<TItem>[K]) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  addEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject | null,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener<K extends keyof FluidboxEventMap<TItem>>(
    type: K,
    listener: (event: FluidboxEventMap<TItem>[K]) => void,
    options?: boolean | EventListenerOptions,
  ): void;
  removeEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject | null,
    options?: boolean | EventListenerOptions,
  ): void;
}

/** An item, its index, or its id. Items are matched by id. */
type FluidboxTarget<TItem extends FluidboxItem = FluidboxItem> = number | string | TItem;

interface FluidboxCommandOptions {
  /** Aborting it stops the command, which resolves false. */
  readonly signal?: AbortSignal;
  /**
   * A move that follows a continuous input, as a thumbnail strip being
   * scrubbed: the slide jumps rather than glides, and the page is not
   * scrolled to the item's trigger. The next move without it ends the scrub
   * and brings the trigger into view.
   */
  readonly scrub?: boolean;
}

/**
 * The options `update` changes on a live fluidbox. A key left out keeps its
 * value; a key set to undefined goes back to its default.
 */
type FluidboxUpdateOptions<TItem extends FluidboxItem = FluidboxItem> = {
  readonly [K in
    | "labels"
    | "zoom"
    | "loop"
    | "controls"
    | "viewer"
    | "filmstrip"
    | "sheet"
    | "loading"]?: FluidboxOptions<TItem>[K] | undefined;
};

interface FluidboxBindOptions<TItem extends FluidboxItem = FluidboxItem> {
  /** Unbinds when aborted. */
  readonly signal?: AbortSignal;
  /** More items that close back onto this element, as on an album cover. */
  readonly returns?: readonly FluidboxTarget<TItem>[];
  /**
   * Called before the closing flight measures the trigger, with the item it
   * lands on: show that item on the trigger, synchronously, as an album cover does.
   */
  readonly onPreviewChange?: (item: TItem, index: number) => void;
}

interface FluidboxOverlayOptions<TItem extends FluidboxItem = FluidboxItem> {
  /** Unbinds when aborted. */
  readonly signal?: AbortSignal;
  /** More items whose flights carry the overlay, as with the trigger's `returns`. */
  readonly returns?: readonly FluidboxTarget<TItem>[];
  /**
   * Fly with the photo. `true` flies a copy of the element; a renderer draws
   * live content into the flying frame.
   */
  readonly travel?: boolean | FluidboxRenderer<TItem>;
  /** With `travel`, an element moved into the flying frame for the flight and put back after. */
  readonly frameLayer?: HTMLElement;
}
```

```ts
fluidbox.update({
  labels: finnish,
  zoom: matchMedia("(pointer: coarse)").matches ? false : undefined,
});
```

## FluidboxState

```ts
/** A frozen snapshot. */
interface FluidboxState<TItem extends FluidboxItem = FluidboxItem> {
  readonly status: FluidboxStatus;
  /** The item the viewer shows or is going to, or null while closed. */
  readonly id: string | null;
  readonly index: number | null;
  readonly item: TItem | null;
  readonly count: number;
  readonly hasPrevious: boolean;
  readonly hasNext: boolean;
}

type FluidboxStatus = "closed" | "opening" | "open" | "closing";
```

## Events

```ts
/**
 * The events a fluidbox and `<fluidbox-gallery>` fire. The before-events fire
 * synchronously for every command that would do something, from a key, a gesture
 * or your code. `preventDefault()` cancels the command, and its promise resolves
 * false. An open fires `beforetoggle`, `toggle`, `itemchange`, and `settle` after
 * the flight.
 */
interface FluidboxEventMap<TItem extends FluidboxItem = FluidboxItem> {
  /** Before the viewer opens or closes. Cancelable. */
  beforetoggle: FluidboxToggleEvent;
  /** The opening or closing motion starts. */
  toggle: FluidboxToggleEvent;
  /** Before the open viewer moves to another item. Cancelable. */
  beforeitemchange: FluidboxItemChangeEvent<TItem>;
  /** The viewer moves to an item, the one it opens on included. */
  itemchange: FluidboxItemChangeEvent<TItem>;
  /** The viewer comes to rest on an item. */
  settle: FluidboxItemChangeEvent<TItem>;
  /** The sheet comes to rest at another stop. */
  sheetchange: FluidboxSheetEvent;
}

/**
 * Fired as the viewer opens or closes, like a popover's events: `beforetoggle`
 * first, cancelable, then `toggle` as the motion starts.
 */
class FluidboxToggleEvent extends Event {
  readonly oldState: FluidboxToggleState;
  readonly newState: FluidboxToggleState;
  readonly reason: FluidboxReason;
  constructor(
    type: "beforetoggle" | "toggle",
    init: EventInit & {
      oldState: FluidboxToggleState;
      newState: FluidboxToggleState;
      reason: FluidboxReason;
    },
  );
}

/**
 * Fired as the viewer moves between items: `beforeitemchange` first, cancelable,
 * then `itemchange` when the item changes (opening included), and `settle` once it
 * comes to rest.
 */
class FluidboxItemChangeEvent<TItem extends FluidboxItem = FluidboxItem> extends Event {
  readonly id: string;
  readonly index: number;
  readonly item: TItem;
  readonly reason: FluidboxReason;
  constructor(
    type: "beforeitemchange" | "itemchange" | "settle",
    init: EventInit & { id: string; index: number; item: TItem; reason: FluidboxReason },
  );
}

/**
 * Fired when the sheet comes to rest at another stop. A swipe or a scroll of the
 * sheet has the reason "pointer", a wheel "wheel", Escape or a key on a stop
 * button "keyboard", and `sheet.set` or the viewer opening and closing "api".
 */
class FluidboxSheetEvent extends Event {
  /** The stop the sheet rests at: 0 at rest, then one per snap point. */
  readonly stop: number;
  readonly reason: FluidboxReason;
  constructor(type: "sheetchange", init: EventInit & { stop: number; reason: FluidboxReason });
}

/**
 * What made something happen. "api" is your code or a default control, "trigger"
 * a click on a trigger, and "keyboard" the arrow keys, Escape, or a page-scrolling
 * key that closes the viewer. "pointer" is a swipe or scroll through the slides, a
 * drag to dismiss or a tap on the backdrop, and "wheel" a wheel or trackpad scroll
 * that dismisses. "items" is a move made by `setItems`. A move from the filmstrip
 * has the reason of its input.
 */
type FluidboxReason = "api" | "trigger" | "keyboard" | "pointer" | "wheel" | "items";

type FluidboxToggleState = "open" | "closed";
```

## Items

```ts
/**
 * One piece of media. Every field is the HTML attribute of the element the viewer
 * draws, by its DOM property name, so `srcset`, `sizes`, `poster` and
 * `referrerPolicy` mean what they mean on `<img>`, `<video>` and `<iframe>`.
 * Extend a type to add fields of your own. They come back in every callback,
 * event and state.
 */
type FluidboxItem =
  | FluidboxImageItem
  | FluidboxVideoItem
  | FluidboxIframeItem
  | FluidboxCustomItem;

/** What every item can carry besides its media. */
interface FluidboxItemBase {
  /** Stable identity across item updates. Defaults to `src`. Unique in a list. */
  readonly id?: string;
  /**
   * A small image of the item: the filmstrip's thumbnail, an iframe's still, and
   * the picture shown while the full image loads. With `srcset`, the browser
   * picks a sharper one where it can.
   */
  readonly thumbnail?: FluidboxThumbnail;
  /** An image URL, such as a data URI, painted at once behind an image slide. */
  readonly placeholder?: string;
}

/** An `<img>`, or a `<picture>` when it has `sources`. */
interface FluidboxImageItem extends FluidboxItemBase {
  readonly type: "img";
  readonly src: string;
  readonly srcset?: string;
  readonly sizes?: string;
  readonly alt: string;
  readonly width: number;
  readonly height: number;
  readonly sources?: readonly FluidboxPictureSource[];
}

/** A `<video>`. */
interface FluidboxVideoItem extends FluidboxItemBase {
  readonly type: "video";
  readonly src: string;
  /** The video's accessible name. */
  readonly title: string;
  readonly width: number;
  readonly height: number;
  readonly poster?: string;
  /** Plays when its slide comes into view, and pauses when it leaves. */
  readonly autoplay?: boolean;
  readonly loop?: boolean;
  readonly muted?: boolean;
  /** @default true */
  readonly playsInline?: boolean;
  /** @default true */
  readonly controls?: boolean;
  /** @default "metadata" */
  readonly preload?: "none" | "metadata" | "auto";
}

/** An `<iframe>`. It gets `loading="lazy"` and `allowfullscreen`. */
interface FluidboxIframeItem extends FluidboxItemBase {
  readonly type: "iframe";
  readonly src: string;
  readonly title: string;
  readonly width: number;
  readonly height: number;
  readonly allow?: string;
  readonly referrerPolicy?: ReferrerPolicy;
  readonly sandbox?: string;
}

/** Your own content, drawn by the renderer named `render`. */
interface FluidboxCustomItem extends FluidboxItemBase {
  readonly type: "custom";
  readonly id: string;
  /** The key of a renderer in the `renderers` option. */
  readonly render: string;
  /** The content's accessible name. */
  readonly title: string;
  readonly width: number;
  readonly height: number;
}

/** An `<img>`'s source attributes. */
interface FluidboxThumbnail {
  readonly src: string;
  readonly srcset?: string;
  readonly sizes?: string;
  readonly width?: number;
  readonly height?: number;
}

/** A `<source>` of a `<picture>`. */
interface FluidboxPictureSource {
  readonly srcset: string;
  readonly sizes?: string;
  readonly media?: string;
  readonly type?: string;
}

/** The id of an item: its own, else its src. Throws a TypeError when it has neither. */
function idOf(item: FluidboxItem): string;

/** The text an item is named by: its alt text or title. */
function itemLabel(item: FluidboxItem): string;
```

## Lists

```ts
/**
 * An array, which the fluidbox copies and checks for empty and duplicate ids, or
 * a lazy list it reads on demand and doesn't check.
 */
type FluidboxItems<TItem extends FluidboxItem = FluidboxItem> =
  | readonly TItem[]
  | FluidboxItemList<TItem>;

/** A list read on demand, for long or generated galleries. */
interface FluidboxItemList<TItem extends FluidboxItem = FluidboxItem> {
  /** A non-negative integer. */
  readonly count: number;
  /** The item at an index, synchronously. Return the same object each time. */
  get(index: number): TItem;
  /** The index of the item with this id, or -1. Without it, ids are found by scanning. */
  indexOf?(id: string): number;
}

/**
 * A lazy list that makes each item once and gives back the same object after,
 * and checks the index it is given for an id.
 */
function createItemList<TItem extends FluidboxItem>(
  options: FluidboxItemListOptions<TItem>,
): FluidboxItemList<TItem>;

/** What `createItemList` makes a list from. */
interface FluidboxItemListOptions<TItem extends FluidboxItem = FluidboxItem> {
  readonly count: number;
  /** Makes the item at an index. The list calls it once per index and keeps the item. */
  readonly item: (index: number) => TItem;
  /**
   * Where an id is, as a guess the list checks: an index out of range, or one whose
   * item has another id, counts as not found. Without it, the list scans.
   */
  readonly index?: (id: string) => number;
}
```

## Renderers

```ts
/**
 * Draws into `target`: a custom slide (`renderers`), a filmstrip thumbnail, the
 * sheet, or a travelling thumbnail overlay. Return a function to release your own
 * resources, such as timers.
 */
type FluidboxRenderer<TItem extends FluidboxItem = FluidboxItem> = (
  context: FluidboxRenderContext<TItem>,
) => void | (() => void);

interface FluidboxRenderContext<TItem extends FluidboxItem = FluidboxItem> {
  readonly item: TItem;
  readonly index: number;
  /** The element to draw into. The viewer empties it afterwards. */
  readonly target: HTMLElement;
  /** Aborted when the content is no longer shown. */
  readonly signal: AbortSignal;
}

/**
 * Draws your controls and captions into the viewer. It runs when the fluidbox is
 * made, and again after an `update` that redraws the viewer's layer. The function
 * it returns, and its `signal`, end each run.
 */
type FluidboxViewerRenderer<TItem extends FluidboxItem = FluidboxItem> = (
  context: FluidboxViewerContext<TItem>,
) => void | (() => void);

interface FluidboxViewerContext<TItem extends FluidboxItem = FluidboxItem> {
  /** The viewer's layer for your controls and captions. */
  readonly target: HTMLElement;
  readonly fluidbox: Fluidbox<TItem>;
  /** Aborted when the fluidbox is destroyed. */
  readonly signal: AbortSignal;
}
```

## Origins

```ts
interface FluidboxOrigin<TItem extends FluidboxItem = FluidboxItem> {
  /** The element that opened the item, scrolled into view on close. */
  readonly trigger: HTMLElement;
  /** The box the flight starts from and lands on. */
  readonly visual: HTMLElement;
  /** The image painted at the start of the flight, if any. */
  readonly thumbnail?: HTMLImageElement;
  /**
   * Called before the closing flight measures `visual`, with the item it lands
   * on: show that item on the trigger, synchronously, as an album cover does.
   */
  readonly onPreviewChange?: (item: TItem, index: number) => void;
}

/** Finds origins the viewer can't see, as in a virtualized grid. */
interface FluidboxOriginAdapter<TItem extends FluidboxItem = FluidboxItem> {
  getOrigin(id: string): FluidboxOrigin<TItem> | null;
  /** Bring the item's trigger into view. Without it, the viewer scrolls it into view. */
  revealOrigin?(id: string, index: number, context: FluidboxRevealContext): void | Promise<void>;
}

interface FluidboxRevealContext {
  readonly signal: AbortSignal;
  /** "open" before the opening flight, "selection" as the viewer moves between items. */
  readonly phase: "open" | "selection";
}
```

## Labels

```ts
/** Every string the viewer shows or announces. Pass some or all to `labels` to translate them. */
interface FluidboxLabels {
  /** The dialog's name when the item has no alt text or title. */
  readonly viewer: string;
  readonly close: string;
  readonly previous: string;
  readonly next: string;
  /** A slide's name, and what the viewer announces as it moves. */
  readonly position: (index: number, count: number) => string;
  /** The strip of slides. */
  readonly slides: string;
  readonly filmstrip: string;
  /** The filmstrip's list of thumbnails. */
  readonly filmstripList: string;
  /** The scroller of a zoomed image. */
  readonly zoomed: string;
  /** The sheet the runtime makes when the sheet has a `render`. */
  readonly sheet: string;
  /** Shown on a slide whose media failed to load. */
  readonly error: string;
  /** The button that loads failed media again. */
  readonly retry: string;
}

/** The labels the viewer uses for any you don't pass. */
const defaultLabels: FluidboxLabels = Object.freeze({
  viewer: "Media viewer",
  close: "Close",
  previous: "Previous",
  next: "Next",
  position: (index: number, count: number) => `Item ${index + 1} of ${count}`,
  slides: "Gallery items",
  filmstrip: "Gallery filmstrip",
  filmstripList: "Choose a gallery item",
  zoomed: "Zoomed image",
  sheet: "Details",
  error: "Unable to load media.",
  retry: "Retry",
});
```

```ts
createFluidbox({
  items: photos,
  labels: {
    close: "Sulje",
    previous: "Edellinen",
    next: "Seuraava",
    position: (index, count) => `Kuva ${index + 1}/${count}`,
  },
});
```

[Languages and direction](https://fluidbox.dwe.workers.dev/docs/languages.md) covers translating with an i18n library and running the viewer right to left.

## Add-ons

```ts
interface FluidboxFilmstripOptions<TItem extends FluidboxItem = FluidboxItem> {
  /**
   * Fills each thumbnail button instead of the default image, while the button
   * is in the strip's rendered window. The `signal` aborts as it leaves.
   */
  readonly render?: FluidboxRenderer<TItem>;
}

interface FluidboxSheetOptions<TItem extends FluidboxItem = FluidboxItem> {
  /**
   * The stops above the resting one, lowest first. Stop 0 is the sheet at rest:
   * away, or showing `--fluidbox-sheet-peek`.
   * @default ["60%", "content"]
   */
  readonly snapPoints?: readonly FluidboxSheetSnapPoint[];
  /**
   * What a swipe down on the photo does with the sheet down. "flight" flies the
   * photo home; "scroll" scrolls it off the bottom of the screen and closes;
   * "none" keeps the viewer open.
   * @default "flight"
   */
  readonly dismiss?: "flight" | "scroll" | "none";
  /**
   * Fills the sheet, a `<section data-fluidbox-sheet>` named by `labels.sheet`,
   * for the item on show. It runs again when the item changes; the `signal`
   * aborts and the section is emptied first.
   */
  readonly render?: FluidboxRenderer<TItem>;
}

/**
 * How much of the sheet shows at a stop: pixels, a percentage of the viewer's
 * height, or "content" for all of it, up to the space under the top inset.
 */
type FluidboxSheetSnapPoint = number | `${number}%` | "content";

/** Reads and moves the sheet. A `sheetchange` event follows each new stop. */
interface FluidboxSheet {
  /** The stop the sheet rests at: 0 at rest, then one per snap point. */
  readonly stop: number;
  /** Move the sheet to a stop. Ignored while the viewer is not open. */
  set(stop: number, options?: { readonly animate?: boolean }): void;
}
```

## Custom elements

```ts
/**
 * The fluidbox as markup: `<fluidbox-item>` children, or an `items` property
 * with descendants marked `data-fluidbox-item-id` as triggers. It takes the
 * same options as createFluidbox as properties, has the same commands, and
 * fires the same events.
 *
 * Children that turn features on: `<fluidbox-filmstrip>`, `<fluidbox-sheet>`
 * (each needs its entry imported), and a `<template slot="viewer">` for your
 * own controls, where `<fluidbox-controls>` stands for the default ones.
 */
class FluidboxGalleryElement extends HTMLElement {
  addEventListener<K extends keyof FluidboxGalleryEventMap>(
    type: K,
    listener: (this: FluidboxGalleryElement, event: FluidboxGalleryEventMap[K]) => unknown,
    options?: boolean | AddEventListenerOptions,
  ): void;
  addEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener<K extends keyof FluidboxGalleryEventMap>(
    type: K,
    listener: (this: FluidboxGalleryElement, event: FluidboxGalleryEventMap[K]) => unknown,
    options?: boolean | EventListenerOptions,
  ): void;
  removeEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | EventListenerOptions,
  ): void;
  /** The fluidbox behind the gallery, once it is ready. A rebuild replaces it. */
  get fluidbox(): Fluidbox | null;
  /** The viewer state, or a closed state before the gallery is ready. */
  get state(): FluidboxState;
  /** The sheet, while a `<fluidbox-sheet>` turns it on. */
  get sheet(): FluidboxSheet | null;
  /** Resolves with the fluidbox once the gallery has read its items. */
  whenReady(): Promise<Fluidbox>;
  /** Items to use instead of the `<fluidbox-item>` children. */
  get items(): FluidboxItems | null;
  set items(items: FluidboxItems | null);
  /** Replace the items, as the `items` property does. Resolves once they apply. */
  async setItems(items: FluidboxItems): Promise<boolean>;
  /** Origins for items that have no trigger in the gallery. */
  get origins(): FluidboxOriginAdapter | undefined;
  set origins(origins: FluidboxOriginAdapter | undefined);
  /** Renderers of `custom` items, by name. Read when a slide mounts. */
  get renderers(): Readonly<Record<string, FluidboxRenderer<any>>>;
  set renderers(renderers: Readonly<Record<string, FluidboxRenderer<any>>>);
  /** Your own controls and captions, instead of a `<template slot="viewer">` or the defaults. */
  get viewer(): FluidboxViewerRenderer<any> | undefined;
  set viewer(viewer: FluidboxViewerRenderer<any> | undefined);
  get labels(): Partial<FluidboxLabels> | undefined;
  set labels(labels: Partial<FluidboxLabels> | undefined);
  /** Zoom options, or false to turn zoom off. Overrides `nozoom` and the zoom attributes. */
  get zoom(): FluidboxOptions["zoom"] | undefined;
  set zoom(zoom: FluidboxOptions["zoom"] | undefined);
  /** When the viewer's code loads, as the `loading` attribute says: `hover` (the default), `press` or `eager`. */
  get loading(): FluidboxLoading;
  set loading(loading: FluidboxLoading);
  /** Follow the viewer state, across the rebuilds a change of options makes. */
  subscribe(listener: (state: FluidboxState) => void): () => void;
  open(target?: FluidboxTarget, options?: FluidboxCommandOptions): Promise<boolean>;
  /** Start loading an item a press is likely to open. Call the result to let go. */
  preload(target: FluidboxTarget): () => void;
  /** Load the viewer's code now. Resolves to true once it has loaded, false if it failed. */
  load(): Promise<boolean>;
  close(options?: FluidboxCommandOptions): Promise<boolean>;
  next(options?: FluidboxCommandOptions): Promise<boolean>;
  previous(options?: FluidboxCommandOptions): Promise<boolean>;
}

/** The gallery's events: the fluidbox's, and an element's own. */
type FluidboxGalleryEventMap = Omit<HTMLElementEventMap, keyof FluidboxEventMap> &
  FluidboxEventMap;

/**
 * Turns on the filmstrip of its parent `<fluidbox-gallery>`. Set `render` to
 * fill each thumbnail button yourself.
 */
class FluidboxFilmstripElement extends HTMLElement {
  get render(): FluidboxRenderer<any> | undefined;
  set render(render: FluidboxRenderer<any> | undefined);
}

/**
 * Turns on the sheet of its parent `<fluidbox-gallery>`, with `snap-points`
 * ("40% 90%", pixels or "content") and `dismiss` ("flight", "scroll" or
 * "none"). It shows each item's `<template slot="sheet">`, else its own
 * `<template>`, else what `render` draws.
 */
class FluidboxSheetElement extends HTMLElement {
  get render(): FluidboxRenderer<any> | undefined;
  set render(render: FluidboxRenderer<any> | undefined);
}

/**
 * An item of its `<fluidbox-gallery>`: a `slot="trigger"` element that opens
 * it, and a `<template slot="content">` holding the `<img>`, `<picture>`,
 * `<video>`, `<iframe>` or other markup the viewer shows.
 *
 * `returns="b c"` lists more items whose close flight returns to this trigger,
 * as a "+3" teaser does for an album; the item's own id may be among them. The
 * trigger opens the item it last showed. Before such a flight, the item
 * dispatches `fluidbox-preview-change` with the id and item it lands on;
 * update the trigger's preview synchronously there. The gallery doesn't read
 * changes inside a trigger as changes to its items.
 */
class FluidboxItemElement extends HTMLElement {
  get itemId(): string;
  /** The ids in `returns`. */
  get returns(): readonly string[];
  /**
   * The `slot="trigger"` child. An item without one is in the gallery but has
   * no thumbnail of its own, as the rest of an album behind a teaser.
   */
  get trigger(): HTMLElement | null;
}

/** What `<fluidbox-item>` dispatches before a close flight lands on a different item of its `returns`. */
type FluidboxPreviewChangeEvent = CustomEvent<{
  readonly id: string;
  readonly item: FluidboxItem;
}>;
```

## React

```ts
interface FluidboxActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {}

/** The viewer state and its commands. */
type FluidboxApi<TItem extends FluidboxItem = FluidboxItem> = FluidboxState<TItem> &
  FluidboxCommands<TItem>;

/** The commands `useFluidbox` hands back with the state. */
interface FluidboxCommands<TItem extends FluidboxItem = FluidboxItem> {
  readonly open: (
    target?: FluidboxTarget<TItem>,
    options?: FluidboxCommandOptions,
  ) => Promise<boolean>;
  readonly close: (options?: FluidboxCommandOptions) => Promise<boolean>;
  readonly next: (options?: FluidboxCommandOptions) => Promise<boolean>;
  readonly previous: (options?: FluidboxCommandOptions) => Promise<boolean>;
  readonly setItems: (items: FluidboxItems<TItem>) => Promise<boolean>;
  /** Start loading an item a press is likely to open. Call the result to let go. */
  readonly preload: (target: FluidboxTarget<TItem>) => () => void;
  /** Load the viewer's code now. Resolves to true once it has loaded, false if it failed. */
  readonly load: () => Promise<boolean>;
}

interface FluidboxFilmstripProps<TItem extends FluidboxItem = FluidboxItem> {
  /**
   * Renders one thumbnail into its button. Leave it out for the default
   * thumbnails. The selected button has `data-fluidbox-selected`.
   */
  readonly children?: (context: FluidboxItemContext<TItem>) => ReactNode;
}

interface FluidboxFrameLayerProps extends HTMLAttributes<HTMLSpanElement> {}

/** What the Sheet's and the Filmstrip's render functions get: the item they draw. */
interface FluidboxItemContext<TItem extends FluidboxItem = FluidboxItem> {
  readonly item: TItem;
  readonly index: number;
}

/** What a React renderer gets: the item it draws, and a signal aborted when it leaves. */
interface FluidboxReactRenderContext<TItem extends FluidboxItem = FluidboxItem> {
  readonly item: TItem;
  readonly index: number;
  readonly signal: AbortSignal;
}

/** Draws a `custom` item as React content, rendered in place with the app's context. */
type FluidboxReactRenderer<TItem extends FluidboxItem = FluidboxItem> = (
  context: FluidboxReactRenderContext<TItem>,
) => ReactNode;

interface FluidboxRootProps<TItem extends FluidboxItem = FluidboxItem> {
  /** An array, or a list read on demand. A new value replaces the items. */
  readonly items: FluidboxItems<TItem>;
  /**
   * Whether the viewer is open, when your state owns it. The viewer opens on
   * `index` and closes when this changes. Leave it out to let triggers own it.
   */
  readonly open?: boolean;
  /** The item the viewer shows, when your state owns it. */
  readonly index?: number;
  /** As the viewer opens or closes, from any input. Set `open` to follow it. */
  readonly onOpenChange?: (open: boolean, details: FluidboxChangeDetails) => void;
  /** As the viewer moves to another item, opening included. Set `index` to follow it. */
  readonly onIndexChange?: (index: number, details: FluidboxChangeDetails) => void;
  /** Go round: past the last item comes the first. A single item never loops. */
  readonly loop?: boolean;
  /**
   * Where the viewer's controls lead before JavaScript runs, when the server
   * renders it open: `null` for Close, an index for Previous and Next.
   */
  readonly href?: (index: number | null) => string | undefined;
  /** Your strings for the viewer. They change the open viewer in place. */
  readonly labels?: Partial<FluidboxLabels>;
  /** Zoom steps, or `false` to turn zoom off. Compared by value. */
  readonly zoom?: FluidboxOptions<TItem>["zoom"];
  /** Draw `custom` items, by the name in their `render`. */
  readonly renderers?: Readonly<Record<string, FluidboxReactRenderer<TItem>>>;
  /** Find origins the viewer can't see, as in a virtualized grid. */
  readonly origins?: FluidboxOriginAdapter<TItem>;
  /** When the viewer's code loads: on `"hover"` (the default), on `"press"`, or `"eager"`ly. */
  readonly loading?: FluidboxOptions<TItem>["loading"];
  /** Before the viewer opens or closes. `event.preventDefault()` stops it. */
  readonly onBeforeToggle?: (event: FluidboxToggleEvent) => void;
  /** As the viewer starts to open or close. */
  readonly onToggle?: (event: FluidboxToggleEvent) => void;
  /** Before the viewer moves to another item. `event.preventDefault()` stops it. */
  readonly onBeforeItemChange?: (event: FluidboxItemChangeEvent<TItem>) => void;
  /** When the active item changes, opening included. */
  readonly onItemChange?: (event: FluidboxItemChangeEvent<TItem>) => void;
  /** When the active item comes to rest. */
  readonly onSettle?: (event: FluidboxItemChangeEvent<TItem>) => void;
  /** When the sheet comes to rest at another stop. */
  readonly onSheetChange?: (event: FluidboxSheetEvent) => void;
  readonly children?: ReactNode;
}

/** The sheet's stop and a function that moves it. */
interface FluidboxSheetApi {
  /** 0 at rest, then one per snap point. 0 while the sheet is not there. */
  readonly stop: number;
  readonly set: (stop: number, options?: { readonly animate?: boolean }) => void;
}

interface FluidboxSheetProps<TItem extends FluidboxItem = FluidboxItem>
  extends
    Omit<HTMLAttributes<HTMLElement>, keyof DOMAttributes<HTMLElement>>,
    Pick<FluidboxSheetOptions<TItem>, "snapPoints" | "dismiss"> {
  /** The sheet's content for the item on show. */
  readonly children?: (context: FluidboxItemContext<TItem>) => ReactNode;
}

interface FluidboxSheetStopProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** The stop the button moves the sheet to. */
  readonly stop: number;
}

interface FluidboxThumbnailOverlayProps extends HTMLAttributes<HTMLSpanElement> {
  /** Fly with the photo, rendered live in the flying frame. */
  readonly travel?: boolean;
}

interface FluidboxTriggerProps<TItem extends FluidboxItem = FluidboxItem> extends Omit<
  ButtonHTMLAttributes<HTMLButtonElement>,
  "children"
> {
  /** Decorate the single child element instead of rendering a `<button>`. */
  readonly asChild?: boolean;
  /** The item to open: the item, its id or its index. */
  readonly item: FluidboxTarget<TItem>;
  /** More items that close back onto this trigger, as for an album cover. */
  readonly returns?: readonly FluidboxTarget<TItem>[];
  readonly children: ReactNode | ((preview: FluidboxTriggerPreview<TItem>) => ReactNode);
}

interface FluidboxViewerProps {
  /** Your controls and captions, rendered into the viewer. */
  readonly children?: ReactNode;
}

interface UseFluidbox {
  <TItem extends FluidboxItem = FluidboxItem>(): FluidboxApi<TItem>;
  <TSelected, TItem extends FluidboxItem = FluidboxItem>(
    selector: (value: FluidboxApi<TItem>) => TSelected,
  ): TSelected;
}

/** What made a controlled `open` or `index` change. */
interface FluidboxChangeDetails {
  readonly reason: FluidboxReason;
}

/** A trigger's preview: the item it shows, which follows the viewer while it is open. */
interface FluidboxTriggerPreview<TItem extends FluidboxItem = FluidboxItem> {
  /** Null when the trigger's items have left the list. */
  readonly item: TItem | null;
  readonly index: number | null;
  readonly status: FluidboxStatus;
}

/** The viewer state and its commands, or the part a selector picks, re-rendering as it changes. */
function useFluidbox<TItem extends FluidboxItem = FluidboxItem>(): FluidboxApi<TItem>;
function useFluidbox<TSelected, TItem extends FluidboxItem = FluidboxItem>(
  selector: (value: FluidboxApi<TItem>) => TSelected,
): TSelected;

/** The sheet's stop, re-rendering as `sheetchange` moves it, and a function that moves it. */
function useFluidboxSheet(): FluidboxSheetApi;

const Fluidbox = {
  Root,
  Trigger,
  Viewer,
  Controls,
  Close,
  Previous,
  Next,
  Filmstrip,
  Sheet,
  SheetStop,
  ThumbnailOverlay,
  FrameLayer,
};

/** The type of `Fluidbox`. */
type FluidboxReactParts = typeof Fluidbox;
```

## Vue

```ts
/**
 * What `useFluidbox()` returns and Root's default slot gets: the state and the
 * commands, in one reactive object. Read its fields where you use them; a
 * destructured field in `<script setup>` stops following the viewer.
 */
type FluidboxApi<TItem extends FluidboxItem = FluidboxItem> = FluidboxState<TItem> &
  FluidboxCommands<TItem>;

/** The viewer's commands. Each resolves true once it happened, false when it was prevented. */
interface FluidboxCommands<TItem extends FluidboxItem = FluidboxItem> {
  /** Open on an item, its index or its id, or move to it while open. */
  open(target?: FluidboxTarget<TItem>, options?: FluidboxCommandOptions): Promise<boolean>;
  close(options?: FluidboxCommandOptions): Promise<boolean>;
  next(options?: FluidboxCommandOptions): Promise<boolean>;
  previous(options?: FluidboxCommandOptions): Promise<boolean>;
  setItems(items: FluidboxItems<TItem>): Promise<boolean>;
  /** Start loading an item a press is likely to open. Call the result to let go. */
  preload(target: FluidboxTarget<TItem>): () => void;
  /** Load the viewer's code now. Resolves to true once it has loaded, false if it failed. */
  load(): Promise<boolean>;
}

/** What the Sheet and Filmstrip slots get: the item they draw and its index. */
interface FluidboxItemContext<TItem extends FluidboxItem = FluidboxItem> {
  readonly item: TItem;
  readonly index: number;
}

/** Reads and moves the sheet, from `useFluidboxSheet()`. `stop` follows `sheetchange`. */
interface FluidboxSheetApi {
  /** The stop the sheet rests at: 0 at rest, then one per snap point. */
  readonly stop: number;
  /** Move the sheet to a stop. Ignored while the viewer is not open. */
  set(stop: number, options?: { readonly animate?: boolean }): void;
}

/** What a renderer gets: the item, its index and an abort signal. */
interface FluidboxVueRenderContext<TItem extends FluidboxItem = FluidboxItem> {
  readonly item: TItem;
  readonly index: number;
  /** Aborted when the viewer lets go of the slide. */
  readonly signal: AbortSignal;
}

/** Draws a `custom` item, keyed by its `render`. Return any Vue content, such as `h(Note)`. */
type FluidboxVueRenderer<TItem extends FluidboxItem = FluidboxItem> = (
  context: FluidboxVueRenderContext<TItem>,
) => VNodeChild;

/** What made a controlled `open` or `index` change. */
interface FluidboxChangeDetails {
  readonly reason: FluidboxReason;
}

/** A trigger's preview: the item it shows, which follows the viewer while it is open. */
interface FluidboxTriggerPreview<TItem extends FluidboxItem = FluidboxItem> {
  /** Null when the trigger's items have left the list. */
  readonly item: TItem | null;
  readonly index: number | null;
  readonly status: FluidboxStatus;
}

interface FluidboxRootProps<TItem extends FluidboxItem = FluidboxItem> {
  /** The items, or a list read on demand. Reactive, also in place. */
  readonly items: FluidboxItems<TItem>;
  /**
   * Whether the viewer is open, for `v-model:open`. The viewer opens on
   * `index` and closes when this changes. Leave it out to let triggers own it.
   */
  readonly open?: boolean;
  /** The item the viewer shows, for `v-model:index`. */
  readonly index?: number;
  /** Your strings for the viewer. */
  readonly labels?: Partial<FluidboxLabels>;
  /** Pinch, double-tap, wheel and keyboard zoom of images. `false` turns it off. */
  readonly zoom?: false | { readonly step?: number; readonly max?: number };
  /** Go round: past the last item comes the first. A single item never loops. */
  readonly loop?: boolean;
  /** Draw `custom` items, by the name in their `render`. */
  readonly renderers?: Readonly<Record<string, FluidboxVueRenderer<TItem>>>;
  /** Find origins the viewer can't see, as in a virtualized grid. */
  readonly origins?: FluidboxOriginAdapter<TItem>;
  /** When the viewer's code loads: on `"hover"` (the default), on `"press"`, or `"eager"`ly. */
  readonly loading?: FluidboxLoading;
}

interface FluidboxTriggerProps<TItem extends FluidboxItem = FluidboxItem> {
  /** Decorate the one child instead of rendering a `<button>`. */
  readonly asChild?: boolean;
  /** Required. The item this trigger opens, or its index or id. */
  readonly item: FluidboxTarget<TItem>;
  /** More items that close back onto this trigger, as an album cover's. */
  readonly returns?: readonly FluidboxTarget<TItem>[];
}

interface FluidboxSheetProps {
  /**
   * The stops above the resting one, lowest first.
   * @default ["60%", "content"]
   */
  readonly snapPoints?: readonly FluidboxSheetSnapPoint[];
  /**
   * What a swipe down on the photo does with the sheet down.
   * @default "flight"
   */
  readonly dismiss?: "flight" | "scroll" | "none";
}

/**
 * The viewer's state and commands, in one reactive object, or a part of it
 * picked by `selector` as a computed ref. Call it in a component inside Root.
 */
function useFluidbox<TItem extends FluidboxItem = FluidboxItem>(): FluidboxApi<TItem>;
function useFluidbox<TItem extends FluidboxItem = FluidboxItem, TSelected = unknown>(
  selector: (fluidbox: FluidboxApi<TItem>) => TSelected,
): ComputedRef<TSelected>;

/** The sheet's stop and a setter, in one reactive object. Call it in a component inside Root. */
function useFluidboxSheet(): FluidboxSheetApi;

/** Every part, as `<Fluidbox.Root>` and so on. */
const Fluidbox = {
  Root,
  Trigger,
  Viewer,
  Controls,
  Close,
  Previous,
  Next,
  Filmstrip,
  Sheet,
  SheetStop,
  ThumbnailOverlay,
  FrameLayer,
};
```

## Solid

```ts
interface FluidboxActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {}

/**
 * The viewer's state and commands. The state fields are reactive getters:
 * read them in JSX or an effect, and don't destructure them.
 */
interface FluidboxApi<
  TItem extends FluidboxItem = FluidboxItem,
> extends FluidboxState<TItem> {
  open(target?: FluidboxTarget<TItem>, options?: FluidboxCommandOptions): Promise<boolean>;
  close(options?: FluidboxCommandOptions): Promise<boolean>;
  next(options?: FluidboxCommandOptions): Promise<boolean>;
  previous(options?: FluidboxCommandOptions): Promise<boolean>;
  setItems(items: FluidboxItems<TItem>): Promise<boolean>;
  /** Start loading an item a press is likely to open. Call the result to let go. */
  preload(target: FluidboxTarget<TItem>): () => void;
  /** Load the viewer's code now. Resolves to true once it has loaded, false if it failed. */
  load(): Promise<boolean>;
}

interface FluidboxFilmstripProps<TItem extends FluidboxItem = FluidboxItem> {
  /** Renders one thumbnail into its button. Without it, the strip shows the default images. */
  readonly children?: (thumb: FluidboxItemContext<TItem>) => JSX.Element;
}

interface FluidboxFrameLayerProps extends JSX.HTMLAttributes<HTMLSpanElement> {}

interface FluidboxRootProps<TItem extends FluidboxItem = FluidboxItem> {
  /** The gallery's items: an array or a lazy list. Tracked: pass a new one to change the gallery. */
  readonly items: FluidboxItems<TItem>;
  /**
   * Whether the viewer is open, when your state owns it. The viewer opens on
   * `index` and closes when this changes. Leave it out to let triggers own it.
   */
  readonly open?: boolean;
  /** The item the viewer shows, when your state owns it. */
  readonly index?: number;
  /** As the viewer opens or closes, from any input. Set `open` to follow it. */
  readonly onOpenChange?: (open: boolean, details: FluidboxChangeDetails) => void;
  /** As the viewer moves to another item, opening included. Set `index` to follow it. */
  readonly onIndexChange?: (index: number, details: FluidboxChangeDetails) => void;
  /** Go round: past the last item comes the first. A single item never loops. */
  readonly loop?: boolean;
  /** Your strings for the viewer. Changes apply to an open viewer. */
  readonly labels?: Partial<FluidboxLabels>;
  /** Pinch, double-tap, wheel and keyboard zoom of images. `false` turns it off. Changes apply to an open viewer. */
  readonly zoom?: false | { readonly step?: number; readonly max?: number };
  /** Render `custom` items, by the name in their `render`. */
  readonly renderers?: Readonly<Record<string, FluidboxSolidRenderer<TItem>>>;
  /** Find origins the viewer can't see, as in a virtualized grid. */
  readonly origins?: FluidboxOriginAdapter<TItem>;
  /** When the viewer's code loads: on `"hover"` (the default), on `"press"`, or `"eager"`ly. */
  readonly loading?: FluidboxLoading;
  /** Before the viewer opens or closes. `event.preventDefault()` cancels a command. */
  readonly onBeforeToggle?: (event: FluidboxToggleEvent) => void;
  /** As the viewer starts to open or close. */
  readonly onToggle?: (event: FluidboxToggleEvent) => void;
  /** Before the viewer moves to another item. `event.preventDefault()` keeps it still. */
  readonly onBeforeItemChange?: (event: FluidboxItemChangeEvent<TItem>) => void;
  /** When the active item changes, opening included. */
  readonly onItemChange?: (event: FluidboxItemChangeEvent<TItem>) => void;
  /** When the active item comes to rest. */
  readonly onSettle?: (event: FluidboxItemChangeEvent<TItem>) => void;
  /** When the sheet comes to rest at another stop. */
  readonly onSheetChange?: (event: FluidboxSheetEvent) => void;
  readonly children?: JSX.Element;
}

interface FluidboxSheetApi {
  /** The stop the sheet rests at: 0 at rest, then one per snap point. Follows `sheetchange`. */
  readonly stop: Accessor<number>;
  /** Move the sheet to a stop. Ignored while the viewer is not open. */
  set(stop: number, options?: { readonly animate?: boolean }): void;
}

/** Attributes for the sheet's `<section data-fluidbox-sheet>`, and its content. */
interface FluidboxSheetProps<TItem extends FluidboxItem = FluidboxItem> extends Omit<
  JSX.HTMLAttributes<HTMLElement>,
  "children"
> {
  /**
   * The stops above the resting one, lowest first.
   * @default ["60%", "content"]
   */
  readonly snapPoints?: readonly FluidboxSheetSnapPoint[];
  /** What a swipe down on the photo does with the sheet down. @default "flight" */
  readonly dismiss?: "flight" | "scroll" | "none";
  /** Renders the sheet's content for the active item. */
  readonly children?: (active: FluidboxItemContext<TItem>) => JSX.Element;
}

interface FluidboxSheetStopProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
  /** The stop to move the sheet to. Pressed at that stop, it lowers the sheet to 0. */
  readonly stop: number;
}

interface FluidboxSolidParts {
  Root: <TItem extends FluidboxItem>(props: FluidboxRootProps<TItem>) => JSX.Element;
  Trigger: <TItem extends FluidboxItem>(props: FluidboxTriggerProps<TItem>) => JSX.Element;
  Viewer: (props: FluidboxViewerProps) => JSX.Element;
  Controls: () => JSX.Element;
  Close: (props: FluidboxActionProps) => JSX.Element;
  Previous: (props: FluidboxActionProps) => JSX.Element;
  Next: (props: FluidboxActionProps) => JSX.Element;
  Filmstrip: <TItem extends FluidboxItem>(props: FluidboxFilmstripProps<TItem>) => JSX.Element;
  Sheet: <TItem extends FluidboxItem>(props: FluidboxSheetProps<TItem>) => JSX.Element;
  SheetStop: (props: FluidboxSheetStopProps) => JSX.Element;
  ThumbnailOverlay: (props: FluidboxThumbnailOverlayProps) => JSX.Element;
  FrameLayer: (props: FluidboxFrameLayerProps) => JSX.Element;
}

interface FluidboxSolidRenderContext<TItem extends FluidboxItem = FluidboxItem> {
  readonly item: TItem;
  readonly index: number;
  /** Aborted when the content is no longer shown. */
  readonly signal: AbortSignal;
}

/** Renders a custom item. It runs under Root, so context provided around Root reaches it. */
type FluidboxSolidRenderer<TItem extends FluidboxItem = FluidboxItem> = (
  context: FluidboxSolidRenderContext<TItem>,
) => JSX.Element;

interface FluidboxThumbnailOverlayProps extends JSX.HTMLAttributes<HTMLSpanElement> {
  /** Fly with the photo: the children render again, live, in the flying frame. */
  readonly travel?: boolean;
}

/** A trigger's preview: the item it shows, which follows the viewer while it is open. */
interface FluidboxTriggerPreview<TItem extends FluidboxItem = FluidboxItem> {
  /** Null when the trigger's items have left the list. */
  readonly item: TItem | null;
  readonly index: number | null;
  readonly status: FluidboxStatus;
}

type FluidboxTriggerProps<TItem extends FluidboxItem = FluidboxItem> = Omit<
  JSX.ButtonHTMLAttributes<HTMLButtonElement>,
  "children" | "ref"
> & {
  /** The element or component to render instead of a button. It must forward `ref` and events. */
  readonly as?: ValidComponent;
  /** The item to open: the item, its id or its index. */
  readonly item: FluidboxTarget<TItem>;
  /** More items that close back onto this trigger, as for an album cover. */
  readonly returns?: readonly FluidboxTarget<TItem>[];
  readonly ref?: HTMLElement | ((element: HTMLElement) => void);
  /** Content, or a function of the preview, which follows the viewer while it is open. */
  readonly children?:
    | JSX.Element
    | ((preview: Accessor<FluidboxTriggerPreview<TItem>>) => JSX.Element);
  /** Attributes for an `as` element or component. */
  readonly [attribute: string]: unknown;
};

interface FluidboxViewerProps {
  /** Your controls and captions, rendered into the viewer. */
  readonly children?: JSX.Element;
}

/**
 * The viewer's state and commands, or a memo of the part a selector picks.
 * The state fields are reactive getters: read them in JSX or an effect.
 */
function useFluidbox<TItem extends FluidboxItem = FluidboxItem>(): FluidboxApi<TItem>;
function useFluidbox<TSelected, TItem extends FluidboxItem = FluidboxItem>(
  selector: (state: FluidboxState<TItem>) => TSelected,
): Accessor<TSelected>;

/** The sheet's stop as a signal, and a command to move it. Needs a Fluidbox.Sheet. */
function useFluidboxSheet(): FluidboxSheetApi;

const Fluidbox: FluidboxSolidParts = {
  Root,
  Trigger,
  Viewer,
  Controls,
  Close,
  Previous,
  Next,
  Filmstrip,
  Sheet,
  SheetStop,
  ThumbnailOverlay,
  FrameLayer,
};
```

## Angular

```ts
@Directive({ selector: "[fluidboxFilmstrip]" })
class FluidboxFilmstripDirective<TItem extends FluidboxItem = FluidboxItem> {}

/**
 * What draws an item: a template with `let-item let-index="index"`, or a
 * component, which gets `item` and `index` inputs when it declares them.
 */
type FluidboxContent<TItem extends FluidboxItem = FluidboxItem> =
  | TemplateRef<FluidboxItemContext<TItem>>
  | Type<unknown>;

/**
 * What the sheet's and the filmstrip's templates get: `{ item, index }`, with
 * the item also as `$implicit`, so `let-item let-index="index"`.
 */
interface FluidboxItemContext<TItem extends FluidboxItem = FluidboxItem> {
  $implicit: TItem;
  item: TItem;
  index: number;
}

@Directive({ selector: "button[fluidboxClose]" })
class FluidboxCloseDirective extends FluidboxControlDirective {}

abstract class FluidboxControlDirective {
  /** Overrides the disabled state the viewer gives the control. */
  @Input() disabled?: boolean;
  readonly isDisabled: Signal<boolean>;
}

@Component({ selector: "[fluidboxControls]" })
class FluidboxControlsComponent {}

@Directive({ selector: "[fluidboxFrameLayer]" })
class FluidboxFrameLayerDirective {}

@Directive({ selector: "button[fluidboxNext]" })
class FluidboxNextDirective extends FluidboxControlDirective {}

@Directive({ selector: "button[fluidboxPrevious]" })
class FluidboxPreviousDirective extends FluidboxControlDirective {}

@Directive({ selector: "button[fluidboxSheetStop]" })
class FluidboxSheetStopDirective {
  @Input({ required: true, transform: numberAttribute }) fluidboxSheetStop!: number;
}

@Component({ selector: "[fluidboxThumbnailOverlay]" })
class FluidboxThumbnailOverlayComponent<TItem extends FluidboxItem = FluidboxItem> {
  /** Fly with the photo. */
  @Input({ transform: booleanAttribute }) travel = false;
  /** The content, rendered here and, with `travel`, into the flying frame. */
  @ContentChild(TemplateRef, { descendants: false }) content?: TemplateRef<unknown>;
  /** For fluidboxFrameLayer: the element that flies, or `undefined` when it goes. */
  setFrameLayer(layer: HTMLElement | undefined): void;
}

@Directive({ selector: "[fluidboxTrigger]", exportAs: "fluidboxTrigger" })
class FluidboxTriggerDirective<TItem extends FluidboxItem = FluidboxItem> {
  /** The item opened: the item, its index or its id. */
  @Input({ required: true }) fluidboxTrigger!: FluidboxTarget<TItem>;
  /** More items that close back onto this trigger, as for an album cover. */
  @Input() fluidboxReturns?: readonly FluidboxTarget<TItem>[];
  /** The item opened. */
  readonly target: Signal<FluidboxTarget<TItem>>;
  /** The other items that return here. */
  readonly returns: Signal<readonly FluidboxTarget<TItem>[]>;
  /**
   * `{ item, index, status }`: the item this trigger shows, which follows the
   * viewer while it is open. Read it in the template; it renders synchronously
   * before the closing flight measures the trigger.
   */
  readonly preview: Signal<FluidboxTriggerPreview<TItem>>;
}

@Directive({ selector: "ng-template[fluidboxViewer]" })
class FluidboxViewerDirective {
  /** Draws the template into the viewer's layer. */
  readonly render: FluidboxViewerRenderer;
}

@Directive({ selector: "[fluidboxRoot]", exportAs: "fluidbox" })
class FluidboxRootDirective<TItem extends FluidboxItem = FluidboxItem> {
  /** The gallery's items: an array, or a list read on demand. */
  @Input() items: FluidboxItems<TItem> = [];
  /**
   * Whether the viewer is open, for `[(open)]`. The viewer opens on `index`
   * and closes when this changes. Leave it out to let triggers own it.
   */
  @Input("open") opened?: boolean;
  /** The item the viewer shows, for `[(index)]`. */
  @Input() index?: number;
  /** Your strings for the viewer. */
  @Input() labels?: Partial<FluidboxLabels>;
  /** Zoom steps, or false to turn zoom off. */
  @Input() zoom?: FluidboxOptions["zoom"];
  /** Go round: past the last item comes the first. A single item never loops. */
  @Input({ transform: booleanAttribute }) loop = false;
  /** Draw `custom` items, by the name in their `render`: a template or a component. */
  @Input() renderers?: Readonly<Record<string, FluidboxContent<TItem>>>;
  /** Find origins the viewer can't see, as in a virtualized grid. */
  @Input() origins?: FluidboxOriginAdapter<TItem>;
  /** When the viewer's code loads: on `"hover"` (the default), on `"press"`, or `"eager"`ly. */
  @Input() loading?: FluidboxOptions["loading"];
  /** Before the viewer opens or closes. `$event.preventDefault()` cancels a command. */
  @Output() readonly beforeToggle = new EventEmitter<FluidboxToggleEvent>();
  /** As the viewer starts to open or close. */
  @Output() readonly toggle = new EventEmitter<FluidboxToggleEvent>();
  /** Before the viewer moves to another item. `$event.preventDefault()` keeps it still. */
  @Output() readonly beforeItemChange = new EventEmitter<FluidboxItemChangeEvent<TItem>>();
  /** When the active item changes, opening included. */
  @Output() readonly itemChange = new EventEmitter<FluidboxItemChangeEvent<TItem>>();
  /** When the active item comes to rest. */
  @Output() readonly settle = new EventEmitter<FluidboxItemChangeEvent<TItem>>();
  /** When the sheet comes to rest at another stop. */
  @Output() readonly sheetChange = new EventEmitter<FluidboxSheetEvent>();
  /** As the viewer opens or closes, from any input. Half of `[(open)]`. */
  @Output() readonly openChange = new EventEmitter<boolean>();
  /** As the viewer moves to another item, opening included. Half of `[(index)]`. */
  @Output() readonly indexChange = new EventEmitter<number>();
  /** The fluidbox. Parts bind to it as they are made. */
  readonly fluidbox: Fluidbox<TItem>;
  /** The viewer's state. */
  readonly state: Signal<FluidboxState<TItem>>;
  /** The item list, which changes with `items`. */
  readonly list: Signal<FluidboxItemList<TItem>>;
  /** The labels, with the defaults filled in. */
  readonly resolvedLabels: Signal<FluidboxLabels>;
  /** State and commands, as `injectFluidbox()` gives them. */
  readonly api: Signal<FluidboxApi<TItem>>;
  /** The sheet's stop and command, as `injectFluidboxSheet()` gives them. */
  readonly sheet: FluidboxSheetApi;
  /** Open an item, by the item, its index or its id, or move to it while open. */
  open(target?: FluidboxTarget<TItem>, options?: FluidboxCommandOptions): Promise<boolean>;
  close(options?: FluidboxCommandOptions): Promise<boolean>;
  next(options?: FluidboxCommandOptions): Promise<boolean>;
  previous(options?: FluidboxCommandOptions): Promise<boolean>;
  setItems(items: FluidboxItems<TItem>): Promise<boolean>;
  /** Start loading an item a press is likely to open. Call the result to let go. */
  preload(target: FluidboxTarget<TItem>): () => void;
  /** Load the viewer's code now. Resolves to true once it has loaded, false if it failed. */
  load(): Promise<boolean>;
}

/**
 * The fluidbox of the root above, as a signal of its state and commands, or of
 * what `select` picks from them. Call in an injection context.
 */
function injectFluidbox<TItem extends FluidboxItem = FluidboxItem>(): Signal<
  FluidboxApi<TItem>
>;
function injectFluidbox<TItem extends FluidboxItem = FluidboxItem, T = unknown>(
  select: (fluidbox: FluidboxApi<TItem>) => T,
): Signal<T>;

/** The sheet of the root above: its stop as a signal, and `set`. Call in an injection context. */
function injectFluidboxSheet(): FluidboxSheetApi;

/** The fluidbox as `injectFluidbox()` gives it: its state, and its commands. */
interface FluidboxApi<
  TItem extends FluidboxItem = FluidboxItem,
> extends FluidboxState<TItem> {
  /** Open an item, by the item, its index or its id, or move to it while open. Without one, the current item, else the first. */
  open(target?: FluidboxTarget<TItem>, options?: FluidboxCommandOptions): Promise<boolean>;
  close(options?: FluidboxCommandOptions): Promise<boolean>;
  next(options?: FluidboxCommandOptions): Promise<boolean>;
  previous(options?: FluidboxCommandOptions): Promise<boolean>;
  setItems(items: FluidboxItems<TItem>): Promise<boolean>;
  /** Start loading an item a press is likely to open. Call the result to let go. */
  preload(target: FluidboxTarget<TItem>): () => void;
  /** Load the viewer's code now. Resolves to true once it has loaded, false if it failed. */
  load(): Promise<boolean>;
}

/** The sheet as `injectFluidboxSheet()` gives it. */
interface FluidboxSheetApi {
  /** The stop the sheet rests at: 0 at rest, then one per snap point. Follows `sheetchange`. */
  readonly stop: Signal<number>;
  /** Move the sheet to a stop. Ignored while the viewer is not open. */
  set(stop: number, options?: { readonly animate?: boolean }): void;
}

@Directive({ selector: "ng-template[fluidboxSheet]" })
class FluidboxSheetDirective<TItem extends FluidboxItem = FluidboxItem> {
  /** The stops above the resting one, lowest first. @default ["60%", "content"] */
  @Input() snapPoints?: readonly FluidboxSheetSnapPoint[];
  /** What a swipe down on the photo does with the sheet down. @default "flight" */
  @Input() dismiss?: "flight" | "scroll" | "none";
  /** Fills the sheet for the active item. */
  readonly render: FluidboxRenderer<TItem>;
}

/** What made a controlled `open` or `index` change. */
interface FluidboxChangeDetails {
  readonly reason: FluidboxReason;
}

/** A trigger's preview: the item it shows, which follows the viewer while it is open. */
interface FluidboxTriggerPreview<TItem extends FluidboxItem = FluidboxItem> {
  /** Null when the trigger's items have left the list. */
  readonly item: TItem | null;
  readonly index: number | null;
  readonly status: FluidboxStatus;
}

class FluidboxModule {}
```

## Binding kit

| Export | What it does |
| --- | --- |
| `controlNames` | The default controls, in the order they render. |
| `fluidboxControls` | Each default control's attribute, label, disabled rule and command. |
| `FluidboxControl` | The label, disabled state and command of each default viewer control. |
| `defaultLabels` | The labels the viewer uses for any you don't pass. |
| `resolveLabels` | Your labels over the defaults. |
| `bindTrigger` | Make an element a trigger with DOM listeners, for plain JavaScript and the custom elements. Framework bindings wire createTrigger into their own events. |
| `createTrigger` | Make a trigger for a fluidbox. Call setElement once the element exists. |
| `frameLayerOf` | The element of a thumbnail overlay marked `data-fluidbox-frame-layer`, the overlay itself or one inside it: it moves into the flying frame for the flight and scales with it, as a scrim does. |
| `resolveOrigin` | Find the parts of a trigger the flight uses: the box marked with `data-fluidbox-origin`, else its image or video, else the trigger itself. |
| `FluidboxTrigger` | The behaviour of a trigger, apart from its element and events, so each framework can wire it into its own event system. |
| `FluidboxTriggerOptions` | What a trigger opens and returns to. |
| `FluidboxTriggerPointer` | Pointer details a trigger reads. DOM and framework events both fit. |
| `FluidboxTriggerPreview` | A trigger's preview: the item it shows, which follows the viewer while it is open. |
| `idOf` | The id of an item: its own, else its src. Throws a TypeError when it has neither. |
| `itemLabel` | The text an item is named by: its alt text or title. |
| `targetKey` | A target as an index or an id. An item names itself by its id. |
| `targetKeys` | A target and the items that return to it, as indexes and ids, without repeats. |
| `fluidboxControlIcon` | The default control icons, as attributes and a path, for frameworks that build their own SVG. |
| `fluidboxEventTypes` | The six event names, for bindings that forward them as props, emits or outputs. |
| `latestOrigins` | An origins adapter that reads the newest adapter on every call, as a framework prop changes. Without a `revealOrigin` of its own, the viewer scrolls the trigger into view. |
| `unloadFluidbox` | Drop a fluidbox's viewer and close it at once, keeping its bindings, as a binding's unmount does. The next press or open makes a new viewer, so a StrictMode remount keeps working. |
| `indexIn` | The index of an id in a list, or null. |
| `createFluidboxSync` | Make the function a binding calls with an app's `open` and `index` each time either changes. It acts only on the one that changed: the app often hears of an open before the move that came with it, and an index it hasn't caught up on yet must not pull the viewer back. `undefined` leaves that one to the viewer, and an index out of range is ignored, as a stale `?photo=` in a URL would be. |
| `watchFluidbox` | Report the viewer opening, closing and moving, as controlled `open` and `index` change callbacks, until `signal` aborts. |
| `FluidboxChangeDetails` | What made a controlled `open` or `index` change. |
| `fluidboxStaticViewer` | The open viewer as HTML, for a server to render where the viewer's root is. It uses the viewer's own markup and stylesheet, absolute at the top of the page, and the viewer takes it over once it loads. Render it outside any positioned or transformed element, which it would sit inside. Only image items have one; for any other item this returns `null` and the viewer opens as usual. |
| `FluidboxStaticViewerOptions` | What the server needs to draw the open viewer. |
| `takeOverFluidbox` | Take over the open viewer the server rendered inside `element`: open at `index` with no motion, and hide the server's copy in the frame the viewer first paints. Resolves whether the viewer opened. A takeover that fails hides the copy too, unless a newer one is under way, as after StrictMode's remount. |
| `FluidboxSlot` | Where a framework draws one piece of rendered content, as a portal. |
| `FluidboxSlots` | A renderer for the sheet or the filmstrip that lists its targets for a framework to fill. |
| `createSlots` | Make a renderer whose targets a framework fills with its own components. |
| `FluidboxAddonName` | The add-ons that load from their own entry. |

## Type index

Every type `@ogdakke/fluidbox` exports: `Fluidbox`, `FluidboxBindOptions`, `FluidboxCommandOptions`, `FluidboxControlName`, `FluidboxCustomItem`, `FluidboxEventMap`, `FluidboxFilmstripOptions`, `FluidboxIframeItem`, `FluidboxImageItem`, `FluidboxImageProps`, `FluidboxItem`, `FluidboxItemBase`, `FluidboxItemList`, `FluidboxItemListOptions`, `FluidboxItems`, `FluidboxLabels`, `FluidboxLoading`, `FluidboxOptions`, `FluidboxOrigin`, `FluidboxOriginAdapter`, `FluidboxOverlayOptions`, `FluidboxPictureSource`, `FluidboxReason`, `FluidboxRenderContext`, `FluidboxRenderer`, `FluidboxRevealContext`, `FluidboxSheet`, `FluidboxSheetOptions`, `FluidboxSheetSnapPoint`, `FluidboxState`, `FluidboxStatus`, `FluidboxTarget`, `FluidboxThumbnail`, `FluidboxToggleState`, `FluidboxUpdateOptions`, `FluidboxVideoItem`, `FluidboxViewerContext` and `FluidboxViewerRenderer`. The classes `FluidboxItemChangeEvent`, `FluidboxSheetEvent` and `FluidboxToggleEvent`, and the functions `createFluidbox`, `createItemList`, `idOf`, `itemLabel` and `imageItem`, are values.
