/** Rotation timing from the spec ("Featured experts carousel"). */ export const FEATURED_INTERVAL_MS = 2800; export type FeaturedSlot = "c0" | "l1" | "r1" | "l2" | "r2" | "l3" | "r3"; /** * Where item `i` sits when `current` is in the center: by its signed offset * from the current index, wrapped so the shorter way round wins. With two * items the other one is a single right neighbor, never an empty pair. */ export function featuredSlot(i: number, current: number, count: number): FeaturedSlot { let off = (((i - current) % count) + count) % count; if (off > count / 2) off -= count; if (off === 0) return "c0"; if (off === 1) return "r1"; if (off === -1) return "l1"; if (off === 2) return "r2"; if (off === -2) return "l2"; return off > 0 ? "r3" : "l3"; } export interface FeaturedAutoplay { readonly index: number; readonly paused: boolean; pause(): void; resume(): void; stop(): void; } /** * Advances the index every `intervalMs` while not paused. A single item never * rotates. The caller stops it on unmount. */ export function startFeaturedAutoplay(opts: { count: number; onChange: (index: number) => void; intervalMs?: number; start?: number; }): FeaturedAutoplay { const { count, onChange, intervalMs = FEATURED_INTERVAL_MS } = opts; let index = opts.start ?? 0; let paused = false; const timer = count > 1 ? setInterval(() => { if (paused) return; index = (index + 1) % count; onChange(index); }, intervalMs) : null; return { get index() { return index; }, get paused() { return paused; }, pause() { paused = true; }, resume() { paused = false; }, stop() { if (timer) clearInterval(timer); }, }; } /** * WAI-ARIA carousel pattern: a rotating carousel must not announce every * slide, so the region is `off` while autoplay runs and `polite` when the * person has paused it (hover, focus) or it never rotates (reduced motion, * a single expert). */ export function featuredLiveMode(opts: { rotating: boolean; paused: boolean }): "off" | "polite" { return opts.rotating && !opts.paused ? "off" : "polite"; }