@deftui/react 0.0.3 → 0.0.4

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