@enigmax/primitives 0.25.0 → 0.27.0

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.
Files changed (55) hide show
  1. package/dist/chunk-2RIO4VDV.js +121 -0
  2. package/dist/{chunk-DZMHY3SU.js → chunk-CPJJ3FPW.js} +17 -7
  3. package/dist/chunk-EYZ366LP.js +189 -0
  4. package/dist/chunk-HUUINHPR.js +135 -0
  5. package/dist/chunk-JLZDS6CV.js +177 -0
  6. package/dist/chunk-O2UJ5ZIX.js +387 -0
  7. package/dist/chunk-PVDU6AZU.js +953 -0
  8. package/dist/{chunk-Z3VDE7OA.js → chunk-YQV5XZIA.js} +2 -2
  9. package/dist/clipboard-menu-eemzEBqB.d.ts +93 -0
  10. package/dist/context-DR1dpO8b.d.ts +54 -0
  11. package/dist/{clipboard-menu-B_ouitfS.d.ts → context-menu-qqpGBkny.d.ts} +7 -91
  12. package/dist/index.d.ts +292 -2
  13. package/dist/index.js +5 -3
  14. package/dist/menu-52PXC5PL.js +33 -0
  15. package/dist/menu-YIGMPFUP.js +58 -0
  16. package/dist/next/index.d.ts +6 -2
  17. package/dist/next/index.js +12 -8
  18. package/dist/react/context-menu.d.ts +15 -55
  19. package/dist/react/context-menu.js +2 -2
  20. package/dist/react/image.d.ts +112 -0
  21. package/dist/react/image.js +3 -0
  22. package/dist/react/index.d.ts +6 -2
  23. package/dist/react/index.js +12 -8
  24. package/dist/react/input.js +1 -1
  25. package/dist/react/video.d.ts +126 -0
  26. package/dist/react/video.js +3 -0
  27. package/dist/react-router/index.d.ts +6 -2
  28. package/dist/react-router/index.js +12 -8
  29. package/dist/viewer-N5WLFB24.js +3 -0
  30. package/package.json +15 -2
  31. package/recipes/context-menu/styles.css +8 -1
  32. package/recipes/image/styles.css +182 -0
  33. package/recipes/video/styles.css +154 -0
  34. package/registry.json +486 -5
  35. package/src/core/context-menu.ts +22 -3
  36. package/src/core/image-viewer.ts +269 -0
  37. package/src/core/player.ts +345 -0
  38. package/src/index.ts +55 -0
  39. package/src/react/context-menu/root.tsx +27 -5
  40. package/src/react/context-menu/styles.ts +8 -1
  41. package/src/react/image/icons.tsx +59 -0
  42. package/src/react/image/index.tsx +185 -0
  43. package/src/react/image/menu.tsx +80 -0
  44. package/src/react/image/styles.ts +213 -0
  45. package/src/react/image/types.ts +116 -0
  46. package/src/react/image/viewer.tsx +571 -0
  47. package/src/react/index.ts +3 -0
  48. package/src/react/video/icons.tsx +135 -0
  49. package/src/react/video/index.tsx +731 -0
  50. package/src/react/video/menu.tsx +66 -0
  51. package/src/react/video/rail.tsx +99 -0
  52. package/src/react/video/styles.ts +161 -0
  53. package/src/react/video/types.ts +141 -0
  54. /package/dist/{chunk-3MGBZOAU.js → chunk-RMNXVWQ4.js} +0 -0
  55. /package/dist/{chunk-MSOCCQGH.js → chunk-T7I6WJSN.js} +0 -0
@@ -0,0 +1,177 @@
1
+ // src/core/player.ts
2
+ var SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
3
+ var SEEK_STEP = 5;
4
+ var SEEK_JUMP = 10;
5
+ var VOLUME_STEP = 0.05;
6
+ function formatTime(seconds, duration = seconds) {
7
+ if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
8
+ const total = Math.floor(seconds);
9
+ const hours = Math.floor(total / 3600);
10
+ const minutes = Math.floor(total % 3600 / 60);
11
+ const rest = total % 60;
12
+ const withHours = Number.isFinite(duration) && duration >= 3600;
13
+ const pad = (value) => String(value).padStart(2, "0");
14
+ if (withHours || hours > 0) return `${hours}:${pad(minutes)}:${pad(rest)}`;
15
+ return `${minutes}:${pad(rest)}`;
16
+ }
17
+ function progress(time, duration) {
18
+ if (!Number.isFinite(duration) || duration <= 0) return 0;
19
+ return Math.min(Math.max(time / duration, 0), 1);
20
+ }
21
+ function fractionAt(box, clientX) {
22
+ if (!box.width) return 0;
23
+ return Math.min(Math.max((clientX - box.left) / box.width, 0), 1);
24
+ }
25
+ function bufferedAhead(ranges, time, duration) {
26
+ if (!ranges || !Number.isFinite(duration) || duration <= 0) return 0;
27
+ for (let at = 0; at < ranges.length; at += 1) {
28
+ if (ranges.start(at) <= time && ranges.end(at) >= time) return Math.min(ranges.end(at) / duration, 1);
29
+ }
30
+ return 0;
31
+ }
32
+ function nextSpeed(current, step = 1) {
33
+ const at = SPEEDS.indexOf(current);
34
+ const from = at === -1 ? SPEEDS.indexOf(1) : at;
35
+ return SPEEDS[(from + step + SPEEDS.length) % SPEEDS.length];
36
+ }
37
+ function commandFor(key) {
38
+ if (key === " " || key === "k" || key === "K") return { type: "toggle" };
39
+ if (key === "ArrowRight") return { type: "seek", by: SEEK_STEP };
40
+ if (key === "ArrowLeft") return { type: "seek", by: -SEEK_STEP };
41
+ if (key === "l" || key === "L") return { type: "seek", by: SEEK_JUMP };
42
+ if (key === "j" || key === "J") return { type: "seek", by: -SEEK_JUMP };
43
+ if (key === "ArrowUp") return { type: "volume", by: VOLUME_STEP };
44
+ if (key === "ArrowDown") return { type: "volume", by: -VOLUME_STEP };
45
+ if (key === "m" || key === "M") return { type: "mute" };
46
+ if (key === "f" || key === "F") return { type: "fullscreen" };
47
+ if (key === "c" || key === "C") return { type: "captions" };
48
+ if (key === "p" || key === "P") return { type: "pip" };
49
+ if (key >= "0" && key <= "9") return { type: "seekTo", fraction: Number(key) / 10 };
50
+ return null;
51
+ }
52
+ function isTypingTarget(target) {
53
+ const element = target;
54
+ if (!element || typeof element.tagName !== "string") return false;
55
+ if (element.isContentEditable) return true;
56
+ return ["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName);
57
+ }
58
+ function fullscreenElement() {
59
+ if (typeof document === "undefined") return null;
60
+ const legacy = document;
61
+ return document.fullscreenElement ?? legacy.webkitFullscreenElement ?? null;
62
+ }
63
+ async function toggleFullscreen(container, video) {
64
+ const legacyDocument = document;
65
+ if (fullscreenElement()) {
66
+ await (document.exitFullscreen?.() ?? legacyDocument.webkitExitFullscreen?.());
67
+ return;
68
+ }
69
+ const legacyElement = container;
70
+ if (container.requestFullscreen) return void await container.requestFullscreen();
71
+ if (legacyElement.webkitRequestFullscreen) return void await legacyElement.webkitRequestFullscreen();
72
+ const iosVideo = video;
73
+ iosVideo?.webkitEnterFullscreen?.();
74
+ }
75
+ function supportsPip() {
76
+ return typeof document !== "undefined" && Boolean(document.pictureInPictureEnabled);
77
+ }
78
+ async function togglePip(video) {
79
+ if (document.pictureInPictureElement) return void await document.exitPictureInPicture();
80
+ await video.requestPictureInPicture();
81
+ }
82
+ var SOURCE_TYPES = {
83
+ mp4: "video/mp4",
84
+ m4v: "video/mp4",
85
+ webm: "video/webm",
86
+ ogv: "video/ogg",
87
+ mov: "video/quicktime",
88
+ mkv: "video/x-matroska",
89
+ m3u8: "application/vnd.apple.mpegurl",
90
+ mpd: "application/dash+xml"
91
+ };
92
+ function sourceType(url) {
93
+ let path = url;
94
+ try {
95
+ path = new URL(url, typeof location === "undefined" ? "https://localhost" : location.href).pathname;
96
+ } catch {
97
+ path = url.split("?")[0].split("#")[0];
98
+ }
99
+ const name = path.split("/").pop() ?? "";
100
+ const dot = name.lastIndexOf(".");
101
+ if (dot <= 0) return void 0;
102
+ return SOURCE_TYPES[name.slice(dot + 1).toLowerCase()];
103
+ }
104
+ function captionTracks(list) {
105
+ if (!list) return [];
106
+ const found = [];
107
+ for (let at = 0; at < list.length; at += 1) {
108
+ const track = list[at];
109
+ if (!track || track.kind !== "subtitles" && track.kind !== "captions") continue;
110
+ found.push({ index: at, label: track.label || track.language || `Track ${found.length + 1}`, language: track.language });
111
+ }
112
+ return found;
113
+ }
114
+ function activeCaption(list) {
115
+ if (!list) return -1;
116
+ for (let at = 0; at < list.length; at += 1) if (list[at]?.mode === "showing") return at;
117
+ return -1;
118
+ }
119
+ function showCaption(list, index) {
120
+ if (!list) return;
121
+ for (let at = 0; at < list.length; at += 1) {
122
+ const track = list[at];
123
+ if (track) track.mode = at === index ? "showing" : "disabled";
124
+ }
125
+ }
126
+ function supportsRemote(video) {
127
+ const element = video;
128
+ if (!element || element.disableRemotePlayback) return false;
129
+ return Boolean(element.remote?.prompt ?? element.webkitShowPlaybackTargetPicker);
130
+ }
131
+ function watchRemote(video, onChange) {
132
+ const element = video;
133
+ const state = { available: false, connected: false };
134
+ const publish = (next) => {
135
+ Object.assign(state, next);
136
+ onChange({ ...state });
137
+ };
138
+ if (element.remote) {
139
+ const remote = element.remote;
140
+ const onState = () => publish({ connected: remote.state === "connected" });
141
+ let watch = null;
142
+ let cancelled = false;
143
+ remote.watchAvailability((available) => {
144
+ if (!cancelled) publish({ available });
145
+ }).then((id) => {
146
+ if (cancelled) void remote.cancelWatchAvailability(id).catch(() => {
147
+ });
148
+ else watch = id;
149
+ }).catch(() => {
150
+ if (!cancelled) publish({ available: true });
151
+ });
152
+ for (const name of ["connect", "connecting", "disconnect"]) remote.addEventListener(name, onState);
153
+ onState();
154
+ return () => {
155
+ cancelled = true;
156
+ for (const name of ["connect", "connecting", "disconnect"]) remote.removeEventListener(name, onState);
157
+ if (watch !== null) void remote.cancelWatchAvailability(watch).catch(() => {
158
+ });
159
+ };
160
+ }
161
+ const onAvailability = (event) => publish({ available: event.availability === "available" });
162
+ const onWireless = () => publish({ connected: Boolean(element.webkitCurrentPlaybackTargetIsWireless) });
163
+ element.addEventListener("webkitplaybacktargetavailabilitychanged", onAvailability);
164
+ element.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", onWireless);
165
+ onWireless();
166
+ return () => {
167
+ element.removeEventListener("webkitplaybacktargetavailabilitychanged", onAvailability);
168
+ element.removeEventListener("webkitcurrentplaybacktargetiswirelesschanged", onWireless);
169
+ };
170
+ }
171
+ async function promptRemote(video) {
172
+ const element = video;
173
+ if (element.remote?.prompt) return void await element.remote.prompt();
174
+ element.webkitShowPlaybackTargetPicker?.();
175
+ }
176
+
177
+ export { SEEK_JUMP, SEEK_STEP, SPEEDS, VOLUME_STEP, activeCaption, bufferedAhead, captionTracks, commandFor, formatTime, fractionAt, fullscreenElement, isTypingTarget, nextSpeed, progress, promptRemote, showCaption, sourceType, supportsPip, supportsRemote, toggleFullscreen, togglePip, watchRemote };
@@ -0,0 +1,387 @@
1
+ import { IDENTITY, prefersReducedMotion, ZOOM_LIMITS, flightFrom, nextIndex, fittedSize, clampPan, zoomAt, pinchDistance, flightMs, ZOOM_STEP, ZOOM_DOUBLE, wheelFactor } from './chunk-HUUINHPR.js';
2
+ import { injectImageStyles } from './chunk-EYZ366LP.js';
3
+ import { createPortal } from 'react-dom';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { lazy, useLayoutEffect, useRef, useState, useMemo, useCallback, useEffect, Suspense } from 'react';
6
+
7
+ function Glyph({ children }) {
8
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "1.125em", height: "1.125em", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
9
+ }
10
+ function Close() {
11
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M6 6 18 18M18 6 6 18" }) });
12
+ }
13
+ function Plus() {
14
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" }) });
15
+ }
16
+ function Minus() {
17
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
18
+ }
19
+ function ChevronLeft() {
20
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m15 5-7 7 7 7" }) });
21
+ }
22
+ function ChevronRight() {
23
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m9 5 7 7-7 7" }) });
24
+ }
25
+ function Trash() {
26
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" }) });
27
+ }
28
+ function Dots() {
29
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "1.125em", height: "1.125em", fill: "currentColor", "aria-hidden": "true", children: [
30
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.75" }),
31
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.75" }),
32
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "19", r: "1.75" })
33
+ ] });
34
+ }
35
+ function Download() {
36
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 3v12M7 11l5 5 5-5M4 20h16" }) });
37
+ }
38
+ function NewTab() {
39
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
40
+ /* @__PURE__ */ jsx("path", { d: "M14 4h6v6" }),
41
+ /* @__PURE__ */ jsx("path", { d: "M20 4 11 13" }),
42
+ /* @__PURE__ */ jsx("path", { d: "M18 14v5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 4 19V7.5A1.5 1.5 0 0 1 5.5 6H10" })
43
+ ] });
44
+ }
45
+ var ImageMenu = lazy(() => import('./menu-YIGMPFUP.js').then((module) => ({ default: module.ImageMenu })));
46
+ function ImageViewer({
47
+ items,
48
+ index,
49
+ onIndex,
50
+ onClose,
51
+ zoom,
52
+ navigation,
53
+ thumbnails,
54
+ menu,
55
+ discardable,
56
+ onDiscard,
57
+ loop,
58
+ caption,
59
+ origin,
60
+ animate,
61
+ styles,
62
+ labels
63
+ }) {
64
+ useLayoutEffect(() => {
65
+ if (styles) injectImageStyles();
66
+ }, [styles]);
67
+ const frameRef = useRef(null);
68
+ const stripRef = useRef(null);
69
+ const imageRef = useRef(null);
70
+ const [transform, setTransform] = useState(IDENTITY);
71
+ const [loading, setLoading] = useState(true);
72
+ const [panning, setPanning] = useState(false);
73
+ const [phase, setPhase] = useState(() => animate && origin && !prefersReducedMotion() ? "measuring" : "open");
74
+ const [flight, setFlight] = useState(null);
75
+ const [discarded, setDiscarded] = useState(() => /* @__PURE__ */ new Set());
76
+ const limits = useMemo(() => ({ min: zoom?.min ?? ZOOM_LIMITS.min, max: zoom?.max ?? ZOOM_LIMITS.max }), [zoom]);
77
+ const item = items[index];
78
+ const zoomed = transform.scale > limits.min;
79
+ const remaining = items.length - discarded.size;
80
+ const position = useMemo(() => {
81
+ let seen = 0;
82
+ for (let at = 0; at <= index && at < items.length; at += 1) if (!discarded.has(at)) seen += 1;
83
+ return seen;
84
+ }, [index, items.length, discarded]);
85
+ const layoutBox = useCallback(() => {
86
+ const frame = frameRef.current;
87
+ const element = imageRef.current;
88
+ if (!frame || !element?.offsetWidth) return null;
89
+ const box = frame.getBoundingClientRect();
90
+ return { left: box.left + element.offsetLeft, top: box.top + element.offsetTop, width: element.offsetWidth, height: element.offsetHeight };
91
+ }, []);
92
+ const requestClose = useCallback(() => {
93
+ if (phase === "closing" || phase === "measuring") return onClose();
94
+ const from = origin?.();
95
+ const to = layoutBox();
96
+ const back = animate && !prefersReducedMotion() && from && to ? flightFrom(from, to) : null;
97
+ if (!back) return onClose();
98
+ setPhase("closing");
99
+ setFlight(back);
100
+ }, [phase, origin, layoutBox, animate, onClose]);
101
+ const go = useCallback((step) => {
102
+ const next = nextIndex(index, items.length, step, { loop, skip: discarded });
103
+ if (next === -1 || next === index) return;
104
+ onIndex(next);
105
+ }, [index, items.length, loop, discarded, onIndex]);
106
+ const showAt = useCallback((next) => {
107
+ if (next === index || discarded.has(next)) return;
108
+ onIndex(next);
109
+ }, [index, discarded, onIndex]);
110
+ const discard = useCallback(() => {
111
+ if (!discardable || !item) return;
112
+ const next = nextIndex(index, items.length, 1, { loop: true, skip: /* @__PURE__ */ new Set([...discarded, index]) });
113
+ setDiscarded((current) => new Set(current).add(index));
114
+ onDiscard?.(item, index);
115
+ if (next === -1) requestClose();
116
+ else onIndex(next);
117
+ }, [discardable, item, index, items.length, discarded, onDiscard, onIndex, requestClose]);
118
+ const reset = useCallback(() => setTransform(IDENTITY), []);
119
+ useEffect(() => {
120
+ setTransform(IDENTITY);
121
+ setLoading(true);
122
+ }, [item?.src]);
123
+ const frameBox = useCallback(() => {
124
+ const box = frameRef.current?.getBoundingClientRect();
125
+ return box ? { left: box.left, top: box.top, width: box.width, height: box.height } : null;
126
+ }, []);
127
+ const bounded = useCallback((next) => {
128
+ const box = frameBox();
129
+ const element = imageRef.current;
130
+ if (!box || !element) return next;
131
+ const fitted = fittedSize({ width: element.naturalWidth, height: element.naturalHeight }, box);
132
+ return clampPan(next, box, fitted);
133
+ }, [frameBox]);
134
+ const zoomBy = useCallback((factor, point) => {
135
+ const box = frameBox();
136
+ if (!box) return;
137
+ const at = point ?? { x: box.left + box.width / 2, y: box.top + box.height / 2 };
138
+ setTransform((current) => bounded(zoomAt(current, factor, at, box, limits)));
139
+ }, [frameBox, bounded, limits]);
140
+ useEffect(() => {
141
+ const element = frameRef.current;
142
+ if (!element || !zoom?.wheel) return;
143
+ const onWheel = (event) => {
144
+ event.preventDefault();
145
+ zoomBy(wheelFactor(event.deltaY, event.deltaMode), { x: event.clientX, y: event.clientY });
146
+ };
147
+ element.addEventListener("wheel", onWheel, { passive: false });
148
+ return () => element.removeEventListener("wheel", onWheel);
149
+ }, [zoom?.wheel, zoomBy]);
150
+ const pointers = useRef(/* @__PURE__ */ new Map());
151
+ const pinch = useRef(null);
152
+ const backdropPress = useRef(null);
153
+ const onPointerDown = useCallback((event) => {
154
+ if (event.button !== 0 && event.pointerType === "mouse") return;
155
+ const element = event.currentTarget;
156
+ backdropPress.current = event.target === element && !zoomed ? { x: event.clientX, y: event.clientY } : null;
157
+ pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
158
+ if (pointers.current.size === 2) {
159
+ const [a, b] = [...pointers.current.values()];
160
+ pinch.current = { distance: pinchDistance(a, b), scale: transform.scale };
161
+ setPanning(false);
162
+ return;
163
+ }
164
+ if (!zoomed) return;
165
+ event.preventDefault();
166
+ setPanning(true);
167
+ try {
168
+ element.setPointerCapture(event.pointerId);
169
+ } catch {
170
+ }
171
+ }, [zoomed, transform.scale]);
172
+ const onPointerMove = useCallback((event) => {
173
+ const previous = pointers.current.get(event.pointerId);
174
+ if (!previous) return;
175
+ pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
176
+ if (pointers.current.size === 2 && pinch.current) {
177
+ const [a, b] = [...pointers.current.values()];
178
+ const distance = pinchDistance(a, b);
179
+ if (!pinch.current.distance) return;
180
+ const box = frameBox();
181
+ if (!box) return;
182
+ const factor = distance / pinch.current.distance * (pinch.current.scale / transform.scale);
183
+ setTransform((current) => bounded(zoomAt(current, factor, { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, box, limits)));
184
+ return;
185
+ }
186
+ if (!panning) return;
187
+ setTransform((current) => bounded({ ...current, x: current.x + (event.clientX - previous.x), y: current.y + (event.clientY - previous.y) }));
188
+ }, [panning, bounded, frameBox, limits, transform.scale]);
189
+ const endPointer = useCallback((event) => {
190
+ pointers.current.delete(event.pointerId);
191
+ if (pointers.current.size < 2) pinch.current = null;
192
+ if (pointers.current.size === 0) setPanning(false);
193
+ const started = backdropPress.current;
194
+ backdropPress.current = null;
195
+ if (!started || event.target !== event.currentTarget) return;
196
+ if (Math.abs(event.clientX - started.x) < 4 && Math.abs(event.clientY - started.y) < 4) requestClose();
197
+ }, [requestClose]);
198
+ useLayoutEffect(() => {
199
+ if (phase !== "measuring") return;
200
+ const element = imageRef.current;
201
+ const from = origin?.();
202
+ if (!element || !from) {
203
+ setPhase("open");
204
+ return;
205
+ }
206
+ if (!element.complete) return;
207
+ const to = layoutBox();
208
+ const start = to ? flightFrom(from, to) : null;
209
+ if (!start) {
210
+ setPhase("open");
211
+ return;
212
+ }
213
+ setFlight(start);
214
+ let second = 0;
215
+ const first = requestAnimationFrame(() => {
216
+ second = requestAnimationFrame(() => {
217
+ setFlight(null);
218
+ setPhase("flying");
219
+ });
220
+ });
221
+ return () => {
222
+ cancelAnimationFrame(first);
223
+ cancelAnimationFrame(second);
224
+ };
225
+ }, [phase, origin, layoutBox, loading]);
226
+ useEffect(() => {
227
+ if (phase !== "flying" && phase !== "closing") return;
228
+ const element = imageRef.current;
229
+ const done = (event) => {
230
+ if (event && event.propertyName !== "transform") return;
231
+ if (phase === "closing") onClose();
232
+ else setPhase("open");
233
+ };
234
+ element?.addEventListener("transitionend", done);
235
+ const timer = window.setTimeout(() => done(), flightMs(element) + 80);
236
+ return () => {
237
+ element?.removeEventListener("transitionend", done);
238
+ window.clearTimeout(timer);
239
+ };
240
+ }, [phase, onClose]);
241
+ useEffect(() => {
242
+ const onKeyDown = (event) => {
243
+ if (event.defaultPrevented) return;
244
+ const key = event.key;
245
+ if (key === "Escape") return void (event.preventDefault(), requestClose());
246
+ if (navigation && (key === "ArrowRight" || key === "ArrowLeft") && !zoomed) {
247
+ event.preventDefault();
248
+ return go(key === "ArrowRight" ? 1 : -1);
249
+ }
250
+ if (zoom && (key === "+" || key === "=")) return void (event.preventDefault(), zoomBy(ZOOM_STEP));
251
+ if (zoom && key === "-") return void (event.preventDefault(), zoomBy(1 / ZOOM_STEP));
252
+ if (zoom && key === "0") return void (event.preventDefault(), reset());
253
+ if (zoomed && key.startsWith("Arrow")) {
254
+ event.preventDefault();
255
+ const step = event.shiftKey ? 120 : 40;
256
+ const by = key === "ArrowLeft" ? [step, 0] : key === "ArrowRight" ? [-step, 0] : key === "ArrowUp" ? [0, step] : [0, -step];
257
+ return setTransform((current) => bounded({ ...current, x: current.x + by[0], y: current.y + by[1] }));
258
+ }
259
+ if (discardable && (key === "Delete" || key === "Backspace")) return void (event.preventDefault(), discard());
260
+ };
261
+ document.addEventListener("keydown", onKeyDown);
262
+ return () => document.removeEventListener("keydown", onKeyDown);
263
+ }, [requestClose, navigation, zoom, zoomed, go, zoomBy, reset, bounded, discardable, discard]);
264
+ useEffect(() => {
265
+ frameRef.current?.focus();
266
+ const { body } = document;
267
+ const overflow = body.style.overflow;
268
+ body.style.overflow = "hidden";
269
+ return () => {
270
+ body.style.overflow = overflow;
271
+ };
272
+ }, []);
273
+ useEffect(() => {
274
+ if (!thumbnails) return;
275
+ stripRef.current?.querySelector("[aria-current=true]")?.scrollIntoView({ block: "nearest", inline: "center" });
276
+ }, [thumbnails, index]);
277
+ if (!item) return null;
278
+ const drawn = flight ?? transform;
279
+ const style = { transform: `translate(${drawn.x}px, ${drawn.y}px) scale(${drawn.scale})` };
280
+ const counter = (labels.counter ?? "{index} of {total}").replace("{index}", String(position)).replace("{total}", String(remaining));
281
+ return createPortal(
282
+ /* @__PURE__ */ jsxs(
283
+ "div",
284
+ {
285
+ "data-enigma-image-viewer": "",
286
+ "data-state": phase === "measuring" || phase === "flying" ? "opening" : phase,
287
+ role: "dialog",
288
+ "aria-modal": "true",
289
+ "aria-label": labels.viewer ?? "Image viewer",
290
+ onPointerDown: (event) => {
291
+ if (event.target === event.currentTarget) requestClose();
292
+ },
293
+ children: [
294
+ /* @__PURE__ */ jsxs("div", { "data-enigma-image-bar": "", children: [
295
+ navigation && /* @__PURE__ */ jsx("span", { "data-enigma-image-counter": "", children: counter }),
296
+ !navigation && /* @__PURE__ */ jsx("span", { "data-enigma-image-counter": "" }),
297
+ zoom && /* @__PURE__ */ jsxs(Fragment, { children: [
298
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.zoomOut ?? "Zoom out", title: labels.zoomOut ?? "Zoom out", disabled: transform.scale <= limits.min, onClick: () => zoomBy(1 / ZOOM_STEP), children: /* @__PURE__ */ jsx(Minus, {}) }),
299
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.zoomIn ?? "Zoom in", title: labels.zoomIn ?? "Zoom in", disabled: transform.scale >= limits.max, onClick: () => zoomBy(ZOOM_STEP), children: /* @__PURE__ */ jsx(Plus, {}) })
300
+ ] }),
301
+ discardable && /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.discard ?? "Discard this image", title: labels.discard ?? "Discard this image", onClick: discard, children: /* @__PURE__ */ jsx(Trash, {}) }),
302
+ menu && /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(MenuButton, { label: labels.menu ?? "More", onPress: void 0 }), children: /* @__PURE__ */ jsx(ImageMenu, { item, index, options: menu, labels }) }),
303
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.close ?? "Close", title: labels.close ?? "Close", onClick: requestClose, children: /* @__PURE__ */ jsx(Close, {}) })
304
+ ] }),
305
+ /* @__PURE__ */ jsxs(
306
+ "div",
307
+ {
308
+ ref: frameRef,
309
+ "data-enigma-image-frame": "",
310
+ "data-zoom": zoom ? "" : void 0,
311
+ "data-flying": phase === "open" ? void 0 : phase === "measuring" ? "pending" : "moving",
312
+ "data-zoomed": zoomed ? "" : void 0,
313
+ "data-panning": panning ? "" : void 0,
314
+ "data-loading": loading ? "" : void 0,
315
+ tabIndex: -1,
316
+ onPointerDown,
317
+ onPointerMove,
318
+ onPointerUp: endPointer,
319
+ onPointerCancel: endPointer,
320
+ onDoubleClick: (event) => {
321
+ if (!zoom?.doubleClick || phase !== "open") return;
322
+ if (zoomed) return reset();
323
+ zoomBy(ZOOM_DOUBLE / transform.scale, { x: event.clientX, y: event.clientY });
324
+ },
325
+ children: [
326
+ /* @__PURE__ */ jsx(
327
+ "img",
328
+ {
329
+ ref: imageRef,
330
+ src: item.src,
331
+ alt: item.alt ?? "",
332
+ style,
333
+ draggable: false,
334
+ onLoad: () => setLoading(false),
335
+ onError: () => setLoading(false)
336
+ },
337
+ item.src
338
+ ),
339
+ loading && /* @__PURE__ */ jsx("span", { "data-enigma-image-spinner": "", role: "progressbar", "aria-label": labels.viewer ?? "Image viewer" }),
340
+ navigation && remaining > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
341
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "data-enigma-image-nav": "previous", "aria-label": labels.previous ?? "Previous image", title: labels.previous ?? "Previous image", onClick: () => go(-1), children: /* @__PURE__ */ jsx(ChevronLeft, {}) }),
342
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "data-enigma-image-nav": "next", "aria-label": labels.next ?? "Next image", title: labels.next ?? "Next image", onClick: () => go(1), children: /* @__PURE__ */ jsx(ChevronRight, {}) })
343
+ ] })
344
+ ]
345
+ }
346
+ ),
347
+ /* @__PURE__ */ jsxs("div", { "data-enigma-image-foot": "", children: [
348
+ (item.caption ?? caption) && /* @__PURE__ */ jsx("p", { "data-enigma-image-caption": "", children: item.caption ?? caption }),
349
+ thumbnails && remaining > 1 && /* @__PURE__ */ jsx("div", { ref: stripRef, "data-enigma-image-strip": "", role: "tablist", "aria-label": labels.thumbnails ?? "Images", children: items.map((entry, at) => discarded.has(at) ? null : /* @__PURE__ */ jsx(
350
+ "button",
351
+ {
352
+ type: "button",
353
+ role: "tab",
354
+ "data-enigma-image-thumb": "",
355
+ "aria-current": at === index,
356
+ "aria-selected": at === index,
357
+ "aria-label": entry.alt ?? `${at + 1}`,
358
+ onClick: () => showAt(at),
359
+ children: /* @__PURE__ */ jsx("img", { src: entry.thumbnail ?? entry.src, alt: "", loading: "lazy", draggable: false })
360
+ },
361
+ entry.src
362
+ )) })
363
+ ] })
364
+ ]
365
+ }
366
+ ),
367
+ document.body
368
+ );
369
+ }
370
+ function MenuButton({ label, onPress, expanded }) {
371
+ return /* @__PURE__ */ jsx(
372
+ "button",
373
+ {
374
+ type: "button",
375
+ "data-enigma-image-button": "",
376
+ "data-enigma-image-menu": "",
377
+ "aria-haspopup": "menu",
378
+ "aria-expanded": expanded ?? false,
379
+ "aria-label": label,
380
+ title: label,
381
+ onClick: onPress,
382
+ children: /* @__PURE__ */ jsx(Dots, {})
383
+ }
384
+ );
385
+ }
386
+
387
+ export { Download, ImageViewer, MenuButton, NewTab };