@deftui/react 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2395 @@
1
+ "use client";
2
+ import { Spinner } from "../shadcn/spinner.mjs";
3
+ import { Button } from "../shadcn/button.mjs";
4
+ import { DropdownMenu, DropdownMenuItem, DropdownMenuTrigger } from "../shadcn/dropdown-menu.mjs";
5
+ import { Skeleton } from "../shadcn/skeleton.mjs";
6
+ import { cn } from "cn";
7
+ import { mergeProps } from "@base-ui/react/merge-props";
8
+ import * as React$1 from "react";
9
+ import { ChevronLeftIcon, ChevronRightIcon, DownloadIcon, EllipsisIcon, ImageOffIcon, XIcon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { Dialog } from "@base-ui/react/dialog";
12
+ import { Menu } from "@base-ui/react/menu";
13
+ import { useRender } from "@base-ui/react/use-render";
14
+ import { animate, useReducedMotion } from "motion/react";
15
+ import { flushSync } from "react-dom";
16
+ import { ScrollArea } from "@base-ui/react/scroll-area";
17
+ // src/vendor/tailored/lightbox.tsx
18
+ const LIGHTBOX_STYLES = String.raw`
19
+ /* Keep child modals' transparent input blockers above the viewer. */
20
+ :where([data-slot="lightbox-portal"] [data-base-ui-portal],
21
+ [data-slot="lightbox-portal"] ~ [data-base-ui-portal])
22
+ > :where([data-base-ui-inert][role="presentation"]) {
23
+ z-index: 50;
24
+ }
25
+
26
+ :where([data-slot="lightbox-viewport"]) {
27
+ overscroll-behavior: contain;
28
+ }
29
+
30
+ :where([data-slot="lightbox-slides"]) {
31
+ touch-action: none;
32
+ -webkit-user-select: none;
33
+ user-select: none;
34
+ }
35
+
36
+ :where([data-slot="lightbox-slide"]) {
37
+ transform-origin: center;
38
+ will-change: transform, opacity;
39
+ backface-visibility: hidden;
40
+ contain: layout paint;
41
+ }
42
+
43
+ :where([data-slot="lightbox-zoom"]) {
44
+ transform-origin: center;
45
+ will-change: transform;
46
+ backface-visibility: hidden;
47
+ }
48
+
49
+ :where([data-slot="lightbox-zoom"][data-zoomable="true"]:not([data-zoomed="true"])) {
50
+ cursor: zoom-in;
51
+ }
52
+
53
+ :where([data-slot="lightbox-zoom"][data-zoomed="true"]) {
54
+ cursor: zoom-out;
55
+ }
56
+
57
+ :where([data-slot="lightbox-zoom"][data-zoomed="true"][data-dragging="true"]) {
58
+ cursor: grabbing;
59
+ }
60
+
61
+ :where([data-slot="lightbox-content"][data-dragging="true"]),
62
+ :where([data-slot="lightbox-content"][data-dragging="true"] *) {
63
+ cursor: grabbing;
64
+ }
65
+
66
+ :where([data-slot="lightbox-slide"][aria-hidden="true"]) {
67
+ pointer-events: none;
68
+ }
69
+
70
+ :where([data-slot="lightbox-thumbnails"]) {
71
+ overscroll-behavior-inline: contain;
72
+ scrollbar-width: thin;
73
+ }
74
+
75
+ html.lightbox-morph-opening [data-slot="lightbox-content"] {
76
+ scale: 1 !important;
77
+ transition-property: opacity !important;
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce) {
81
+ :where([data-slot="lightbox-slide"]) {
82
+ transition: none !important;
83
+ animation: none !important;
84
+ }
85
+
86
+ }
87
+
88
+ @media (forced-colors: active) {
89
+ :where([data-slot="lightbox-toolbar"] button:focus-visible),
90
+ :where([data-slot="lightbox-viewport"] button:focus-visible),
91
+ :where([data-slot="lightbox-thumbnails"] button:focus-visible) {
92
+ outline: 2px solid CanvasText;
93
+ outline-offset: 2px;
94
+ }
95
+ }
96
+ `;
97
+ const EMPTY_CAPTIONS_TRACK = "data:text/vtt;charset=utf-8,WEBVTT%0A%0A";
98
+ const VIDEO_THUMBNAIL_MAX_SIZE = 112;
99
+ const interactiveSelector = "a,button,input,select,textarea,summary,video,audio,iframe,[contenteditable=true],[role=button],[role=link],[data-lightbox-gesture-ignore]";
100
+ const postGestureActivationSelector = "a[href],button,input:not([type=\"hidden\"]),select,textarea,summary,label,[contenteditable=true],[role=\"button\"],[role=\"link\"],[role=\"checkbox\"],[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"],[role=\"option\"],[role=\"radio\"],[role=\"switch\"],[role=\"tab\"]";
101
+ const generatedVideoThumbnails = /* @__PURE__ */ new WeakMap();
102
+ function mergeLightboxBackdropStyle(baseStyle, style) {
103
+ if (typeof style === "function") return (state) => {
104
+ return {
105
+ ...baseStyle,
106
+ ...style(state)
107
+ };
108
+ };
109
+ return {
110
+ ...baseStyle,
111
+ ...style
112
+ };
113
+ }
114
+ function mergeLightboxContentStyle(baseStyle, style) {
115
+ if (typeof style === "function") return (state) => {
116
+ return {
117
+ ...baseStyle,
118
+ ...style(state)
119
+ };
120
+ };
121
+ return {
122
+ ...baseStyle,
123
+ ...style
124
+ };
125
+ }
126
+ const initialTransform = {
127
+ scale: 1,
128
+ panX: 0,
129
+ panY: 0,
130
+ swipeX: 0,
131
+ dismissY: 0,
132
+ dragging: false
133
+ };
134
+ const LightboxContext = React$1.createContext(null);
135
+ function useLightbox() {
136
+ const context = React$1.useContext(LightboxContext);
137
+ if (!context) throw new Error("Lightbox parts must be rendered inside <Lightbox>.");
138
+ return context;
139
+ }
140
+ function getAssetLoadKey(type, source) {
141
+ return `${type}:${source}`;
142
+ }
143
+ function getMediaLoadKey(item) {
144
+ return getAssetLoadKey(item.type, item.src);
145
+ }
146
+ function getThumbnailLoadKey(item) {
147
+ const source = item.type === "image" ? item.thumbnailSrc ?? item.src : item.poster ?? item.src;
148
+ return getAssetLoadKey(item.type, source);
149
+ }
150
+ function clamp(value, min, max) {
151
+ return Math.min(Math.max(value, min), max);
152
+ }
153
+ function normalizeIndex(index, length, loop) {
154
+ if (length === 0) return 0;
155
+ if (loop) return (index % length + length) % length;
156
+ return clamp(index, 0, length - 1);
157
+ }
158
+ function getItemLabel(item, index) {
159
+ if (!item) return `Media ${index + 1}`;
160
+ return item.type === "image" ? item.alt || `Image ${index + 1}` : item.label;
161
+ }
162
+ function isElementVisible(element) {
163
+ const rect = element.getBoundingClientRect();
164
+ return rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
165
+ }
166
+ function getMorphTarget(element) {
167
+ if (!element) return null;
168
+ if (element.matches("[data-lightbox-morph-target]")) return element;
169
+ const explicitTarget = element.querySelector("[data-lightbox-morph-target]");
170
+ if (explicitTarget) return explicitTarget;
171
+ if (element.matches("img,video,canvas")) return element;
172
+ return element.querySelector("img,video,canvas") ?? element;
173
+ }
174
+ function isStandaloneMedia(media, gallery) {
175
+ const interactiveAncestor = media.parentElement?.closest(interactiveSelector);
176
+ return !interactiveAncestor || interactiveAncestor === gallery;
177
+ }
178
+ function getStandaloneGalleryMedia(target, gallery) {
179
+ if (!(target instanceof Element)) return null;
180
+ const media = target.closest("img,video");
181
+ if (!(media instanceof HTMLImageElement || media instanceof HTMLVideoElement) || !gallery.contains(media) || !isStandaloneMedia(media, gallery)) return null;
182
+ return media;
183
+ }
184
+ function getPostGestureActivationTarget(target) {
185
+ if (!(target instanceof Element)) return null;
186
+ const element = target.closest(postGestureActivationSelector);
187
+ if (!element || element.matches(":disabled,[aria-disabled=\"true\"]") || element.closest("[inert]")) return null;
188
+ return element;
189
+ }
190
+ function imageToItem(image) {
191
+ const figure = image.closest("figure");
192
+ const caption = image.dataset.lightboxCaption ?? figure?.querySelector(":scope > figcaption")?.textContent?.trim() ?? void 0;
193
+ const width = Number(image.getAttribute("width"));
194
+ const height = Number(image.getAttribute("height"));
195
+ const srcSet = image.srcset || void 0;
196
+ return {
197
+ id: image.id || void 0,
198
+ type: "image",
199
+ src: image.currentSrc || image.src,
200
+ alt: image.alt,
201
+ width: width > 0 ? width : image.naturalWidth || void 0,
202
+ height: height > 0 ? height : image.naturalHeight || void 0,
203
+ srcSet,
204
+ sizes: srcSet ? image.sizes || void 0 : void 0,
205
+ thumbnailSrc: image.currentSrc || image.src,
206
+ caption
207
+ };
208
+ }
209
+ function getVideoThumbnail(video) {
210
+ const source = video.currentSrc || video.src;
211
+ if (!source || video.readyState < 2 || video.videoWidth <= 0 || video.videoHeight <= 0) return;
212
+ const cached = generatedVideoThumbnails.get(video);
213
+ if (cached?.source === source) return cached.thumbnail;
214
+ const scale = Math.min(1, VIDEO_THUMBNAIL_MAX_SIZE / Math.max(video.videoWidth, video.videoHeight));
215
+ const canvas = document.createElement("canvas");
216
+ canvas.width = Math.max(1, Math.round(video.videoWidth * scale));
217
+ canvas.height = Math.max(1, Math.round(video.videoHeight * scale));
218
+ try {
219
+ const context = canvas.getContext("2d");
220
+ if (!context) return void 0;
221
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
222
+ const thumbnail = canvas.toDataURL("image/webp", .72);
223
+ generatedVideoThumbnails.set(video, {
224
+ source,
225
+ thumbnail
226
+ });
227
+ return thumbnail;
228
+ } catch {
229
+ return;
230
+ }
231
+ }
232
+ function videoToItem(video) {
233
+ const figure = video.closest("figure");
234
+ const caption = video.dataset.lightboxCaption ?? figure?.querySelector(":scope > figcaption")?.textContent?.trim() ?? void 0;
235
+ const width = Number(video.getAttribute("width"));
236
+ const height = Number(video.getAttribute("height"));
237
+ const sources = Array.from(video.querySelectorAll("source"), (source) => {
238
+ return {
239
+ media: source.media || void 0,
240
+ src: source.src,
241
+ type: source.type || void 0
242
+ };
243
+ });
244
+ const src = [
245
+ video.currentSrc,
246
+ video.dataset.lightboxSrc,
247
+ video.src,
248
+ sources.at(0)?.src
249
+ ].find((source) => source) ?? "";
250
+ const label = [
251
+ video.getAttribute("aria-label"),
252
+ video.title,
253
+ caption
254
+ ].find((value) => value);
255
+ return {
256
+ id: video.id || void 0,
257
+ type: "video",
258
+ src,
259
+ label: label ?? "Video",
260
+ poster: video.poster || getVideoThumbnail(video),
261
+ width: width > 0 ? width : video.videoWidth || void 0,
262
+ height: height > 0 ? height : video.videoHeight || void 0,
263
+ sources: sources.length > 0 ? sources : void 0,
264
+ caption,
265
+ videoProps: {
266
+ autoPlay: video.autoplay,
267
+ controls: true,
268
+ loop: video.loop,
269
+ muted: video.muted,
270
+ playsInline: video.playsInline,
271
+ preload: "auto"
272
+ }
273
+ };
274
+ }
275
+ function getLoadedAssetKeys(media, item) {
276
+ if (!(media instanceof HTMLImageElement ? media.complete && media.naturalWidth > 0 : media.readyState >= 2)) return [];
277
+ const mediaKey = getMediaLoadKey(item);
278
+ const thumbnailKey = getThumbnailLoadKey(item);
279
+ if (mediaKey === thumbnailKey) return [mediaKey];
280
+ return item.type === "video" && item.poster?.startsWith("data:") ? [mediaKey, thumbnailKey] : [mediaKey];
281
+ }
282
+ function mediaToItem(media) {
283
+ return media instanceof HTMLImageElement ? imageToItem(media) : videoToItem(media);
284
+ }
285
+ function hasCustomContent(children) {
286
+ return React$1.Children.toArray(children).some((child) => React$1.isValidElement(child) && child.type.__lightboxContent === true);
287
+ }
288
+ function getEventDetails(reason, event, trigger) {
289
+ return {
290
+ reason,
291
+ event,
292
+ trigger
293
+ };
294
+ }
295
+ function Lightbox({ children, items: itemsProp, open: openProp, defaultOpen = false, onOpenChange, onOpenChangeComplete, onOpen, onClose, index: indexProp, defaultIndex = 0, onChangeImage, overlay = "blur", loop = true, noCounter = false, noCarousel = false, noControls = false, loading = "eager", preload = 1, maxZoom = 4, controls = [], dir, className, actionsRef, ...rootProps }) {
296
+ const galleryRef = React$1.useRef(null);
297
+ const mediaRef = React$1.useRef(null);
298
+ const sourceMapRef = React$1.useRef(/* @__PURE__ */ new Map());
299
+ const galleryTouchPressRef = React$1.useRef(null);
300
+ const suppressedGalleryClickRef = React$1.useRef(null);
301
+ const annotatedMediaRef = React$1.useRef(/* @__PURE__ */ new Map());
302
+ const openingCleanupRef = React$1.useRef(null);
303
+ const postGestureActivationCleanupRef = React$1.useRef(null);
304
+ const zoomAnimationRef = React$1.useRef(null);
305
+ const transformRef = React$1.useRef(initialTransform);
306
+ const [autoItems, setAutoItems] = React$1.useState([]);
307
+ const [loadedAssets, setLoadedAssets] = React$1.useState(() => /* @__PURE__ */ new Set());
308
+ const [uncontrolledOpen, setUncontrolledOpen] = React$1.useState(defaultOpen);
309
+ const [uncontrolledIndex, setUncontrolledIndex] = React$1.useState(defaultIndex);
310
+ const [transform, setTransform] = React$1.useState(initialTransform);
311
+ React$1.useLayoutEffect(() => {
312
+ transformRef.current = transform;
313
+ }, [transform]);
314
+ const [inheritedDirection, setInheritedDirection] = React$1.useState(() => typeof document !== "undefined" && document.documentElement.dir === "rtl" ? "rtl" : "ltr");
315
+ const shouldReduceMotion = Boolean(useReducedMotion());
316
+ const isOpenControlled = openProp !== void 0;
317
+ const isIndexControlled = indexProp !== void 0;
318
+ const open = openProp ?? uncontrolledOpen;
319
+ const items = itemsProp ?? autoItems;
320
+ const activeIndex = normalizeIndex(indexProp ?? uncontrolledIndex, items.length, loop);
321
+ const activeItem = items[activeIndex];
322
+ const direction = dir ?? inheritedDirection;
323
+ const armPostGestureActivationRepair = React$1.useCallback((releaseEvent) => {
324
+ if (!(releaseEvent instanceof PointerEvent) || releaseEvent.pointerType !== "touch" && releaseEvent.pointerType !== "pen") return;
325
+ const releaseTarget = releaseEvent.target;
326
+ const doc = releaseTarget instanceof Node ? releaseTarget.ownerDocument ?? document : document;
327
+ const view = doc.defaultView ?? window;
328
+ postGestureActivationCleanupRef.current?.();
329
+ let active = true;
330
+ let fallbackTimer = 0;
331
+ let suppressionTimer = 0;
332
+ let press = null;
333
+ let pendingTarget = null;
334
+ let syntheticTarget = null;
335
+ function cleanup() {
336
+ if (!active) return;
337
+ active = false;
338
+ view.clearTimeout(fallbackTimer);
339
+ view.clearTimeout(suppressionTimer);
340
+ doc.removeEventListener("click", handleClick, true);
341
+ doc.removeEventListener("pointercancel", handlePointerCancel, true);
342
+ doc.removeEventListener("pointerdown", handlePointerDown, true);
343
+ doc.removeEventListener("pointermove", handlePointerMove, true);
344
+ doc.removeEventListener("pointerup", handlePointerUp, true);
345
+ if (postGestureActivationCleanupRef.current === cleanup) postGestureActivationCleanupRef.current = null;
346
+ }
347
+ function handlePointerDown(event) {
348
+ if (!event.isPrimary) {
349
+ if (press || pendingTarget) cleanup();
350
+ return;
351
+ }
352
+ if (event.button !== 0) return;
353
+ if (press || pendingTarget || syntheticTarget) {
354
+ cleanup();
355
+ return;
356
+ }
357
+ const target = getPostGestureActivationTarget(event.target);
358
+ if (!target || target.closest("[data-slot=\"lightbox-portal\"]")) {
359
+ cleanup();
360
+ return;
361
+ }
362
+ press = {
363
+ pointerId: event.pointerId,
364
+ target,
365
+ startX: event.clientX,
366
+ startY: event.clientY
367
+ };
368
+ }
369
+ function handlePointerMove(event) {
370
+ if (!press || press.pointerId !== event.pointerId) return;
371
+ if (Math.hypot(event.clientX - press.startX, event.clientY - press.startY) > 8) cleanup();
372
+ }
373
+ function handlePointerCancel(event) {
374
+ if (press?.pointerId === event.pointerId) cleanup();
375
+ }
376
+ function handlePointerUp(event) {
377
+ if (!press || press.pointerId !== event.pointerId) return;
378
+ const currentPress = press;
379
+ press = null;
380
+ if (getPostGestureActivationTarget(event.target) !== currentPress.target || Math.hypot(event.clientX - currentPress.startX, event.clientY - currentPress.startY) > 8) {
381
+ cleanup();
382
+ return;
383
+ }
384
+ pendingTarget = currentPress.target;
385
+ fallbackTimer = view.setTimeout(() => {
386
+ const activationTarget = pendingTarget;
387
+ if (!active || !activationTarget?.isConnected || getPostGestureActivationTarget(activationTarget) !== activationTarget) {
388
+ cleanup();
389
+ return;
390
+ }
391
+ pendingTarget = null;
392
+ syntheticTarget = activationTarget;
393
+ activationTarget.click();
394
+ suppressionTimer = view.setTimeout(cleanup, 500);
395
+ }, 80);
396
+ }
397
+ function handleClick(event) {
398
+ if (!event.isTrusted) return;
399
+ const target = event.target instanceof Element ? event.target.closest(postGestureActivationSelector) : null;
400
+ if (syntheticTarget && target === syntheticTarget) {
401
+ event.preventDefault();
402
+ event.stopImmediatePropagation();
403
+ cleanup();
404
+ return;
405
+ }
406
+ if (pendingTarget) cleanup();
407
+ }
408
+ doc.addEventListener("click", handleClick, true);
409
+ doc.addEventListener("pointercancel", handlePointerCancel, true);
410
+ doc.addEventListener("pointerdown", handlePointerDown, true);
411
+ doc.addEventListener("pointermove", handlePointerMove, true);
412
+ doc.addEventListener("pointerup", handlePointerUp, true);
413
+ postGestureActivationCleanupRef.current = cleanup;
414
+ }, []);
415
+ const markAssetsLoaded = React$1.useCallback((keys) => {
416
+ if (keys.length === 0) return;
417
+ setLoadedAssets((current) => {
418
+ const missingKeys = keys.filter((key) => !current.has(key));
419
+ if (missingKeys.length === 0) return current;
420
+ const next = new Set(current);
421
+ for (const key of missingKeys) next.add(key);
422
+ return next;
423
+ });
424
+ }, []);
425
+ const markAssetLoaded = React$1.useCallback((key) => markAssetsLoaded([key]), [markAssetsLoaded]);
426
+ React$1.useLayoutEffect(() => {
427
+ transformRef.current = transform;
428
+ }, [transform]);
429
+ const stopZoomAnimation = React$1.useCallback(() => {
430
+ zoomAnimationRef.current?.stop();
431
+ zoomAnimationRef.current = null;
432
+ }, []);
433
+ const animateZoom = React$1.useCallback((target) => {
434
+ stopZoomAnimation();
435
+ const start = transformRef.current;
436
+ const end = {
437
+ scale: clamp(target.scale, 1, maxZoom),
438
+ panX: target.panX,
439
+ panY: target.panY
440
+ };
441
+ const update = (progress) => {
442
+ const next = {
443
+ ...transformRef.current,
444
+ scale: start.scale + (end.scale - start.scale) * progress,
445
+ panX: start.panX + (end.panX - start.panX) * progress,
446
+ panY: start.panY + (end.panY - start.panY) * progress,
447
+ swipeX: 0,
448
+ dismissY: 0
449
+ };
450
+ transformRef.current = next;
451
+ setTransform(next);
452
+ };
453
+ if (shouldReduceMotion) {
454
+ update(1);
455
+ return;
456
+ }
457
+ zoomAnimationRef.current = animate(0, 1, {
458
+ duration: .22,
459
+ ease: [
460
+ .22,
461
+ 1,
462
+ .36,
463
+ 1
464
+ ],
465
+ onUpdate: update,
466
+ onComplete: () => {
467
+ zoomAnimationRef.current = null;
468
+ }
469
+ });
470
+ }, [
471
+ maxZoom,
472
+ shouldReduceMotion,
473
+ stopZoomAnimation
474
+ ]);
475
+ React$1.useEffect(() => () => {
476
+ openingCleanupRef.current?.();
477
+ postGestureActivationCleanupRef.current?.();
478
+ stopZoomAnimation();
479
+ }, [stopZoomAnimation]);
480
+ const handleGalleryRef = React$1.useCallback((element) => {
481
+ galleryRef.current = element;
482
+ if (!dir && element) setInheritedDirection(window.getComputedStyle(element).direction);
483
+ }, [dir]);
484
+ const restoreAnnotations = React$1.useCallback(() => {
485
+ for (const [media, previous] of annotatedMediaRef.current) for (const [name, value] of Object.entries(previous)) if (value == null) media.removeAttribute(name);
486
+ else media.setAttribute(name, value);
487
+ annotatedMediaRef.current.clear();
488
+ }, []);
489
+ const scanMedia = React$1.useCallback(() => {
490
+ const gallery = galleryRef.current;
491
+ if (!gallery || itemsProp) return {
492
+ foundItems: [],
493
+ sources: []
494
+ };
495
+ restoreAnnotations();
496
+ const mediaItems = Array.from(gallery.querySelectorAll("img,video")).filter((media) => isStandaloneMedia(media, gallery));
497
+ mediaItems.forEach((media, index) => {
498
+ annotatedMediaRef.current.set(media, {
499
+ role: media.getAttribute("role"),
500
+ tabindex: media.getAttribute("tabindex"),
501
+ "aria-haspopup": media.getAttribute("aria-haspopup"),
502
+ "aria-label": media.getAttribute("aria-label"),
503
+ "data-lightbox-source": media.getAttribute("data-lightbox-source")
504
+ });
505
+ media.setAttribute("role", "button");
506
+ media.setAttribute("tabindex", "0");
507
+ media.setAttribute("aria-haspopup", "dialog");
508
+ const label = media instanceof HTMLImageElement ? media.alt || `image ${index + 1}` : [media.getAttribute("aria-label"), media.title].find((value) => value) ?? `video ${index + 1}`;
509
+ media.setAttribute("aria-label", media.getAttribute("aria-label") ?? `Open ${label} in lightbox`);
510
+ media.setAttribute("data-lightbox-source", "");
511
+ if (media instanceof HTMLImageElement && !media.hasAttribute("alt")) console.debug("Lightbox auto-discovered an <img> without an alt attribute.");
512
+ });
513
+ const foundItems = mediaItems.map(mediaToItem);
514
+ markAssetsLoaded(mediaItems.flatMap((media, index) => {
515
+ const item = foundItems[index];
516
+ return item ? getLoadedAssetKeys(media, item) : [];
517
+ }));
518
+ return {
519
+ foundItems,
520
+ sources: mediaItems
521
+ };
522
+ }, [
523
+ itemsProp,
524
+ markAssetsLoaded,
525
+ restoreAnnotations
526
+ ]);
527
+ React$1.useEffect(() => {
528
+ if (itemsProp) {
529
+ restoreAnnotations();
530
+ return;
531
+ }
532
+ const gallery = galleryRef.current;
533
+ if (!gallery) return;
534
+ const refresh = () => {
535
+ if (open && autoItems.length > 0) return;
536
+ const { foundItems, sources } = scanMedia();
537
+ sourceMapRef.current.clear();
538
+ sources.forEach((element, index) => {
539
+ sourceMapRef.current.set(index, {
540
+ element,
541
+ morph: true
542
+ });
543
+ });
544
+ setAutoItems(foundItems);
545
+ };
546
+ refresh();
547
+ const observer = new MutationObserver(refresh);
548
+ observer.observe(gallery, {
549
+ childList: true,
550
+ subtree: true,
551
+ attributes: true,
552
+ attributeFilter: [
553
+ "src",
554
+ "srcset",
555
+ "sizes",
556
+ "alt",
557
+ "poster",
558
+ "width",
559
+ "height",
560
+ "data-lightbox-caption",
561
+ "data-lightbox-src",
562
+ "class"
563
+ ]
564
+ });
565
+ return () => {
566
+ observer.disconnect();
567
+ restoreAnnotations();
568
+ };
569
+ }, [
570
+ autoItems.length,
571
+ itemsProp,
572
+ open,
573
+ restoreAnnotations,
574
+ scanMedia
575
+ ]);
576
+ const changeIndex = React$1.useCallback((nextIndex, reason, event) => {
577
+ if (noCarousel || items.length === 0) return;
578
+ const normalized = normalizeIndex(nextIndex, items.length, loop);
579
+ if (normalized === activeIndex) return;
580
+ stopZoomAnimation();
581
+ if (!isIndexControlled) setUncontrolledIndex(normalized);
582
+ setTransform(initialTransform);
583
+ onChangeImage?.(normalized, items[normalized], {
584
+ reason,
585
+ event,
586
+ previousIndex: activeIndex
587
+ });
588
+ }, [
589
+ activeIndex,
590
+ isIndexControlled,
591
+ items,
592
+ loop,
593
+ noCarousel,
594
+ onChangeImage,
595
+ stopZoomAnimation
596
+ ]);
597
+ const commitOpenChange = React$1.useCallback((nextOpen, details, lifecycleIndex = activeIndex, lifecycleItem = activeItem) => {
598
+ if (!isOpenControlled) setUncontrolledOpen(nextOpen);
599
+ onOpenChange?.(nextOpen, details);
600
+ const lifecycleDetails = {
601
+ reason: details.reason,
602
+ event: "event" in details ? details.event : void 0,
603
+ index: lifecycleIndex,
604
+ item: lifecycleItem
605
+ };
606
+ if (nextOpen) onOpen?.(lifecycleDetails);
607
+ else onClose?.(lifecycleDetails);
608
+ }, [
609
+ activeIndex,
610
+ activeItem,
611
+ isOpenControlled,
612
+ onClose,
613
+ onOpen,
614
+ onOpenChange
615
+ ]);
616
+ const runOpenChange = React$1.useCallback((nextOpen, details, source, lifecycleIndex = activeIndex, lifecycleItem = activeItem) => {
617
+ openingCleanupRef.current?.();
618
+ openingCleanupRef.current = null;
619
+ stopZoomAnimation();
620
+ if (!nextOpen) {
621
+ commitOpenChange(false, details, lifecycleIndex, lifecycleItem);
622
+ return;
623
+ }
624
+ const from = source?.element;
625
+ const canMorph = Boolean(source?.morph && from?.isConnected && isElementVisible(from) && !shouldReduceMotion);
626
+ const root = document.documentElement;
627
+ if (canMorph) root.classList.add("lightbox-morph-opening");
628
+ const fromRect = canMorph ? from?.getBoundingClientRect() : void 0;
629
+ const fromRadius = canMorph ? getComputedStyle(from).borderRadius : "";
630
+ flushSync(() => {
631
+ setTransform(initialTransform);
632
+ commitOpenChange(true, details, lifecycleIndex, lifecycleItem);
633
+ });
634
+ if (!canMorph || !fromRect) return;
635
+ const destination = getMorphTarget(mediaRef.current);
636
+ if (!destination || !isElementVisible(destination)) {
637
+ root.classList.remove("lightbox-morph-opening");
638
+ return;
639
+ }
640
+ const toRect = destination.getBoundingClientRect();
641
+ if (toRect.width <= 0 || toRect.height <= 0) {
642
+ root.classList.remove("lightbox-morph-opening");
643
+ return;
644
+ }
645
+ const scale = Math.max(fromRect.width / toRect.width, fromRect.height / toRect.height);
646
+ const translateX = fromRect.left + fromRect.width / 2 - (toRect.left + toRect.width / 2);
647
+ const translateY = fromRect.top + fromRect.height / 2 - (toRect.top + toRect.height / 2);
648
+ const previous = {
649
+ borderRadius: destination.style.borderRadius,
650
+ opacity: destination.style.opacity,
651
+ transform: destination.style.transform,
652
+ transition: destination.style.transition,
653
+ transformOrigin: destination.style.transformOrigin,
654
+ willChange: destination.style.willChange
655
+ };
656
+ let firstFrame = 0;
657
+ let secondFrame = 0;
658
+ let finishTimer = 0;
659
+ let finished = false;
660
+ const cleanup = () => {
661
+ if (finished) return;
662
+ finished = true;
663
+ window.cancelAnimationFrame(firstFrame);
664
+ window.cancelAnimationFrame(secondFrame);
665
+ window.clearTimeout(finishTimer);
666
+ destination.removeEventListener("transitionend", handleTransitionEnd);
667
+ Object.assign(destination.style, previous);
668
+ root.classList.remove("lightbox-morph-opening");
669
+ if (openingCleanupRef.current === cleanup) openingCleanupRef.current = null;
670
+ };
671
+ const handleTransitionEnd = (event) => {
672
+ if (event.target === destination && event.propertyName === "transform") cleanup();
673
+ };
674
+ Object.assign(destination.style, {
675
+ borderRadius: fromRadius,
676
+ opacity: "0.2",
677
+ transform: `translate3d(${translateX}px, ${translateY}px, 0) scale(${scale}) ${previous.transform}`.trim(),
678
+ transformOrigin: "center",
679
+ transition: "none",
680
+ willChange: "transform, opacity"
681
+ });
682
+ destination.addEventListener("transitionend", handleTransitionEnd);
683
+ openingCleanupRef.current = cleanup;
684
+ firstFrame = window.requestAnimationFrame(() => {
685
+ secondFrame = window.requestAnimationFrame(() => {
686
+ destination.style.transition = "transform 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease-out, border-radius 260ms cubic-bezier(0.22, 1, 0.36, 1)";
687
+ destination.style.transform = previous.transform;
688
+ destination.style.opacity = previous.opacity;
689
+ destination.style.borderRadius = previous.borderRadius;
690
+ finishTimer = window.setTimeout(cleanup, 320);
691
+ });
692
+ });
693
+ }, [
694
+ activeIndex,
695
+ activeItem,
696
+ commitOpenChange,
697
+ shouldReduceMotion,
698
+ stopZoomAnimation
699
+ ]);
700
+ const closeWithDetails = React$1.useCallback((details) => {
701
+ runOpenChange(false, details, sourceMapRef.current.get(activeIndex));
702
+ }, [activeIndex, runOpenChange]);
703
+ const requestOpen = React$1.useCallback((nextIndex, source, event) => {
704
+ let nextItems = items;
705
+ let nextSources = [];
706
+ if (!itemsProp) {
707
+ const scan = scanMedia();
708
+ nextItems = scan.foundItems;
709
+ nextSources = scan.sources;
710
+ sourceMapRef.current.clear();
711
+ nextSources.forEach((element, sourceIndex) => {
712
+ sourceMapRef.current.set(sourceIndex, {
713
+ element,
714
+ morph: true
715
+ });
716
+ });
717
+ flushSync(() => setAutoItems(nextItems));
718
+ }
719
+ if (nextItems.length === 0) return;
720
+ const normalized = normalizeIndex(nextIndex, nextItems.length, loop);
721
+ if (!isIndexControlled) flushSync(() => setUncontrolledIndex(normalized));
722
+ if (normalized !== activeIndex) onChangeImage?.(normalized, nextItems[normalized], {
723
+ reason: "trigger-press",
724
+ event,
725
+ previousIndex: activeIndex
726
+ });
727
+ if (source) sourceMapRef.current.set(normalized, source);
728
+ const details = getEventDetails("trigger-press", event, source?.element ?? nextSources[normalized]);
729
+ runOpenChange(true, details, source ?? sourceMapRef.current.get(normalized), normalized, nextItems[normalized]);
730
+ }, [
731
+ activeIndex,
732
+ isIndexControlled,
733
+ items,
734
+ itemsProp,
735
+ loop,
736
+ onChangeImage,
737
+ runOpenChange,
738
+ scanMedia
739
+ ]);
740
+ const handleOpenChange = React$1.useCallback((nextOpen, details) => {
741
+ if (nextOpen) runOpenChange(true, details, sourceMapRef.current.get(activeIndex));
742
+ else closeWithDetails(details);
743
+ }, [
744
+ activeIndex,
745
+ closeWithDetails,
746
+ runOpenChange
747
+ ]);
748
+ const handleOpenChangeComplete = React$1.useCallback((nextOpen) => {
749
+ if (!nextOpen && transformRef.current.dismissY === 0) {
750
+ stopZoomAnimation();
751
+ setTransform(initialTransform);
752
+ }
753
+ onOpenChangeComplete?.(nextOpen);
754
+ }, [onOpenChangeComplete, stopZoomAnimation]);
755
+ const registerSource = React$1.useCallback((sourceIndex, source) => {
756
+ if (source) sourceMapRef.current.set(sourceIndex, source);
757
+ else sourceMapRef.current.delete(sourceIndex);
758
+ }, []);
759
+ const registerMedia = React$1.useCallback((element) => {
760
+ mediaRef.current = element;
761
+ }, []);
762
+ const activeZoomable = activeItem?.type === "image" ? activeItem.zoomable !== false : Boolean(activeItem?.zoomable);
763
+ const actions = React$1.useMemo(() => {
764
+ return {
765
+ open: (nextIndex = activeIndex) => requestOpen(nextIndex),
766
+ close: () => closeWithDetails(getEventDetails("imperative-action", void 0, null)),
767
+ goTo: (nextIndex) => changeIndex(nextIndex, "programmatic"),
768
+ next: () => changeIndex(activeIndex + 1, "next-press"),
769
+ previous: () => changeIndex(activeIndex - 1, "previous-press"),
770
+ zoomIn: () => {
771
+ if (!activeZoomable) return;
772
+ const current = transformRef.current;
773
+ animateZoom({
774
+ scale: Math.min(maxZoom, current.scale < 2 ? 2 : current.scale + 1),
775
+ panX: current.panX,
776
+ panY: current.panY
777
+ });
778
+ },
779
+ zoomOut: () => {
780
+ const current = transformRef.current;
781
+ const scale = Math.max(1, current.scale - 1);
782
+ const panRatio = current.scale > 1 ? Math.max(0, (scale - 1) / (current.scale - 1)) : 0;
783
+ animateZoom({
784
+ scale,
785
+ panX: current.panX * panRatio,
786
+ panY: current.panY * panRatio
787
+ });
788
+ },
789
+ resetZoom: () => animateZoom({
790
+ scale: 1,
791
+ panX: 0,
792
+ panY: 0
793
+ })
794
+ };
795
+ }, [
796
+ activeIndex,
797
+ activeZoomable,
798
+ animateZoom,
799
+ changeIndex,
800
+ closeWithDetails,
801
+ maxZoom,
802
+ requestOpen
803
+ ]);
804
+ React$1.useImperativeHandle(actionsRef, () => actions, [actions]);
805
+ const context = React$1.useMemo(() => {
806
+ return {
807
+ activeIndex,
808
+ activeItem,
809
+ actions,
810
+ armPostGestureActivationRepair,
811
+ animateZoom,
812
+ changeIndex,
813
+ closeWithDetails,
814
+ controls,
815
+ direction,
816
+ getSource: (sourceIndex) => sourceMapRef.current.get(sourceIndex),
817
+ items,
818
+ loadedAssets,
819
+ loading,
820
+ loop,
821
+ markAssetLoaded,
822
+ maxZoom,
823
+ noCarousel,
824
+ noControls,
825
+ noCounter,
826
+ open,
827
+ overlay,
828
+ preload: noCarousel ? 0 : Math.max(0, Math.floor(preload)),
829
+ registerMedia,
830
+ registerSource,
831
+ requestOpen,
832
+ setTransform,
833
+ shouldReduceMotion,
834
+ stopZoomAnimation,
835
+ transform
836
+ };
837
+ }, [
838
+ activeIndex,
839
+ activeItem,
840
+ actions,
841
+ armPostGestureActivationRepair,
842
+ animateZoom,
843
+ changeIndex,
844
+ closeWithDetails,
845
+ controls,
846
+ direction,
847
+ items,
848
+ loadedAssets,
849
+ loading,
850
+ loop,
851
+ markAssetLoaded,
852
+ maxZoom,
853
+ noCarousel,
854
+ noControls,
855
+ noCounter,
856
+ open,
857
+ overlay,
858
+ preload,
859
+ registerMedia,
860
+ registerSource,
861
+ requestOpen,
862
+ shouldReduceMotion,
863
+ stopZoomAnimation,
864
+ transform
865
+ ]);
866
+ const handleGalleryClick = (event) => {
867
+ if (event.defaultPrevented || itemsProp) return;
868
+ const gallery = galleryRef.current;
869
+ if (!gallery) return;
870
+ const media = getStandaloneGalleryMedia(event.target, gallery);
871
+ if (!media) return;
872
+ const suppressedClick = suppressedGalleryClickRef.current;
873
+ suppressedGalleryClickRef.current = null;
874
+ if (event.detail !== 0 && suppressedClick?.media === media && performance.now() - suppressedClick.time < 700) return;
875
+ openGalleryMedia(media, event.nativeEvent);
876
+ };
877
+ const handleGalleryPointerDown = (event) => {
878
+ suppressedGalleryClickRef.current = null;
879
+ if (itemsProp || event.pointerType !== "touch" || !event.isPrimary || event.button !== 0) {
880
+ galleryTouchPressRef.current = null;
881
+ return;
882
+ }
883
+ const gallery = galleryRef.current;
884
+ if (!gallery) return;
885
+ const media = getStandaloneGalleryMedia(event.target, gallery);
886
+ galleryTouchPressRef.current = media ? {
887
+ pointerId: event.pointerId,
888
+ media,
889
+ startX: event.clientX,
890
+ startY: event.clientY
891
+ } : null;
892
+ };
893
+ const handleGalleryPointerMove = (event) => {
894
+ const press = galleryTouchPressRef.current;
895
+ if (!press || press.pointerId !== event.pointerId || Math.hypot(event.clientX - press.startX, event.clientY - press.startY) <= 8) return;
896
+ galleryTouchPressRef.current = null;
897
+ };
898
+ const handleGalleryPointerUp = (event) => {
899
+ const press = galleryTouchPressRef.current;
900
+ galleryTouchPressRef.current = null;
901
+ if (!press || press.pointerId !== event.pointerId || Math.hypot(event.clientX - press.startX, event.clientY - press.startY) > 8) return;
902
+ const gallery = galleryRef.current;
903
+ if (!gallery) return;
904
+ const media = getStandaloneGalleryMedia(event.target, gallery);
905
+ if (media !== press.media) return;
906
+ suppressedGalleryClickRef.current = {
907
+ media,
908
+ time: performance.now()
909
+ };
910
+ openGalleryMedia(media, event.nativeEvent);
911
+ };
912
+ function openGalleryMedia(media, event) {
913
+ const gallery = galleryRef.current;
914
+ if (!gallery) return;
915
+ const sourceIndex = Array.from(gallery.querySelectorAll("img,video")).filter((item) => isStandaloneMedia(item, gallery)).indexOf(media);
916
+ if (sourceIndex < 0) return;
917
+ requestOpen(sourceIndex, {
918
+ element: media,
919
+ morph: true
920
+ }, event);
921
+ }
922
+ const handleGalleryKeyDown = (event) => {
923
+ if (itemsProp || event.key !== "Enter" && event.key !== " ") return;
924
+ const gallery = galleryRef.current;
925
+ const target = event.target;
926
+ if (!gallery || !(target instanceof HTMLImageElement || target instanceof HTMLVideoElement)) return;
927
+ if (!isStandaloneMedia(target, gallery)) return;
928
+ event.preventDefault();
929
+ openGalleryMedia(target, event.nativeEvent);
930
+ };
931
+ const handleGalleryMediaLoad = () => {
932
+ if (itemsProp) return;
933
+ const { foundItems } = scanMedia();
934
+ setAutoItems(foundItems);
935
+ };
936
+ const customContent = hasCustomContent(children);
937
+ return /* @__PURE__ */ jsxs(LightboxContext.Provider, {
938
+ value: context,
939
+ children: [/* @__PURE__ */ jsx("style", {
940
+ "data-slot": "lightbox-styles",
941
+ children: LIGHTBOX_STYLES
942
+ }), /* @__PURE__ */ jsxs(Dialog.Root, {
943
+ modal: true,
944
+ open,
945
+ onOpenChange: handleOpenChange,
946
+ onOpenChangeComplete: handleOpenChangeComplete,
947
+ ...rootProps,
948
+ children: [itemsProp ? children : /* @__PURE__ */ jsx("div", {
949
+ ref: handleGalleryRef,
950
+ className,
951
+ "data-slot": "lightbox-gallery",
952
+ onClick: handleGalleryClick,
953
+ onKeyDown: handleGalleryKeyDown,
954
+ onLoadCapture: handleGalleryMediaLoad,
955
+ onLoadedDataCapture: handleGalleryMediaLoad,
956
+ onPointerCancel: () => {
957
+ galleryTouchPressRef.current = null;
958
+ },
959
+ onPointerDown: handleGalleryPointerDown,
960
+ onPointerMove: handleGalleryPointerMove,
961
+ onPointerUp: handleGalleryPointerUp,
962
+ children
963
+ }), !customContent && /* @__PURE__ */ jsx(LightboxContent, {})]
964
+ })]
965
+ });
966
+ }
967
+ function LightboxTrigger({ index = 0, morph, onClick, ref, ...props }) {
968
+ const { registerSource, requestOpen } = useLightbox();
969
+ const sourceRef = React$1.useRef(null);
970
+ const mergedRef = React$1.useCallback((element) => {
971
+ sourceRef.current = element;
972
+ if (element) {
973
+ const inferredMorph = morph ?? element.matches("img,video,canvas,picture,[data-lightbox-morph]");
974
+ registerSource(index, {
975
+ element,
976
+ morph: inferredMorph
977
+ });
978
+ } else registerSource(index, null);
979
+ if (typeof ref === "function") return ref(element);
980
+ if (ref) ref.current = element;
981
+ }, [
982
+ index,
983
+ morph,
984
+ ref,
985
+ registerSource
986
+ ]);
987
+ return /* @__PURE__ */ jsx(Dialog.Trigger, {
988
+ ref: mergedRef,
989
+ "data-slot": "lightbox-trigger",
990
+ onClick: (event) => {
991
+ onClick?.(event);
992
+ if (event.defaultPrevented) return;
993
+ const element = sourceRef.current;
994
+ if (!element) return;
995
+ const inferredMorph = morph ?? element.matches("img,video,canvas,picture,[data-lightbox-morph]");
996
+ registerSource(index, {
997
+ element,
998
+ morph: inferredMorph
999
+ });
1000
+ event.preventBaseUIHandler();
1001
+ requestOpen(index, {
1002
+ element,
1003
+ morph: inferredMorph
1004
+ }, event.nativeEvent);
1005
+ },
1006
+ ...props
1007
+ });
1008
+ }
1009
+ function LightboxBackdrop({ className, style, ...props }) {
1010
+ const { overlay, transform } = useLightbox();
1011
+ const backdropStyle = mergeLightboxBackdropStyle({ "--lightbox-backdrop-opacity": clamp(1 - Math.abs(transform.dismissY) / Math.max(typeof window === "undefined" ? 1 : window.innerHeight || 1, 1), 0, 1) }, style);
1012
+ return /* @__PURE__ */ jsx(Dialog.Backdrop, {
1013
+ "data-slot": "lightbox-backdrop",
1014
+ "data-overlay": overlay,
1015
+ className: cn("fixed inset-0 z-50 min-h-dvh opacity-[var(--lightbox-backdrop-opacity)] transition-[opacity,backdrop-filter] duration-200", overlay === "blur" && "bg-black/80 backdrop-blur-md", overlay === "brightness" && "bg-black/90", "data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:pointer-events-none motion-reduce:transition-opacity", className),
1016
+ style: backdropStyle,
1017
+ ...props
1018
+ });
1019
+ }
1020
+ function LightboxViewport({ className, ...props }) {
1021
+ return /* @__PURE__ */ jsx(Dialog.Viewport, {
1022
+ "data-slot": "lightbox-viewport",
1023
+ className: cn("fixed inset-0 z-50 flex min-h-dvh items-stretch overflow-hidden overscroll-contain data-ending-style:pointer-events-none", className),
1024
+ ...props
1025
+ });
1026
+ }
1027
+ function LightboxContent({ className, children, initialFocus, onKeyDown, style, ...props }) {
1028
+ const { activeIndex, activeItem, actions, changeIndex, direction, items, noCarousel, noControls, noCounter, setTransform, shouldReduceMotion, transform } = useLightbox();
1029
+ const popupRef = React$1.useRef(null);
1030
+ const handleKeyDown = (event) => {
1031
+ onKeyDown?.(event);
1032
+ if (event.defaultPrevented) return;
1033
+ if (event.target.matches("input,textarea,select,[contenteditable=true]")) return;
1034
+ const rtlMultiplier = direction === "rtl" ? -1 : 1;
1035
+ if (!noCarousel && (event.key === "ArrowLeft" || event.key === "ArrowRight") && event.shiftKey) {
1036
+ event.preventDefault();
1037
+ const physical = event.key === "ArrowRight" ? 1 : -1;
1038
+ changeIndex(activeIndex + physical * rtlMultiplier, "keyboard", event.nativeEvent);
1039
+ return;
1040
+ }
1041
+ if (transform.scale > 1 && event.key.startsWith("Arrow")) {
1042
+ event.preventDefault();
1043
+ const amount = 40;
1044
+ const delta = {
1045
+ ArrowLeft: [amount, 0],
1046
+ ArrowRight: [-40, 0],
1047
+ ArrowUp: [0, amount],
1048
+ ArrowDown: [0, -40]
1049
+ }[event.key];
1050
+ if (delta) panBy(delta[0], delta[1]);
1051
+ return;
1052
+ }
1053
+ if (!noCarousel && transform.scale === 1 && (event.key === "ArrowLeft" || event.key === "ArrowRight")) {
1054
+ event.preventDefault();
1055
+ const physical = event.key === "ArrowRight" ? 1 : -1;
1056
+ changeIndex(activeIndex + physical * rtlMultiplier, "keyboard", event.nativeEvent);
1057
+ return;
1058
+ }
1059
+ if (event.key === "+" || event.key === "=") {
1060
+ event.preventDefault();
1061
+ actions.zoomIn();
1062
+ } else if (event.key === "-") {
1063
+ event.preventDefault();
1064
+ actions.zoomOut();
1065
+ } else if (event.key === "0") {
1066
+ event.preventDefault();
1067
+ actions.resetZoom();
1068
+ }
1069
+ };
1070
+ function panBy(deltaX, deltaY) {
1071
+ const media = popupRef.current?.querySelector("[data-slot=\"lightbox-zoom\"][data-active=\"true\"]");
1072
+ if (!media) return;
1073
+ const rect = media.getBoundingClientRect();
1074
+ const baseWidth = rect.width / transform.scale;
1075
+ const baseHeight = rect.height / transform.scale;
1076
+ const maxX = Math.max(0, (baseWidth * transform.scale - window.innerWidth) / 2);
1077
+ const maxY = Math.max(0, (baseHeight * transform.scale - window.innerHeight) / 2);
1078
+ setTransform((current) => {
1079
+ return {
1080
+ ...current,
1081
+ panX: clamp(current.panX + deltaX, -maxX, maxX),
1082
+ panY: clamp(current.panY + deltaY, -maxY, maxY)
1083
+ };
1084
+ });
1085
+ }
1086
+ return /* @__PURE__ */ jsxs(Dialog.Portal, {
1087
+ "data-slot": "lightbox-portal",
1088
+ children: [/* @__PURE__ */ jsx(LightboxBackdrop, { forceRender: true }), /* @__PURE__ */ jsx(LightboxViewport, {
1089
+ dir: direction,
1090
+ "data-lightbox-motion": shouldReduceMotion ? "reduced" : "full",
1091
+ children: /* @__PURE__ */ jsxs(Dialog.Popup, {
1092
+ ref: popupRef,
1093
+ "data-slot": "lightbox-content",
1094
+ "data-dragging": transform.dragging ? "true" : "false",
1095
+ "aria-describedby": activeItem?.caption ? `lightbox-caption-${activeIndex}` : void 0,
1096
+ initialFocus: initialFocus ?? (() => popupRef.current?.querySelector("[data-slot=\"lightbox-close\"]") ?? popupRef.current),
1097
+ className: cn("relative flex size-full min-h-0 min-w-0 flex-col overflow-hidden bg-transparent text-white outline-none", "transition-[opacity,scale] duration-200 ease-out data-starting-style:scale-[0.98] data-starting-style:opacity-0 data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:pointer-events-none", "motion-reduce:transition-opacity motion-reduce:transform-none", className),
1098
+ style: mergeLightboxContentStyle({ "--lightbox-header-inset": "max(5rem, calc(env(safe-area-inset-top) + 4rem))" }, style),
1099
+ onKeyDown: handleKeyDown,
1100
+ ...props,
1101
+ children: [
1102
+ /* @__PURE__ */ jsx(Dialog.Title, {
1103
+ className: "sr-only",
1104
+ children: "Media viewer"
1105
+ }),
1106
+ children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1107
+ (!noCounter || !noControls) && /* @__PURE__ */ jsxs("div", {
1108
+ className: "pointer-events-none absolute inset-x-0 top-0 z-30 flex items-start justify-between gap-4 p-3 pt-[max(0.75rem,env(safe-area-inset-top))] sm:p-4",
1109
+ children: [!noCounter && /* @__PURE__ */ jsx(LightboxCounter, {}), !noControls && /* @__PURE__ */ jsx(LightboxToolbar, { className: cn(noCounter && "ms-auto") })]
1110
+ }),
1111
+ /* @__PURE__ */ jsx(LightboxSlides, {}),
1112
+ !noCarousel && items.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LightboxPrevious, {}), /* @__PURE__ */ jsx(LightboxNext, {})] }),
1113
+ (Boolean(activeItem?.caption) || !noCarousel && items.length > 1) && /* @__PURE__ */ jsxs("div", {
1114
+ className: "pointer-events-none absolute inset-x-0 bottom-0 z-30 flex flex-col items-center gap-3 px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-4",
1115
+ children: [/* @__PURE__ */ jsx(LightboxCaptions, {}), !noCarousel && items.length > 1 && /* @__PURE__ */ jsx(LightboxThumbnails, {})]
1116
+ })
1117
+ ] }),
1118
+ /* @__PURE__ */ jsx("span", {
1119
+ className: "sr-only",
1120
+ role: "status",
1121
+ "aria-live": "polite",
1122
+ "aria-atomic": "true",
1123
+ children: items.length > 0 ? `${getItemLabel(activeItem, activeIndex)}, ${activeIndex + 1} of ${items.length}` : "No media"
1124
+ })
1125
+ ]
1126
+ })
1127
+ })]
1128
+ });
1129
+ }
1130
+ LightboxContent.__lightboxContent = true;
1131
+ function LightboxZoom({ active = true, className, render, style, ref, ...props }) {
1132
+ const { activeItem, registerMedia, transform } = useLightbox();
1133
+ const zoomable = activeItem?.type === "image" ? activeItem.zoomable !== false : Boolean(activeItem?.zoomable);
1134
+ const mediaRegistrationRef = React$1.useCallback((element) => {
1135
+ if (active) registerMedia(element);
1136
+ }, [active, registerMedia]);
1137
+ const defaultProps = {
1138
+ className: cn("flex max-h-full max-w-full origin-center items-center justify-center will-change-transform", className),
1139
+ "data-active": active ? "true" : "false",
1140
+ "data-dragging": transform.dragging ? "true" : "false",
1141
+ "data-slot": "lightbox-zoom",
1142
+ "data-zoomable": zoomable ? "true" : "false",
1143
+ "data-zoomed": transform.scale > 1 ? "true" : "false",
1144
+ style: {
1145
+ transform: active ? `translate3d(${transform.panX}px, ${transform.panY}px, 0) scale(${transform.scale})` : void 0,
1146
+ ...style
1147
+ }
1148
+ };
1149
+ return useRender({
1150
+ defaultTagName: "div",
1151
+ ref: ref ? [ref, mediaRegistrationRef] : mediaRegistrationRef,
1152
+ props: mergeProps(defaultProps, props),
1153
+ render
1154
+ });
1155
+ }
1156
+ function LightboxSlide({ index, active = false, className, style, ...props }) {
1157
+ const { activeItem, items, loadedAssets, loading, markAssetLoaded, noCarousel, noControls, noCounter } = useLightbox();
1158
+ const item = items[index];
1159
+ const [failedLoadKey, setFailedLoadKey] = React$1.useState(null);
1160
+ if (!item) return null;
1161
+ const loadKey = getMediaLoadKey(item);
1162
+ const mediaFailed = failedLoadKey === loadKey;
1163
+ const mediaLoaded = loadedAssets.has(loadKey);
1164
+ const shouldMountMedia = loading === "eager" || active || mediaLoaded;
1165
+ const hasTopChrome = !noCounter || !noControls;
1166
+ const hasBottomChrome = Boolean(activeItem?.caption);
1167
+ const captionTrack = item.type === "video" ? item.tracks?.find((track) => track.kind === "captions") : void 0;
1168
+ const media = item.type === "image" ? /* @__PURE__ */ jsx("img", {
1169
+ src: item.src,
1170
+ srcSet: item.srcSet,
1171
+ sizes: item.sizes,
1172
+ alt: item.alt,
1173
+ width: item.width,
1174
+ height: item.height,
1175
+ draggable: false,
1176
+ decoding: "async",
1177
+ loading: active ? "eager" : "lazy",
1178
+ onLoad: () => {
1179
+ setFailedLoadKey(null);
1180
+ markAssetLoaded(loadKey);
1181
+ },
1182
+ onError: () => setFailedLoadKey(loadKey),
1183
+ className: cn("block h-auto w-[min(82vw,64rem)] max-w-[calc(100vw-2rem)] select-none rounded-lg object-contain", "max-h-[calc(100dvh-11rem)] sm:max-h-[calc(100dvh-12rem)]", "transition-opacity duration-200", !mediaLoaded && "opacity-0", item.className)
1184
+ }) : /* @__PURE__ */ jsxs("video", {
1185
+ ...item.videoProps,
1186
+ src: item.src,
1187
+ poster: item.poster,
1188
+ width: item.width,
1189
+ height: item.height,
1190
+ "aria-label": item.label,
1191
+ controls: item.videoProps?.controls ?? true,
1192
+ muted: item.videoProps?.muted ?? false,
1193
+ onError: (event) => {
1194
+ item.videoProps?.onError?.(event);
1195
+ setFailedLoadKey(loadKey);
1196
+ },
1197
+ onCanPlay: (event) => {
1198
+ item.videoProps?.onCanPlay?.(event);
1199
+ setFailedLoadKey(null);
1200
+ markAssetLoaded(loadKey);
1201
+ },
1202
+ onLoadedData: (event) => {
1203
+ item.videoProps?.onLoadedData?.(event);
1204
+ setFailedLoadKey(null);
1205
+ markAssetLoaded(loadKey);
1206
+ },
1207
+ onLoadedMetadata: (event) => {
1208
+ item.videoProps?.onLoadedMetadata?.(event);
1209
+ markAssetLoaded(loadKey);
1210
+ },
1211
+ playsInline: item.videoProps?.playsInline ?? true,
1212
+ preload: item.videoProps?.preload ?? (active ? "auto" : "metadata"),
1213
+ className: cn("block h-auto w-[min(82vw,64rem)] max-w-[calc(100vw-2rem)] rounded-lg bg-black object-contain", "max-h-[calc(100dvh-11rem)] sm:max-h-[calc(100dvh-12rem)]", "transition-opacity duration-200", !mediaLoaded && "opacity-0", item.className),
1214
+ children: [
1215
+ item.sources?.map((source) => /* @__PURE__ */ jsx("source", { ...source }, `${source.src}-${source.type}`)),
1216
+ /* @__PURE__ */ jsx("track", {
1217
+ ...captionTrack,
1218
+ kind: "captions",
1219
+ label: captionTrack?.label ?? "Captions",
1220
+ src: captionTrack?.src ?? EMPTY_CAPTIONS_TRACK,
1221
+ srcLang: captionTrack?.srcLang ?? "en"
1222
+ }),
1223
+ item.tracks?.filter((track) => track !== captionTrack).map((track) => /* @__PURE__ */ jsx("track", { ...track }, `${track.src}-${track.kind}-${track.srcLang}`))
1224
+ ]
1225
+ });
1226
+ const content = /* @__PURE__ */ jsxs("div", {
1227
+ className: "relative flex min-h-10 min-w-10 max-h-full max-w-full items-center justify-center",
1228
+ children: [mediaFailed ? /* @__PURE__ */ jsx("span", {
1229
+ role: "status",
1230
+ className: "px-4 py-8 text-sm text-white",
1231
+ children: item.type === "image" ? "Unable to load image" : "Unable to load video"
1232
+ }) : !mediaLoaded && /* @__PURE__ */ jsx("span", {
1233
+ className: "absolute inset-0 flex items-center justify-center",
1234
+ children: /* @__PURE__ */ jsx(Spinner, { className: "size-10 text-white" })
1235
+ }), shouldMountMedia && /* @__PURE__ */ jsx("div", {
1236
+ className: cn("flex max-h-full max-w-full items-center justify-center", mediaFailed && "hidden"),
1237
+ children: media
1238
+ })]
1239
+ });
1240
+ return /* @__PURE__ */ jsx("div", {
1241
+ "data-slot": "lightbox-slide",
1242
+ "data-active": active ? "true" : "false",
1243
+ "data-type": item.type,
1244
+ role: "group",
1245
+ "aria-roledescription": "slide",
1246
+ "aria-label": `${index + 1} of ${items.length}: ${getItemLabel(item, index)}`,
1247
+ "aria-hidden": active ? void 0 : true,
1248
+ inert: active ? void 0 : true,
1249
+ className: cn("pointer-events-none absolute inset-0 flex items-center justify-center px-4", noCarousel ? hasTopChrome || hasBottomChrome ? "pt-[var(--lightbox-header-inset,5rem)] pb-20" : "py-4" : "pt-[var(--lightbox-header-inset,5rem)] pb-36", active && "pointer-events-auto", className),
1250
+ style,
1251
+ ...props,
1252
+ children: /* @__PURE__ */ jsx(LightboxZoom, {
1253
+ active,
1254
+ children: content
1255
+ })
1256
+ });
1257
+ }
1258
+ function relativeSlideOffset(index, activeIndex, length, loop, preferredOffset = 0) {
1259
+ let offset = index - activeIndex;
1260
+ if (!loop || length < 2) return offset;
1261
+ if (offset > length / 2) offset -= length;
1262
+ if (offset < -length / 2) offset += length;
1263
+ if (length === 2 && Math.abs(offset) === 1 && preferredOffset !== 0) return preferredOffset;
1264
+ return offset;
1265
+ }
1266
+ function isIndexMounted(index, activeIndex, length, preload, loop) {
1267
+ return Math.abs(relativeSlideOffset(index, activeIndex, length, loop)) <= preload;
1268
+ }
1269
+ function LightboxSlides({ className, onClick, onKeyDown, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onWheel, onDoubleClick, ...props }) {
1270
+ const { activeIndex, activeItem, armPostGestureActivationRepair, animateZoom, changeIndex, closeWithDetails, direction, items, loading, loop, maxZoom, noCarousel, preload, setTransform, shouldReduceMotion, stopZoomAnimation, transform } = useLightbox();
1271
+ const viewportRef = React$1.useRef(null);
1272
+ const pointersRef = React$1.useRef(/* @__PURE__ */ new Map());
1273
+ const sessionRef = React$1.useRef(null);
1274
+ const animationRef = React$1.useRef(null);
1275
+ const wheelRef = React$1.useRef({
1276
+ x: 0,
1277
+ y: 0,
1278
+ timer: 0
1279
+ });
1280
+ const lastTapRef = React$1.useRef({
1281
+ time: 0,
1282
+ x: 0,
1283
+ y: 0
1284
+ });
1285
+ const suppressClickRef = React$1.useRef(false);
1286
+ const suppressClickTimerRef = React$1.useRef(0);
1287
+ const transformRef = React$1.useRef(transform);
1288
+ React$1.useLayoutEffect(() => {
1289
+ suppressClickRef.current = true;
1290
+ window.clearTimeout(suppressClickTimerRef.current);
1291
+ suppressClickTimerRef.current = window.setTimeout(() => {
1292
+ suppressClickRef.current = false;
1293
+ }, 0);
1294
+ }, []);
1295
+ React$1.useLayoutEffect(() => {
1296
+ transformRef.current = transform;
1297
+ }, [transform]);
1298
+ const zoomable = activeItem?.type === "image" ? activeItem.zoomable !== false : Boolean(activeItem?.zoomable);
1299
+ const clickZoomable = activeItem?.type === "image" && zoomable;
1300
+ React$1.useEffect(() => {
1301
+ const animation = animationRef;
1302
+ const wheel = wheelRef;
1303
+ const pointers = pointersRef;
1304
+ const session = sessionRef;
1305
+ return () => {
1306
+ animation.current?.stop();
1307
+ window.clearTimeout(wheel.current.timer);
1308
+ window.clearTimeout(suppressClickTimerRef.current);
1309
+ wheel.current = {
1310
+ x: 0,
1311
+ y: 0,
1312
+ timer: 0
1313
+ };
1314
+ pointers.current.clear();
1315
+ session.current = null;
1316
+ };
1317
+ }, []);
1318
+ React$1.useEffect(() => {
1319
+ if (loading === "lazy") return;
1320
+ for (let distance = 1; distance <= preload; distance += 1) for (const candidate of [activeIndex - distance, activeIndex + distance]) {
1321
+ const normalized = normalizeIndex(candidate, items.length, loop);
1322
+ const item = items[normalized];
1323
+ if (item?.type !== "image") continue;
1324
+ const image = new Image();
1325
+ image.srcset = item.srcSet ?? "";
1326
+ image.sizes = item.sizes ?? "";
1327
+ image.src = item.src;
1328
+ image.decode?.().catch(() => void 0);
1329
+ }
1330
+ }, [
1331
+ activeIndex,
1332
+ items,
1333
+ loading,
1334
+ loop,
1335
+ preload
1336
+ ]);
1337
+ const getPanBounds = React$1.useCallback((scale) => {
1338
+ const media = viewportRef.current?.querySelector("[data-slot=\"lightbox-zoom\"][data-active=\"true\"]");
1339
+ const viewport = viewportRef.current;
1340
+ if (!media || !viewport) return {
1341
+ x: 0,
1342
+ y: 0
1343
+ };
1344
+ const rect = media.getBoundingClientRect();
1345
+ const currentScale = transformRef.current.scale || 1;
1346
+ const width = rect.width / currentScale;
1347
+ const height = rect.height / currentScale;
1348
+ return {
1349
+ x: Math.max(0, (width * scale - viewport.clientWidth) / 2),
1350
+ y: Math.max(0, (height * scale - viewport.clientHeight) / 2)
1351
+ };
1352
+ }, []);
1353
+ const stopAnimation = React$1.useCallback(() => {
1354
+ animationRef.current?.stop();
1355
+ animationRef.current = null;
1356
+ }, []);
1357
+ const animateTransformValue = React$1.useCallback((key, to, onComplete) => {
1358
+ stopAnimation();
1359
+ if (shouldReduceMotion) {
1360
+ setTransform((current) => {
1361
+ return {
1362
+ ...current,
1363
+ [key]: to
1364
+ };
1365
+ });
1366
+ onComplete?.();
1367
+ return;
1368
+ }
1369
+ const from = transformRef.current[key];
1370
+ animationRef.current = animate(from, to, {
1371
+ duration: .24,
1372
+ ease: "easeOut",
1373
+ onUpdate: (value) => setTransform((current) => {
1374
+ return {
1375
+ ...current,
1376
+ [key]: value
1377
+ };
1378
+ }),
1379
+ onComplete: () => {
1380
+ animationRef.current = null;
1381
+ onComplete?.();
1382
+ }
1383
+ });
1384
+ }, [
1385
+ setTransform,
1386
+ shouldReduceMotion,
1387
+ stopAnimation
1388
+ ]);
1389
+ const getZoomTarget = React$1.useCallback((nextScale, clientX, clientY) => {
1390
+ const viewport = viewportRef.current;
1391
+ if (!viewport) return null;
1392
+ const current = transformRef.current;
1393
+ const scale = clamp(nextScale, 1, maxZoom);
1394
+ if (scale === 1) return {
1395
+ ...current,
1396
+ scale: 1,
1397
+ panX: 0,
1398
+ panY: 0
1399
+ };
1400
+ const rect = viewport.getBoundingClientRect();
1401
+ const pointX = clientX - (rect.left + rect.width / 2);
1402
+ const pointY = clientY - (rect.top + rect.height / 2);
1403
+ const ratio = scale / current.scale;
1404
+ const bounds = getPanBounds(scale);
1405
+ return {
1406
+ ...current,
1407
+ scale,
1408
+ panX: clamp(pointX - (pointX - current.panX) * ratio, -bounds.x, bounds.x),
1409
+ panY: clamp(pointY - (pointY - current.panY) * ratio, -bounds.y, bounds.y),
1410
+ swipeX: 0,
1411
+ dismissY: 0
1412
+ };
1413
+ }, [getPanBounds, maxZoom]);
1414
+ const zoomAt = React$1.useCallback((nextScale, clientX, clientY) => {
1415
+ const target = getZoomTarget(nextScale, clientX, clientY);
1416
+ if (target) {
1417
+ transformRef.current = target;
1418
+ setTransform(target);
1419
+ }
1420
+ }, [getZoomTarget, setTransform]);
1421
+ const animateZoomAt = React$1.useCallback((nextScale, clientX, clientY) => {
1422
+ const target = getZoomTarget(nextScale, clientX, clientY);
1423
+ if (target) animateZoom(target);
1424
+ }, [animateZoom, getZoomTarget]);
1425
+ const toggleZoomAt = React$1.useCallback((clientX, clientY) => {
1426
+ if (activeItem?.zoomable === false || activeItem?.type === "video" && !activeItem.zoomable) return;
1427
+ const nextScale = transformRef.current.scale > 1 ? 1 : Math.min(2, maxZoom);
1428
+ animateZoomAt(nextScale, clientX, clientY);
1429
+ }, [
1430
+ activeItem,
1431
+ animateZoomAt,
1432
+ maxZoom
1433
+ ]);
1434
+ const animateNavigation = React$1.useCallback((logicalDelta, reason, event) => {
1435
+ if (items.length < 2) return;
1436
+ const nextIndex = normalizeIndex(activeIndex + logicalDelta, items.length, loop);
1437
+ if (nextIndex === activeIndex) {
1438
+ animateTransformValue("swipeX", 0);
1439
+ return;
1440
+ }
1441
+ const viewportWidth = viewportRef.current?.clientWidth ?? window.innerWidth;
1442
+ const rtl = direction === "rtl" ? -1 : 1;
1443
+ const target = -logicalDelta * rtl * viewportWidth;
1444
+ animateTransformValue("swipeX", target, () => {
1445
+ changeIndex(nextIndex, reason, event);
1446
+ setTransform(initialTransform);
1447
+ });
1448
+ }, [
1449
+ activeIndex,
1450
+ animateTransformValue,
1451
+ changeIndex,
1452
+ direction,
1453
+ items.length,
1454
+ loop,
1455
+ setTransform
1456
+ ]);
1457
+ const closeFromGesture = React$1.useCallback((event) => {
1458
+ if (event) armPostGestureActivationRepair(event);
1459
+ closeWithDetails(getEventDetails("imperative-action", event, null));
1460
+ }, [armPostGestureActivationRepair, closeWithDetails]);
1461
+ const releaseGesture = React$1.useCallback((event) => {
1462
+ const session = sessionRef.current;
1463
+ if (!session) return;
1464
+ const viewport = viewportRef.current;
1465
+ const width = viewport?.clientWidth ?? window.innerWidth;
1466
+ const height = viewport?.clientHeight ?? window.innerHeight;
1467
+ const current = transformRef.current;
1468
+ setTransform((value) => {
1469
+ return {
1470
+ ...value,
1471
+ dragging: false
1472
+ };
1473
+ });
1474
+ if (session.mode === "swipe") {
1475
+ if (noCarousel) {
1476
+ animateTransformValue("swipeX", 0);
1477
+ sessionRef.current = null;
1478
+ return;
1479
+ }
1480
+ if (Math.abs(current.swipeX) >= width * .25 || Math.abs(session.velocityX) >= .5) {
1481
+ const rtl = direction === "rtl" ? -1 : 1;
1482
+ const logicalDelta = current.swipeX < 0 ? rtl : -rtl;
1483
+ animateNavigation(logicalDelta, "swipe", event);
1484
+ } else animateTransformValue("swipeX", 0);
1485
+ } else if (session.mode === "dismiss") {
1486
+ if (Math.abs(current.dismissY) >= height * .2 || Math.abs(session.velocityY) >= .5) {
1487
+ const target = Math.sign(current.dismissY || session.velocityY) * height;
1488
+ animateTransformValue("dismissY", target);
1489
+ closeFromGesture(event);
1490
+ } else animateTransformValue("dismissY", 0);
1491
+ } else if (session.mode === "pan" || session.mode === "pinch") {
1492
+ const scale = clamp(current.scale, 1, maxZoom);
1493
+ const bounds = getPanBounds(scale);
1494
+ setTransform((value) => {
1495
+ return {
1496
+ ...value,
1497
+ scale,
1498
+ panX: clamp(value.panX, -bounds.x, bounds.x),
1499
+ panY: clamp(value.panY, -bounds.y, bounds.y),
1500
+ dragging: false
1501
+ };
1502
+ });
1503
+ } else {
1504
+ animateTransformValue("swipeX", 0);
1505
+ animateTransformValue("dismissY", 0);
1506
+ }
1507
+ sessionRef.current = null;
1508
+ }, [
1509
+ animateNavigation,
1510
+ animateTransformValue,
1511
+ closeFromGesture,
1512
+ direction,
1513
+ getPanBounds,
1514
+ maxZoom,
1515
+ noCarousel,
1516
+ setTransform
1517
+ ]);
1518
+ const handlePointerDown = (event) => {
1519
+ onPointerDown?.(event);
1520
+ if (event.defaultPrevented) return;
1521
+ const interactiveTarget = event.target.closest(interactiveSelector);
1522
+ if (interactiveTarget && interactiveTarget !== event.currentTarget) return;
1523
+ stopAnimation();
1524
+ stopZoomAnimation();
1525
+ event.currentTarget.setPointerCapture(event.pointerId);
1526
+ pointersRef.current.set(event.pointerId, {
1527
+ x: event.clientX,
1528
+ y: event.clientY
1529
+ });
1530
+ const points = Array.from(pointersRef.current.values());
1531
+ const first = points[0] ?? {
1532
+ x: event.clientX,
1533
+ y: event.clientY
1534
+ };
1535
+ const second = points[1] ?? first;
1536
+ const isPinch = points.length > 1;
1537
+ const pinchDistance = isPinch ? Math.hypot(second.x - first.x, second.y - first.y) : 0;
1538
+ const pinchCenterX = isPinch ? (first.x + second.x) / 2 : event.clientX;
1539
+ const pinchCenterY = isPinch ? (first.y + second.y) / 2 : event.clientY;
1540
+ const current = transformRef.current;
1541
+ const now = performance.now();
1542
+ sessionRef.current = {
1543
+ mode: isPinch ? "pinch" : "pending",
1544
+ startX: event.clientX,
1545
+ startY: event.clientY,
1546
+ startPanX: current.panX,
1547
+ startPanY: current.panY,
1548
+ startSwipeX: current.swipeX,
1549
+ startDismissY: current.dismissY,
1550
+ startScale: current.scale,
1551
+ pinchDistance,
1552
+ pinchCenterX,
1553
+ pinchCenterY,
1554
+ lastX: event.clientX,
1555
+ lastY: event.clientY,
1556
+ lastTime: now,
1557
+ velocityX: 0,
1558
+ velocityY: 0
1559
+ };
1560
+ setTransform((value) => {
1561
+ return {
1562
+ ...value,
1563
+ dragging: true
1564
+ };
1565
+ });
1566
+ };
1567
+ const handlePointerMove = (event) => {
1568
+ onPointerMove?.(event);
1569
+ if (event.defaultPrevented || !pointersRef.current.has(event.pointerId)) return;
1570
+ pointersRef.current.set(event.pointerId, {
1571
+ x: event.clientX,
1572
+ y: event.clientY
1573
+ });
1574
+ const session = sessionRef.current;
1575
+ if (!session) return;
1576
+ const points = Array.from(pointersRef.current.values());
1577
+ if (points.length >= 2 && zoomable) {
1578
+ const [first, second] = points;
1579
+ const distance = Math.hypot(second.x - first.x, second.y - first.y);
1580
+ const centerX = (first.x + second.x) / 2;
1581
+ const centerY = (first.y + second.y) / 2;
1582
+ if (!session.pinchDistance) {
1583
+ session.pinchDistance = distance;
1584
+ session.pinchCenterX = centerX;
1585
+ session.pinchCenterY = centerY;
1586
+ session.startScale = transformRef.current.scale;
1587
+ }
1588
+ session.mode = "pinch";
1589
+ const rawScale = session.startScale * (distance / Math.max(session.pinchDistance, 1));
1590
+ const elasticScale = rawScale < 1 ? 1 - (1 - rawScale) * .25 : rawScale > maxZoom ? maxZoom + (rawScale - maxZoom) * .25 : rawScale;
1591
+ zoomAt(elasticScale, centerX, centerY);
1592
+ return;
1593
+ }
1594
+ const dx = event.clientX - session.startX;
1595
+ const dy = event.clientY - session.startY;
1596
+ const now = performance.now();
1597
+ const elapsed = Math.max(1, now - session.lastTime);
1598
+ session.velocityX = (event.clientX - session.lastX) / elapsed;
1599
+ session.velocityY = (event.clientY - session.lastY) / elapsed;
1600
+ session.lastX = event.clientX;
1601
+ session.lastY = event.clientY;
1602
+ session.lastTime = now;
1603
+ if (transformRef.current.scale > 1) session.mode = "pan";
1604
+ else if (session.mode === "pending" && Math.hypot(dx, dy) > 8) session.mode = Math.abs(dx) > Math.abs(dy) ? noCarousel ? "pending" : "swipe" : "dismiss";
1605
+ if (session.mode === "pan") {
1606
+ const bounds = getPanBounds(transformRef.current.scale);
1607
+ const rawX = session.startPanX + dx;
1608
+ const rawY = session.startPanY + dy;
1609
+ const resist = (value, maximum) => Math.abs(value) <= maximum ? value : Math.sign(value) * (maximum + (Math.abs(value) - maximum) * .35);
1610
+ setTransform((value) => {
1611
+ return {
1612
+ ...value,
1613
+ panX: resist(rawX, bounds.x),
1614
+ panY: resist(rawY, bounds.y)
1615
+ };
1616
+ });
1617
+ } else if (session.mode === "swipe") setTransform((value) => {
1618
+ return {
1619
+ ...value,
1620
+ swipeX: session.startSwipeX + dx
1621
+ };
1622
+ });
1623
+ else if (session.mode === "dismiss") setTransform((value) => {
1624
+ return {
1625
+ ...value,
1626
+ dismissY: session.startDismissY + dy
1627
+ };
1628
+ });
1629
+ };
1630
+ const handlePointerEnd = (event) => {
1631
+ if (event.type === "pointercancel") onPointerCancel?.(event);
1632
+ else onPointerUp?.(event);
1633
+ const session = sessionRef.current;
1634
+ pointersRef.current.delete(event.pointerId);
1635
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
1636
+ if (!session) {
1637
+ suppressClickRef.current = false;
1638
+ return;
1639
+ }
1640
+ if (pointersRef.current.size > 0) {
1641
+ if (session?.mode === "pinch") {
1642
+ const [remaining] = pointersRef.current.values();
1643
+ if (remaining) {
1644
+ const current = transformRef.current;
1645
+ const now = performance.now();
1646
+ sessionRef.current = {
1647
+ ...session,
1648
+ mode: "pan",
1649
+ startX: remaining.x,
1650
+ startY: remaining.y,
1651
+ startPanX: current.panX,
1652
+ startPanY: current.panY,
1653
+ startSwipeX: current.swipeX,
1654
+ startDismissY: current.dismissY,
1655
+ startScale: current.scale,
1656
+ pinchDistance: 0,
1657
+ pinchCenterX: remaining.x,
1658
+ pinchCenterY: remaining.y,
1659
+ lastX: remaining.x,
1660
+ lastY: remaining.y,
1661
+ lastTime: now,
1662
+ velocityX: 0,
1663
+ velocityY: 0
1664
+ };
1665
+ }
1666
+ }
1667
+ return;
1668
+ }
1669
+ let shouldToggleTouchZoom = false;
1670
+ if (event.type !== "pointercancel" && event.pointerType === "touch" && session?.mode === "pending") {
1671
+ const now = performance.now();
1672
+ const last = lastTapRef.current;
1673
+ if (now - last.time < 300 && Math.hypot(event.clientX - last.x, event.clientY - last.y) < 30) {
1674
+ shouldToggleTouchZoom = true;
1675
+ lastTapRef.current.time = 0;
1676
+ } else lastTapRef.current = {
1677
+ time: now,
1678
+ x: event.clientX,
1679
+ y: event.clientY
1680
+ };
1681
+ }
1682
+ const shouldToggleZoomOnDesktop = event.type !== "pointercancel" && event.pointerType !== "touch" && event.button === 0 && session?.mode === "pending" && clickZoomable;
1683
+ suppressClickRef.current = event.type === "pointercancel" || event.pointerType === "touch" || session?.mode !== "pending" || shouldToggleZoomOnDesktop;
1684
+ window.clearTimeout(suppressClickTimerRef.current);
1685
+ if (suppressClickRef.current) suppressClickTimerRef.current = window.setTimeout(() => {
1686
+ suppressClickRef.current = false;
1687
+ }, 0);
1688
+ releaseGesture(event.nativeEvent);
1689
+ if (shouldToggleTouchZoom) toggleZoomAt(event.clientX, event.clientY);
1690
+ else if (shouldToggleZoomOnDesktop) toggleZoomAt(event.clientX, event.clientY);
1691
+ };
1692
+ const handleClick = (event) => {
1693
+ onClick?.(event);
1694
+ if (event.defaultPrevented) return;
1695
+ if (suppressClickRef.current) {
1696
+ suppressClickRef.current = false;
1697
+ return;
1698
+ }
1699
+ const target = event.target;
1700
+ const activeZoom = target.closest("[data-slot=\"lightbox-zoom\"][data-active=\"true\"]");
1701
+ const interactiveTarget = target.closest(interactiveSelector);
1702
+ if (!activeZoom || interactiveTarget && interactiveTarget !== event.currentTarget || !clickZoomable) return;
1703
+ toggleZoomAt(event.clientX, event.clientY);
1704
+ };
1705
+ const handleKeyDown = (event) => {
1706
+ onKeyDown?.(event);
1707
+ if (event.defaultPrevented || event.target !== event.currentTarget || !clickZoomable || event.key !== "Enter" && event.key !== " ") return;
1708
+ event.preventDefault();
1709
+ const rect = event.currentTarget.getBoundingClientRect();
1710
+ toggleZoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2);
1711
+ };
1712
+ const releaseWheel = React$1.useCallback((event) => {
1713
+ const current = wheelRef.current;
1714
+ const width = viewportRef.current?.clientWidth ?? window.innerWidth;
1715
+ const height = viewportRef.current?.clientHeight ?? window.innerHeight;
1716
+ if (!noCarousel && (Math.abs(current.x) > 100 || Math.abs(transformRef.current.swipeX) > width * .25)) {
1717
+ const rtl = direction === "rtl" ? -1 : 1;
1718
+ animateNavigation(current.x > 0 ? rtl : -rtl, "swipe", event);
1719
+ } else if (Math.abs(current.y) > 100 || Math.abs(transformRef.current.dismissY) > height * .2) {
1720
+ const target = Math.sign(current.y || transformRef.current.dismissY) * height;
1721
+ animateTransformValue("dismissY", target);
1722
+ closeFromGesture(event);
1723
+ } else {
1724
+ animateTransformValue("swipeX", 0);
1725
+ animateTransformValue("dismissY", 0);
1726
+ }
1727
+ wheelRef.current.x = 0;
1728
+ wheelRef.current.y = 0;
1729
+ }, [
1730
+ animateNavigation,
1731
+ animateTransformValue,
1732
+ closeFromGesture,
1733
+ direction,
1734
+ noCarousel
1735
+ ]);
1736
+ const handleWheel = (event) => {
1737
+ onWheel?.(event);
1738
+ event.stopPropagation();
1739
+ if (event.defaultPrevented) return;
1740
+ const interactiveTarget = event.target.closest(interactiveSelector);
1741
+ if (interactiveTarget && interactiveTarget !== event.currentTarget) return;
1742
+ const likelyMouseWheel = event.deltaMode !== WheelEvent.DOM_DELTA_PIXEL || Math.abs(event.deltaY) >= 50 && Math.abs(event.deltaX) < 8;
1743
+ if (event.altKey || likelyMouseWheel) {
1744
+ if (!zoomable) return;
1745
+ event.preventDefault();
1746
+ const sensitivity = event.altKey ? 8e-4 : .0016;
1747
+ const scale = transformRef.current.scale * Math.exp(-event.deltaY * sensitivity);
1748
+ animateZoomAt(scale, event.clientX, event.clientY);
1749
+ return;
1750
+ }
1751
+ event.preventDefault();
1752
+ if (transformRef.current.scale > 1) {
1753
+ const bounds = getPanBounds(transformRef.current.scale);
1754
+ setTransform((value) => {
1755
+ return {
1756
+ ...value,
1757
+ panX: clamp(value.panX - event.deltaX, -bounds.x, bounds.x),
1758
+ panY: clamp(value.panY - event.deltaY, -bounds.y, bounds.y)
1759
+ };
1760
+ });
1761
+ return;
1762
+ }
1763
+ wheelRef.current.x += event.deltaX;
1764
+ wheelRef.current.y += event.deltaY;
1765
+ if (!noCarousel && Math.abs(wheelRef.current.x) > Math.abs(wheelRef.current.y)) setTransform((value) => {
1766
+ return {
1767
+ ...value,
1768
+ swipeX: -wheelRef.current.x
1769
+ };
1770
+ });
1771
+ else setTransform((value) => {
1772
+ return {
1773
+ ...value,
1774
+ dismissY: wheelRef.current.y
1775
+ };
1776
+ });
1777
+ window.clearTimeout(wheelRef.current.timer);
1778
+ const nativeEvent = event.nativeEvent;
1779
+ wheelRef.current.timer = window.setTimeout(() => releaseWheel(nativeEvent), 120);
1780
+ };
1781
+ return /* @__PURE__ */ jsx("div", {
1782
+ ref: viewportRef,
1783
+ "data-slot": "lightbox-slides",
1784
+ "data-dragging": transform.dragging ? "true" : "false",
1785
+ "data-zoomed": transform.scale > 1 ? "true" : "false",
1786
+ role: clickZoomable ? "button" : void 0,
1787
+ "aria-label": clickZoomable ? transform.scale > 1 ? "Zoom out current image" : "Zoom in current image" : void 0,
1788
+ tabIndex: clickZoomable ? 0 : void 0,
1789
+ className: cn("relative min-h-0 flex-1 overflow-hidden touch-none outline-none select-none focus-visible:outline-none focus-visible:ring-0", className),
1790
+ onPointerDown: handlePointerDown,
1791
+ onPointerMove: handlePointerMove,
1792
+ onPointerUp: handlePointerEnd,
1793
+ onPointerCancel: handlePointerEnd,
1794
+ onClick: handleClick,
1795
+ onKeyDown: handleKeyDown,
1796
+ onWheel: handleWheel,
1797
+ onDoubleClick: (event) => {
1798
+ onDoubleClick?.(event);
1799
+ },
1800
+ ...props,
1801
+ children: items.map((item, itemIndex) => {
1802
+ if (!isIndexMounted(itemIndex, activeIndex, items.length, preload, loop)) return null;
1803
+ const preferredOffset = (transform.swipeX === 0 ? 0 : -Math.sign(transform.swipeX)) * (direction === "rtl" ? -1 : 1);
1804
+ const offset = relativeSlideOffset(itemIndex, activeIndex, items.length, loop, preferredOffset);
1805
+ return /* @__PURE__ */ jsx(LightboxSlide, {
1806
+ index: itemIndex,
1807
+ active: itemIndex === activeIndex,
1808
+ style: { transform: `translate3d(calc(${offset * 100 * (direction === "rtl" ? -1 : 1)}% + ${transform.swipeX}px), ${transform.dismissY}px, 0)` }
1809
+ }, item.id ?? itemIndex);
1810
+ })
1811
+ });
1812
+ }
1813
+ function LightboxToolbar({ className, children, ...props }) {
1814
+ return /* @__PURE__ */ jsx("div", {
1815
+ "data-slot": "lightbox-toolbar",
1816
+ className: cn("pointer-events-auto flex items-center gap-1", className),
1817
+ role: "toolbar",
1818
+ "aria-label": "Lightbox controls",
1819
+ ...props,
1820
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1821
+ /* @__PURE__ */ jsx(LightboxDownload, {}),
1822
+ /* @__PURE__ */ jsx(LightboxZoomButton, {}),
1823
+ /* @__PURE__ */ jsx(LightboxMore, {}),
1824
+ /* @__PURE__ */ jsx(LightboxClose, {})
1825
+ ] })
1826
+ });
1827
+ }
1828
+ function LightboxCounter({ className, ...props }) {
1829
+ const { activeIndex, items } = useLightbox();
1830
+ return /* @__PURE__ */ jsxs("div", {
1831
+ "data-slot": "lightbox-counter",
1832
+ className: cn("pointer-events-auto rounded-lg px-3 py-2 text-sm font-medium tabular-nums", className),
1833
+ ...props,
1834
+ children: [/* @__PURE__ */ jsxs("span", {
1835
+ "aria-hidden": "true",
1836
+ children: [
1837
+ items.length > 0 ? activeIndex + 1 : 0,
1838
+ " / ",
1839
+ items.length
1840
+ ]
1841
+ }), /* @__PURE__ */ jsxs("span", {
1842
+ className: "sr-only",
1843
+ children: [
1844
+ "Item ",
1845
+ items.length > 0 ? activeIndex + 1 : 0,
1846
+ " of ",
1847
+ items.length
1848
+ ]
1849
+ })]
1850
+ });
1851
+ }
1852
+ function LightboxCaptions({ className, ...props }) {
1853
+ const { activeIndex, activeItem } = useLightbox();
1854
+ if (!activeItem?.caption) return null;
1855
+ return /* @__PURE__ */ jsx("div", {
1856
+ id: `lightbox-caption-${activeIndex}`,
1857
+ "data-slot": "lightbox-captions",
1858
+ className: cn("pointer-events-auto max-w-2xl px-3 py-2 text-center text-sm [text-shadow:0_1px_3px_rgb(0_0_0/0.8)]", className),
1859
+ ...props,
1860
+ children: activeItem.caption
1861
+ });
1862
+ }
1863
+ function LightboxVideoThumbnail({ crossOrigin, onLoad, src }) {
1864
+ const videoRef = React$1.useRef(null);
1865
+ React$1.useEffect(() => {
1866
+ const video = videoRef.current;
1867
+ if (!video) return;
1868
+ const showFrame = () => {
1869
+ const duration = Number.isFinite(video.duration) ? video.duration : 0;
1870
+ video.currentTime = Math.min(.1, Math.max(0, duration / 2));
1871
+ };
1872
+ video.addEventListener("loadedmetadata", showFrame);
1873
+ if (video.readyState >= HTMLMediaElement.HAVE_METADATA) showFrame();
1874
+ return () => video.removeEventListener("loadedmetadata", showFrame);
1875
+ }, [src]);
1876
+ return /* @__PURE__ */ jsx("video", {
1877
+ ref: videoRef,
1878
+ "aria-hidden": "true",
1879
+ crossOrigin,
1880
+ muted: true,
1881
+ onLoadedData: onLoad,
1882
+ playsInline: true,
1883
+ preload: "metadata",
1884
+ src,
1885
+ tabIndex: -1,
1886
+ className: "size-full object-cover"
1887
+ });
1888
+ }
1889
+ function LightboxThumbnail({ index, className, onClick, ...props }) {
1890
+ const { activeIndex, changeIndex, items, loadedAssets, loading, markAssetLoaded } = useLightbox();
1891
+ const item = items[index];
1892
+ const buttonRef = React$1.useRef(null);
1893
+ const active = index === activeIndex;
1894
+ const [intersected, setIntersected] = React$1.useState(false);
1895
+ const [failedLoadKey, setFailedLoadKey] = React$1.useState(null);
1896
+ const thumbnailLoadKey = item ? getThumbnailLoadKey(item) : "";
1897
+ const thumbnailFailed = failedLoadKey === thumbnailLoadKey;
1898
+ const thumbnailCached = item ? loadedAssets.has(thumbnailLoadKey) : false;
1899
+ React$1.useEffect(() => {
1900
+ if (loading === "eager" || active || intersected || thumbnailCached) return;
1901
+ const button = buttonRef.current;
1902
+ if (!button) return;
1903
+ if (typeof IntersectionObserver === "undefined") {
1904
+ const frame = requestAnimationFrame(() => setIntersected(true));
1905
+ return () => cancelAnimationFrame(frame);
1906
+ }
1907
+ const root = button.closest("[data-slot=\"scroll-area-viewport\"]");
1908
+ const observer = new IntersectionObserver(([entry]) => {
1909
+ if (!entry?.isIntersecting) return;
1910
+ setIntersected(true);
1911
+ observer.disconnect();
1912
+ }, {
1913
+ root,
1914
+ threshold: .01
1915
+ });
1916
+ observer.observe(button);
1917
+ return () => observer.disconnect();
1918
+ }, [
1919
+ active,
1920
+ intersected,
1921
+ loading,
1922
+ thumbnailCached
1923
+ ]);
1924
+ React$1.useEffect(() => {
1925
+ if (active) buttonRef.current?.scrollIntoView({
1926
+ block: "nearest",
1927
+ inline: "center"
1928
+ });
1929
+ }, [active]);
1930
+ if (!item) return null;
1931
+ const shouldLoad = loading === "eager" || active || intersected || thumbnailCached;
1932
+ const thumbnailSource = item.type === "image" ? item.thumbnailSrc ?? item.src : item.poster;
1933
+ const thumbnail = shouldLoad ? item.thumbnail ?? (item.type === "image" ? /* @__PURE__ */ jsx("img", {
1934
+ src: thumbnailSource,
1935
+ alt: "",
1936
+ draggable: false,
1937
+ loading: "lazy",
1938
+ className: "size-full object-cover",
1939
+ onLoad: () => markAssetLoaded(thumbnailLoadKey)
1940
+ }) : item.poster ? /* @__PURE__ */ jsx("img", {
1941
+ src: item.poster,
1942
+ alt: "",
1943
+ draggable: false,
1944
+ loading: "lazy",
1945
+ className: "size-full object-cover",
1946
+ onLoad: () => markAssetLoaded(thumbnailLoadKey)
1947
+ }) : /* @__PURE__ */ jsx(LightboxVideoThumbnail, {
1948
+ crossOrigin: item.videoProps?.crossOrigin,
1949
+ onLoad: () => markAssetLoaded(thumbnailLoadKey),
1950
+ src: item.src
1951
+ })) : null;
1952
+ const mediaLoaded = item.thumbnail !== void 0 ? shouldLoad : thumbnailCached;
1953
+ return /* @__PURE__ */ jsx(Button, {
1954
+ ref: buttonRef,
1955
+ "data-slot": "lightbox-thumbnail",
1956
+ "data-index": index,
1957
+ "data-active": active ? "true" : "false",
1958
+ variant: "ghost",
1959
+ size: "icon-lg",
1960
+ "aria-current": active ? "true" : void 0,
1961
+ "aria-label": `Show ${getItemLabel(item, index)}${thumbnailFailed ? " (thumbnail unavailable)" : ""}`,
1962
+ tabIndex: active ? 0 : -1,
1963
+ className: cn("size-12 shrink-0 overflow-hidden rounded-lg bg-transparent! p-0 text-muted-foreground text-xs font-medium opacity-65 hover:bg-transparent! hover:opacity-100 active:bg-transparent! focus-visible:bg-transparent! focus-visible:opacity-100 aria-expanded:bg-transparent! data-[active=true]:opacity-100 sm:size-14", className),
1964
+ onClick: (event) => {
1965
+ onClick?.(event);
1966
+ if (!event.defaultPrevented) changeIndex(index, "thumbnail-press", event.nativeEvent);
1967
+ },
1968
+ ...props,
1969
+ children: /* @__PURE__ */ jsxs("span", {
1970
+ "aria-hidden": "true",
1971
+ "data-slot": "lightbox-thumbnail-content",
1972
+ className: "pointer-events-none relative flex size-full items-center justify-center",
1973
+ onErrorCapture: () => setFailedLoadKey(thumbnailLoadKey),
1974
+ onLoadCapture: () => setFailedLoadKey(null),
1975
+ onLoadedDataCapture: () => setFailedLoadKey(null),
1976
+ children: [thumbnailFailed ? /* @__PURE__ */ jsx(ImageOffIcon, { className: "size-5" }) : !mediaLoaded && /* @__PURE__ */ jsx(Skeleton, {
1977
+ "aria-hidden": "true",
1978
+ className: "absolute inset-0 size-full rounded-none"
1979
+ }), thumbnail && /* @__PURE__ */ jsx("span", {
1980
+ className: cn("relative size-full transition-opacity duration-200 [&>*]:size-full [&>img]:object-cover [&>video]:object-cover", !mediaLoaded && "opacity-0", thumbnailFailed && "hidden"),
1981
+ children: thumbnail
1982
+ })]
1983
+ })
1984
+ });
1985
+ }
1986
+ function LightboxThumbnails({ className, children, onKeyDown, ...props }) {
1987
+ const { activeIndex, changeIndex, direction, items, loop } = useLightbox();
1988
+ return /* @__PURE__ */ jsx(ScrollArea$1, {
1989
+ "data-slot": "lightbox-thumbnails",
1990
+ className: cn("pointer-events-auto max-w-[min(100%,42rem)] rounded-lg p-1.5", className),
1991
+ orientation: "horizontal",
1992
+ hideScrollbar: true,
1993
+ scrollShadow: "horizontal",
1994
+ fadeColor: "rgb(0 0 0 / 0.25)",
1995
+ ...props,
1996
+ children: /* @__PURE__ */ jsx(ScrollAreaContent, {
1997
+ className: "flex w-max items-center gap-1.5",
1998
+ role: "toolbar",
1999
+ "aria-label": "Choose media",
2000
+ onKeyDown: (event) => {
2001
+ onKeyDown?.(event);
2002
+ if (event.defaultPrevented) return;
2003
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
2004
+ event.preventDefault();
2005
+ const rtl = direction === "rtl" ? -1 : 1;
2006
+ const physical = event.key === "ArrowRight" ? 1 : -1;
2007
+ const next = normalizeIndex(activeIndex + physical * rtl, items.length, loop);
2008
+ const thumbnails = event.currentTarget;
2009
+ changeIndex(next, "keyboard", event.nativeEvent);
2010
+ requestAnimationFrame(() => {
2011
+ thumbnails.querySelector(`[data-slot="lightbox-thumbnail"][data-index="${next}"]`)?.focus();
2012
+ });
2013
+ },
2014
+ children: children ?? items.map((item, index) => /* @__PURE__ */ jsx(LightboxThumbnail, { index }, item.id ?? index))
2015
+ })
2016
+ });
2017
+ }
2018
+ function LightboxPrevious({ className, disabled: disabledProp, onClick, style, ...props }) {
2019
+ const { activeIndex, changeIndex, direction, items, loop } = useLightbox();
2020
+ if (disabledProp === true || items.length < 2 || !loop && activeIndex === 0) return null;
2021
+ return /* @__PURE__ */ jsx(Button, {
2022
+ "data-slot": "lightbox-previous",
2023
+ variant: "ghost",
2024
+ size: "icon-lg",
2025
+ "aria-label": "Previous item",
2026
+ className: cn("absolute start-2 z-20 size-12 -translate-y-1/2 rounded-lg bg-transparent text-white hover:bg-white/15 hover:text-white focus-visible:bg-white/15 sm:start-4 [&_svg:not([class*='size-'])]:size-4.5", className),
2027
+ style: {
2028
+ top: "calc(50% + (var(--lightbox-header-inset, 5rem) - 9rem) / 2)",
2029
+ ...style
2030
+ },
2031
+ onClick: (event) => {
2032
+ onClick?.(event);
2033
+ if (!event.defaultPrevented) changeIndex(activeIndex - 1, "previous-press", event.nativeEvent);
2034
+ },
2035
+ ...props,
2036
+ children: direction === "rtl" ? /* @__PURE__ */ jsx(ChevronRightIcon, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronLeftIcon, { "aria-hidden": "true" })
2037
+ });
2038
+ }
2039
+ function LightboxNext({ className, disabled: disabledProp, onClick, style, ...props }) {
2040
+ const { activeIndex, changeIndex, direction, items, loop } = useLightbox();
2041
+ if (disabledProp === true || items.length < 2 || !loop && activeIndex === items.length - 1) return null;
2042
+ return /* @__PURE__ */ jsx(Button, {
2043
+ "data-slot": "lightbox-next",
2044
+ variant: "ghost",
2045
+ size: "icon-lg",
2046
+ "aria-label": "Next item",
2047
+ className: cn("absolute end-2 z-20 size-12 -translate-y-1/2 rounded-lg bg-transparent text-white hover:bg-white/15 hover:text-white focus-visible:bg-white/15 sm:end-4 [&_svg:not([class*='size-'])]:size-4.5", className),
2048
+ style: {
2049
+ top: "calc(50% + (var(--lightbox-header-inset, 5rem) - 9rem) / 2)",
2050
+ ...style
2051
+ },
2052
+ onClick: (event) => {
2053
+ onClick?.(event);
2054
+ if (!event.defaultPrevented) changeIndex(activeIndex + 1, "next-press", event.nativeEvent);
2055
+ },
2056
+ ...props,
2057
+ children: direction === "rtl" ? /* @__PURE__ */ jsx(ChevronLeftIcon, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRightIcon, { "aria-hidden": "true" })
2058
+ });
2059
+ }
2060
+ function LightboxZoomButton({ className, disabled, onClick, ...props }) {
2061
+ const { activeItem, actions, transform } = useLightbox();
2062
+ if (!(activeItem?.type === "image" ? activeItem.zoomable !== false : Boolean(activeItem?.zoomable)) || disabled) return null;
2063
+ const zoomed = transform.scale > 1;
2064
+ return /* @__PURE__ */ jsx(Button, {
2065
+ "data-slot": "lightbox-zoom-button",
2066
+ variant: "ghost",
2067
+ size: "icon-lg",
2068
+ "aria-label": zoomed ? "Zoom out" : "Zoom in",
2069
+ className: cn("text-white hover:bg-white/15 hover:text-white [&_svg:not([class*='size-'])]:size-4.5", className),
2070
+ onClick: (event) => {
2071
+ onClick?.(event);
2072
+ if (!event.defaultPrevented) {
2073
+ if (zoomed) actions.resetZoom();
2074
+ else actions.zoomIn();
2075
+ }
2076
+ },
2077
+ ...props,
2078
+ children: zoomed ? /* @__PURE__ */ jsx(ZoomOutIcon, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ZoomInIcon, { "aria-hidden": "true" })
2079
+ });
2080
+ }
2081
+ function getDownload(item) {
2082
+ if (!item) return void 0;
2083
+ if (item.download === false) return void 0;
2084
+ if (typeof item.download === "object") return {
2085
+ src: item.download.src ?? item.src,
2086
+ filename: item.download.filename
2087
+ };
2088
+ return {
2089
+ src: item.src,
2090
+ filename: void 0
2091
+ };
2092
+ }
2093
+ function getDownloadFilename(response, configured) {
2094
+ if (configured) return configured;
2095
+ const disposition = response.headers.get("content-disposition");
2096
+ const encodedFilename = disposition?.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
2097
+ const plainFilename = disposition?.match(/filename="?([^";]+)"?/i)?.[1];
2098
+ const candidate = encodedFilename ?? plainFilename;
2099
+ if (candidate) try {
2100
+ return decodeURIComponent(candidate).replace(/[\\/:*?"<>|]/g, "-");
2101
+ } catch {
2102
+ return candidate.replace(/[\\/:*?"<>|]/g, "-");
2103
+ }
2104
+ try {
2105
+ const segment = new URL(response.url).pathname.split("/").filter(Boolean).at(-1);
2106
+ return segment ? decodeURIComponent(segment) : "download";
2107
+ } catch {
2108
+ return "download";
2109
+ }
2110
+ }
2111
+ async function downloadFile(source, filename, signal) {
2112
+ const response = await fetch(source, { signal });
2113
+ if (!response.ok) throw new Error(`Download failed with status ${response.status}`);
2114
+ const blobUrl = URL.createObjectURL(await response.blob());
2115
+ const anchor = document.createElement("a");
2116
+ anchor.href = blobUrl;
2117
+ anchor.download = getDownloadFilename(response, filename);
2118
+ anchor.hidden = true;
2119
+ document.body.append(anchor);
2120
+ anchor.click();
2121
+ anchor.remove();
2122
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1e3);
2123
+ }
2124
+ function LightboxDownload({ className, disabled, onClick, ...props }) {
2125
+ const { activeItem } = useLightbox();
2126
+ const download = getDownload(activeItem);
2127
+ const abortControllerRef = React$1.useRef(null);
2128
+ const resetTimerRef = React$1.useRef(0);
2129
+ const [status, setStatus] = React$1.useState("idle");
2130
+ React$1.useEffect(() => () => {
2131
+ const controller = abortControllerRef.current;
2132
+ abortControllerRef.current = null;
2133
+ controller?.abort();
2134
+ window.clearTimeout(resetTimerRef.current);
2135
+ }, [download?.src]);
2136
+ if (!download || disabled) return null;
2137
+ const statusMessage = {
2138
+ idle: "",
2139
+ loading: "Preparing download",
2140
+ success: "Download started",
2141
+ error: "Download failed"
2142
+ }[status];
2143
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
2144
+ "data-slot": "lightbox-download",
2145
+ type: "button",
2146
+ variant: "ghost",
2147
+ size: "icon-lg",
2148
+ "aria-label": status === "loading" ? "Preparing current item download" : "Download current item",
2149
+ "aria-busy": status === "loading" || void 0,
2150
+ className: cn("text-white hover:bg-white/15 hover:text-white [&_svg:not([class*='size-'])]:size-4.5", className),
2151
+ onClick: (event) => {
2152
+ onClick?.(event);
2153
+ if (event.defaultPrevented || status === "loading") return;
2154
+ abortControllerRef.current?.abort();
2155
+ window.clearTimeout(resetTimerRef.current);
2156
+ const controller = new AbortController();
2157
+ abortControllerRef.current = controller;
2158
+ setStatus("loading");
2159
+ downloadFile(download.src, download.filename, controller.signal).then(() => setStatus("success")).catch((error) => {
2160
+ if (error instanceof DOMException && error.name === "AbortError") return;
2161
+ setStatus("error");
2162
+ }).finally(() => {
2163
+ if (abortControllerRef.current === controller) {
2164
+ abortControllerRef.current = null;
2165
+ resetTimerRef.current = window.setTimeout(() => setStatus("idle"), 2e3);
2166
+ }
2167
+ });
2168
+ },
2169
+ ...props,
2170
+ children: /* @__PURE__ */ jsx(DownloadIcon, { "aria-hidden": "true" })
2171
+ }), /* @__PURE__ */ jsx("span", {
2172
+ className: "sr-only",
2173
+ role: "status",
2174
+ "aria-live": "polite",
2175
+ children: statusMessage
2176
+ })] });
2177
+ }
2178
+ function LightboxMore({ className, ...props }) {
2179
+ const { activeIndex, activeItem, actions, controls } = useLightbox();
2180
+ const availableControls = controls.filter((control) => !control.disabled);
2181
+ if (availableControls.length === 0 || props.disabled) return null;
2182
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
2183
+ render: /* @__PURE__ */ jsx(Button, {
2184
+ "data-slot": "lightbox-more",
2185
+ variant: "ghost",
2186
+ size: "icon-lg",
2187
+ "aria-label": "More actions",
2188
+ className: cn("text-white hover:bg-white/15 hover:text-white [&_svg:not([class*='size-'])]:size-4.5", className),
2189
+ ...props
2190
+ }),
2191
+ children: /* @__PURE__ */ jsx(EllipsisIcon, { "aria-hidden": "true" })
2192
+ }), /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
2193
+ align: "end",
2194
+ sideOffset: 4,
2195
+ className: "isolate z-50 outline-none",
2196
+ children: /* @__PURE__ */ jsx(Menu.Popup, {
2197
+ "data-slot": "dropdown-menu-content",
2198
+ className: "max-h-(--available-height) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95",
2199
+ children: availableControls.map((control) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
2200
+ onClick: (event) => control.onClick({
2201
+ item: activeItem,
2202
+ index: activeIndex,
2203
+ event,
2204
+ actions
2205
+ }),
2206
+ children: [control.icon, /* @__PURE__ */ jsx("span", { children: control.label })]
2207
+ }, control.id))
2208
+ })
2209
+ }) })] });
2210
+ }
2211
+ function LightboxClose({ className, children, ...props }) {
2212
+ return /* @__PURE__ */ jsx(Dialog.Close, {
2213
+ "data-slot": "lightbox-close",
2214
+ "aria-label": "Close lightbox",
2215
+ render: /* @__PURE__ */ jsx(Button, {
2216
+ variant: "ghost",
2217
+ size: "icon-lg",
2218
+ className: cn("text-white hover:bg-white/15 hover:text-white [&_svg:not([class*='size-'])]:size-4.5", className)
2219
+ }),
2220
+ ...props,
2221
+ children: children ?? /* @__PURE__ */ jsx(XIcon, { "aria-hidden": "true" })
2222
+ });
2223
+ }
2224
+ function ScrollArea$1({ className, cornerStyle, fadeColor, horizontalScrollbarStyle, viewportClassName, viewportTabIndex, hideScrollbar = false, orientation = "vertical", scrollbarClassName, scrollShadow = "none", verticalScrollbarStyle, children, ...props }) {
2225
+ const fadeColorStyle = fadeColor === void 0 ? {} : { "--scroll-area-fade": fadeColor };
2226
+ return /* @__PURE__ */ jsxs(ScrollArea.Root, {
2227
+ className: cn("relative isolate min-h-0 flex flex-col", className),
2228
+ ...props,
2229
+ children: [
2230
+ /* @__PURE__ */ jsxs(ScrollArea.Viewport, {
2231
+ "data-fade-mode": fadeColor === void 0 ? "auto" : "custom",
2232
+ "data-scroll-shadow": scrollShadow,
2233
+ "data-slot": "scroll-area-viewport",
2234
+ tabIndex: viewportTabIndex,
2235
+ className: cn("size-full min-h-0 flex-1 overscroll-contain rounded-[inherit] outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", orientation === "vertical" && "overflow-x-hidden! [overflow-x:hidden!important]", orientation === "horizontal" && "overflow-y-hidden! [overflow-y:hidden!important]", viewportClassName),
2236
+ style: {
2237
+ ...orientation === "vertical" ? { overflowX: "hidden" } : {},
2238
+ ...orientation === "horizontal" ? { overflowY: "hidden" } : {}
2239
+ },
2240
+ children: [
2241
+ /* @__PURE__ */ jsx("div", {
2242
+ "data-slot": "scroll-area-vertical-shadow",
2243
+ className: cn(scrollShadow === "vertical" || scrollShadow === "both" ? "block" : "hidden", [
2244
+ "pointer-events-none absolute inset-0 z-10",
2245
+ "before:absolute before:top-0 before:block before:w-full before:rounded-md before:rounded-b-none before:content-[''] after:absolute after:bottom-0 after:block after:w-full after:rounded-md after:rounded-t-none after:content-['']",
2246
+ "before:pointer-events-none after:pointer-events-none",
2247
+ "before:transition-[height] after:transition-[height] before:duration-100 after:duration-100 before:ease-out after:ease-out",
2248
+ "before:[--scroll-area-overflow-y-start:inherit]",
2249
+ "after:[--scroll-area-overflow-y-end:inherit]",
2250
+ "[--scroll-area-fade:var(--background)] before:h-[min(var(--scroll-area-fade-size,40px),var(--scroll-area-overflow-y-start))] before:bg-[linear-gradient(to_bottom,var(--scroll-area-fade),transparent)] after:h-[min(var(--scroll-area-fade-size,40px),var(--scroll-area-overflow-y-end,40px))] after:bg-[linear-gradient(to_top,var(--scroll-area-fade),transparent)]"
2251
+ ]),
2252
+ style: {
2253
+ "--scroll-area-overflow-y-start": "inherit",
2254
+ "--scroll-area-overflow-y-end": "inherit",
2255
+ ...fadeColorStyle
2256
+ }
2257
+ }),
2258
+ /* @__PURE__ */ jsx("div", {
2259
+ "data-slot": "scroll-area-horizontal-shadow",
2260
+ className: cn(scrollShadow === "horizontal" || scrollShadow === "both" ? "block" : "hidden", [
2261
+ "pointer-events-none absolute inset-0 z-10",
2262
+ "before:absolute before:top-0 before:block before:h-full before:rounded-md before:rounded-r-none before:content-[''] after:absolute after:top-0 after:block after:h-full after:rounded-md after:rounded-l-none after:content-['']",
2263
+ "before:pointer-events-none after:pointer-events-none",
2264
+ "before:transition-[width] after:transition-[width] before:duration-100 after:duration-100 before:ease-out after:ease-out",
2265
+ "before:[--scroll-area-overflow-x-start:inherit]",
2266
+ "after:[--scroll-area-overflow-x-end:inherit]",
2267
+ "[--scroll-area-fade:var(--background)] before:left-0 before:w-[min(var(--scroll-area-fade-size,40px),var(--scroll-area-overflow-x-start))] before:bg-[linear-gradient(to_right,var(--scroll-area-fade),transparent)] after:right-0 after:w-[min(var(--scroll-area-fade-size,40px),var(--scroll-area-overflow-x-end,40px))] after:bg-[linear-gradient(to_left,var(--scroll-area-fade),transparent)]"
2268
+ ]),
2269
+ style: {
2270
+ "--scroll-area-overflow-x-start": "inherit",
2271
+ "--scroll-area-overflow-x-end": "inherit",
2272
+ ...fadeColorStyle
2273
+ }
2274
+ }),
2275
+ children
2276
+ ]
2277
+ }),
2278
+ !hideScrollbar && (orientation === "both" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ScrollBar, {
2279
+ orientation: "vertical",
2280
+ className: scrollbarClassName,
2281
+ style: verticalScrollbarStyle
2282
+ }), /* @__PURE__ */ jsx(ScrollBar, {
2283
+ orientation: "horizontal",
2284
+ className: scrollbarClassName,
2285
+ style: horizontalScrollbarStyle
2286
+ })] }) : /* @__PURE__ */ jsx(ScrollBar, {
2287
+ orientation,
2288
+ className: scrollbarClassName,
2289
+ style: orientation === "vertical" ? verticalScrollbarStyle : horizontalScrollbarStyle
2290
+ })),
2291
+ /* @__PURE__ */ jsx(ScrollArea.Corner, {
2292
+ "data-slot": "scroll-area-corner",
2293
+ style: cornerStyle
2294
+ }),
2295
+ /* @__PURE__ */ jsx("style", { children: `
2296
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"][data-scroll-shadow="vertical"],
2297
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"][data-scroll-shadow="both"] {
2298
+ --scroll-area-fade-y-start: min(var(--scroll-area-overflow-y-start, 0px), var(--scroll-area-fade-size, 40px));
2299
+ --scroll-area-fade-y-end: min(var(--scroll-area-overflow-y-end, 0px), var(--scroll-area-fade-size, 40px));
2300
+ -webkit-mask-image: linear-gradient(
2301
+ to bottom,
2302
+ transparent 0,
2303
+ black var(--scroll-area-fade-y-start),
2304
+ black calc(100% - var(--scroll-area-fade-y-end)),
2305
+ transparent 100%
2306
+ );
2307
+ mask-image: linear-gradient(
2308
+ to bottom,
2309
+ transparent 0,
2310
+ black var(--scroll-area-fade-y-start),
2311
+ black calc(100% - var(--scroll-area-fade-y-end)),
2312
+ transparent 100%
2313
+ );
2314
+ }
2315
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"][data-scroll-shadow="horizontal"],
2316
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"][data-scroll-shadow="both"] {
2317
+ --scroll-area-fade-x-start: min(var(--scroll-area-overflow-x-start, 0px), var(--scroll-area-fade-size, 40px));
2318
+ --scroll-area-fade-x-end: min(var(--scroll-area-overflow-x-end, 0px), var(--scroll-area-fade-size, 40px));
2319
+ -webkit-mask-image: linear-gradient(
2320
+ to right,
2321
+ transparent 0,
2322
+ black var(--scroll-area-fade-x-start),
2323
+ black calc(100% - var(--scroll-area-fade-x-end)),
2324
+ transparent 100%
2325
+ );
2326
+ mask-image: linear-gradient(
2327
+ to right,
2328
+ transparent 0,
2329
+ black var(--scroll-area-fade-x-start),
2330
+ black calc(100% - var(--scroll-area-fade-x-end)),
2331
+ transparent 100%
2332
+ );
2333
+ }
2334
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"][data-scroll-shadow="both"] {
2335
+ -webkit-mask-image: linear-gradient(
2336
+ to bottom,
2337
+ transparent 0,
2338
+ black var(--scroll-area-fade-y-start),
2339
+ black calc(100% - var(--scroll-area-fade-y-end)),
2340
+ transparent 100%
2341
+ ),
2342
+ linear-gradient(
2343
+ to right,
2344
+ transparent 0,
2345
+ black var(--scroll-area-fade-x-start),
2346
+ black calc(100% - var(--scroll-area-fade-x-end)),
2347
+ transparent 100%
2348
+ );
2349
+ mask-image: linear-gradient(
2350
+ to bottom,
2351
+ transparent 0,
2352
+ black var(--scroll-area-fade-y-start),
2353
+ black calc(100% - var(--scroll-area-fade-y-end)),
2354
+ transparent 100%
2355
+ ),
2356
+ linear-gradient(
2357
+ to right,
2358
+ transparent 0,
2359
+ black var(--scroll-area-fade-x-start),
2360
+ black calc(100% - var(--scroll-area-fade-x-end)),
2361
+ transparent 100%
2362
+ );
2363
+ -webkit-mask-composite: source-in;
2364
+ mask-composite: intersect;
2365
+ }
2366
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"] [data-slot="scroll-area-vertical-shadow"],
2367
+ [data-slot="scroll-area-viewport"][data-fade-mode="auto"] [data-slot="scroll-area-horizontal-shadow"] {
2368
+ display: none;
2369
+ }
2370
+ ` })
2371
+ ]
2372
+ });
2373
+ }
2374
+ function ScrollAreaContent({ className, children, ...props }) {
2375
+ return /* @__PURE__ */ jsx(ScrollArea.Content, {
2376
+ "data-slot": "scroll-area-content",
2377
+ className: cn("w-full", className),
2378
+ ...props,
2379
+ children
2380
+ });
2381
+ }
2382
+ function ScrollBar({ className, orientation = "vertical", ...props }) {
2383
+ return /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
2384
+ className: cn("relative z-20 m-1.5 flex rounded-full bg-muted opacity-0 transition-opacity delay-200 data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:w-1.5 data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100", className),
2385
+ "data-base-ui-swipe-ignore": true,
2386
+ "data-slot": "scroll-area-scrollbar",
2387
+ orientation,
2388
+ ...props,
2389
+ children: /* @__PURE__ */ jsx(ScrollArea.Thumb, {
2390
+ className: "relative flex-1 rounded-full bg-muted-foreground/40",
2391
+ "data-slot": "scroll-area-thumb"
2392
+ })
2393
+ });
2394
+ }
2395
+ export { Lightbox, LightboxBackdrop, LightboxCaptions, LightboxClose, LightboxContent, LightboxCounter, LightboxDownload, LightboxMore, LightboxNext, LightboxPrevious, LightboxSlide, LightboxSlides, LightboxThumbnail, LightboxThumbnails, LightboxToolbar, LightboxTrigger, LightboxViewport, LightboxZoom, LightboxZoomButton, useLightbox };