{"version":3,"file":"Lightbox.cjs","names":[],"sources":["../../../src/components/Lightbox/Lightbox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — closeLabel,\n * previousLabel and nextLabel are three separate strings an app has to translate,\n * showThumbnails/showCounter/loop are the chrome, and items/index/\n * onIndexChange/open/onClose are the controlled contract. The body is the focus\n * trap, the key handling and the swipe, which all move the same index.\n */\nimport { ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { useScrollLock } from \"@/hooks/use-scroll-lock\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"../Portal\";\nimport styles from \"./Lightbox.module.css\";\n\n/** One item of the gallery. */\nexport interface LightboxItem {\n    /** Full-size image URL. */\n    src: string;\n    /** Alt text. Required: a gallery of unlabeled images is unusable with a screen reader. */\n    alt: string;\n    /** Caption rendered under the image. */\n    caption?: string;\n    /** Thumbnail URL for the strip. Falls back to `src`. */\n    thumbnail?: string;\n}\n\nexport interface LightboxProps {\n    items: LightboxItem[];\n    /** Whether the overlay is open. */\n    open: boolean;\n    /** Index shown while open. Default `0`. */\n    index?: number;\n    onIndexChange?: (index: number) => void;\n    onClose: () => void;\n    /** Renders the thumbnail strip. Default `true` when there is more than one item. */\n    showThumbnails?: boolean;\n    /** Renders the `3 / 12` counter. Default `true`. */\n    showCounter?: boolean;\n    /**\n     * Wrap around at the ends. Default `true` — in a photo viewer, hitting a dead\n     * end at the last image reads as a bug more often than as a boundary.\n     */\n    loop?: boolean;\n    closeLabel?: string;\n    previousLabel?: string;\n    nextLabel?: string;\n    className?: string;\n}\n\n/**\n * Full-screen image viewer with keyboard navigation.\n *\n * `Esc` closes, `←`/`→` walk the gallery, `Home`/`End` jump to the ends. Focus is\n * trapped inside while open and the page behind is scroll-locked, so the overlay\n * behaves like the dialog it is instead of a fancy `<div>` the keyboard can escape.\n *\n * Only the current image is mounted; neighbours are **preloaded** via `Image()` so\n * pressing `→` does not flash an empty frame.\n *\n * @example\n * ```tsx\n * const [open, setOpen] = useState(false);\n * const [index, setIndex] = useState(0);\n *\n * <Lightbox\n *   open={open}\n *   items={photos.map((p) => ({ src: p.url, alt: p.description }))}\n *   index={index}\n *   onIndexChange={setIndex}\n *   onClose={() => setOpen(false)}\n * />\n * ```\n */\nexport function Lightbox({\n    items,\n    open,\n    index = 0,\n    onIndexChange,\n    onClose,\n    showThumbnails,\n    showCounter = true,\n    loop = true,\n    closeLabel = \"Close\",\n    previousLabel = \"Previous image\",\n    nextLabel = \"Next image\",\n    className,\n}: LightboxProps) {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const [internalIndex, setInternalIndex] = useState(index);\n    const isControlled = onIndexChange !== undefined;\n    const current = isControlled ? index : internalIndex;\n\n    useFocusTrap(containerRef, open);\n    useScrollLock(open);\n\n    const move = useCallback(\n        (next: number): void => {\n            if (items.length === 0) return;\n            const target = loop\n                ? (next + items.length) % items.length\n                : Math.max(0, Math.min(next, items.length - 1));\n            if (!isControlled) setInternalIndex(target);\n            onIndexChange?.(target);\n        },\n        [isControlled, items.length, loop, onIndexChange],\n    );\n\n    useEffect(() => {\n        if (!isControlled) setInternalIndex(index);\n    }, [index, isControlled]);\n\n    useEffect(() => {\n        if (!open) return;\n        function onKeyDown(event: KeyboardEvent): void {\n            switch (event.key) {\n                case \"Escape\":\n                    onClose();\n                    break;\n                case \"ArrowRight\":\n                    move(current + 1);\n                    break;\n                case \"ArrowLeft\":\n                    move(current - 1);\n                    break;\n                case \"Home\":\n                    move(0);\n                    break;\n                case \"End\":\n                    move(items.length - 1);\n                    break;\n                default:\n                    return;\n            }\n            event.preventDefault();\n        }\n        document.addEventListener(\"keydown\", onKeyDown);\n        return () => document.removeEventListener(\"keydown\", onKeyDown);\n    }, [current, items.length, move, onClose, open]);\n\n    useEffect(() => {\n        if (!open || typeof Image === \"undefined\") return;\n        for (const offset of [-1, 1]) {\n            const neighbour = items[(current + offset + items.length) % items.length];\n            if (!neighbour) continue;\n            const preload = new Image();\n            preload.src = neighbour.src;\n        }\n    }, [current, items, open]);\n\n    if (!open || items.length === 0) return null;\n\n    const item = items[Math.max(0, Math.min(current, items.length - 1))];\n    const withThumbnails = showThumbnails ?? items.length > 1;\n    const multiple = items.length > 1;\n\n    return (\n        <Portal>\n            <div\n                ref={containerRef}\n                role=\"dialog\"\n                aria-modal=\"true\"\n                aria-label={item.alt}\n                className={cn(styles.overlay, className)}\n            >\n                <div className={styles.topBar}>\n                    {showCounter && multiple ? (\n                        <span className={styles.counter}>\n                            {current + 1} / {items.length}\n                        </span>\n                    ) : (\n                        <span />\n                    )}\n                    <button\n                        type=\"button\"\n                        className={styles.iconButton}\n                        aria-label={closeLabel}\n                        onClick={onClose}\n                    >\n                        <X size={20} />\n                    </button>\n                </div>\n\n                <div className={styles.stage}>\n                    {multiple ? (\n                        <button\n                            type=\"button\"\n                            className={cn(styles.iconButton, styles.nav)}\n                            aria-label={previousLabel}\n                            disabled={!loop && current === 0}\n                            onClick={() => move(current - 1)}\n                        >\n                            <ChevronLeft size={24} />\n                        </button>\n                    ) : null}\n\n                    <figure className={styles.figure}>\n                        <img src={item.src} alt={item.alt} className={styles.image} />\n                        {item.caption ? (\n                            <figcaption className={styles.caption}>{item.caption}</figcaption>\n                        ) : null}\n                    </figure>\n\n                    {multiple ? (\n                        <button\n                            type=\"button\"\n                            className={cn(styles.iconButton, styles.nav)}\n                            aria-label={nextLabel}\n                            disabled={!loop && current === items.length - 1}\n                            onClick={() => move(current + 1)}\n                        >\n                            <ChevronRight size={24} />\n                        </button>\n                    ) : null}\n                </div>\n\n                {withThumbnails && multiple ? (\n                    <div className={styles.thumbnails} role=\"tablist\" aria-label={item.alt}>\n                        {items.map((entry, entryIndex) => (\n                            <button\n                                key={`${entry.src}-${entryIndex}`}\n                                type=\"button\"\n                                role=\"tab\"\n                                aria-selected={entryIndex === current}\n                                aria-label={entry.alt}\n                                className={cn(\n                                    styles.thumbnail,\n                                    entryIndex === current && styles.thumbnailActive,\n                                )}\n                                onClick={() => move(entryIndex)}\n                            >\n                                <img\n                                    src={entry.thumbnail ?? entry.src}\n                                    alt=\"\"\n                                    className={styles.thumbnailImage}\n                                />\n                            </button>\n                        ))}\n                    </div>\n                ) : null}\n            </div>\n        </Portal>\n    );\n}\n"],"mappings":"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"}