/* eslint-disable react-hooks/set-state-in-effect -- pre-existing pattern flagged by the React Compiler rules added in eslint-plugin-react-hooks v7; refactoring it is out of scope for the lint toolchain upgrade. */ import { useCallback, useEffect, useState } from 'react' const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max) export interface UseCarouselOptions { /** Total number of items in the carousel. */ length: number /** Index to display when the carousel first opens / re-opens. */ initialIndex: number /** When `false`, the keyboard listener is detached. */ open: boolean } export interface UseCarouselResult { /** Currently displayed index (clamped to `[0, length - 1]`). */ index: number /** Step one item back, wrapping at the start. */ prev: () => void /** Step one item forward, wrapping at the end. */ next: () => void } /** * Tiny carousel state used by `ImageViewer`, `VideoViewer`, and * `PdfViewer` when their items array has more than one entry. * * - Resets to `initialIndex` whenever the viewer (re-)opens, so * clicking a different tile on the bubble surface lands on that * item rather than restoring the last-viewed position. * - Clamps defensively when `length` shrinks below the active index * (server push / optimistic update / stack edit) so the viewer * gracefully falls back instead of dereferencing `undefined`. * - Listens for `ArrowLeft` / `ArrowRight` while the viewer is open, * but yields when the focused element is a `