@patternmode/aperto 0.1.1

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.
package/dist/index.js ADDED
@@ -0,0 +1,1113 @@
1
+ "use client";
2
+
3
+ // src/Aperto/ApertoGroup.tsx
4
+ import { ChevronLeft, ChevronRight, X } from "lucide-react";
5
+ import {
6
+ useCallback as useCallback3,
7
+ useId as useId2,
8
+ useLayoutEffect,
9
+ useMemo as useMemo2,
10
+ useRef,
11
+ useState as useState3
12
+ } from "react";
13
+ import { flushSync } from "react-dom";
14
+
15
+ // src/aperto-content.tsx
16
+ import * as Dialog from "@radix-ui/react-dialog";
17
+ import {
18
+ motion,
19
+ useMotionValue,
20
+ useSpring,
21
+ useTransform
22
+ } from "motion/react";
23
+ import {
24
+ forwardRef,
25
+ useCallback,
26
+ useMemo,
27
+ useState
28
+ } from "react";
29
+
30
+ // src/context.ts
31
+ import { createContext, useContext } from "react";
32
+ var ApertoContext = createContext(null);
33
+ function useApertoContext() {
34
+ const ctx = useContext(ApertoContext);
35
+ if (!ctx) {
36
+ throw new Error("Aperto components must be used within <Aperto.Root>");
37
+ }
38
+ return ctx;
39
+ }
40
+
41
+ // src/physics.ts
42
+ var RESISTANCE_DIVISOR = 20;
43
+ var RESISTANCE_SCALE = 4;
44
+ var RESISTANCE_MIN = 0.1;
45
+ function calculateResistance(distance) {
46
+ return Math.max(
47
+ RESISTANCE_MIN,
48
+ 1 - Math.log(distance / RESISTANCE_DIVISOR + 1) / RESISTANCE_SCALE
49
+ );
50
+ }
51
+ var SCALE_INPUT = [0, 50, 100];
52
+ var SCALE_OUTPUT = [1, 0.98, 0.95];
53
+ var OPACITY_INPUT = [0, 80];
54
+ var OPACITY_OUTPUT = [1, 0.96];
55
+ var DEFAULT_DISTANCE_THRESHOLD = 100;
56
+ var DEFAULT_VELOCITY_THRESHOLD = 500;
57
+
58
+ // src/motion-tokens.ts
59
+ var durations = {
60
+ snappy: 0.22,
61
+ moderate: 0.4
62
+ };
63
+ var easings = {
64
+ customGentle: [0.25, 0.1, 0.12, 1],
65
+ snappy: [0.22, 1, 0.36, 1]
66
+ };
67
+ var springs = {
68
+ bouncy: { stiffness: 260, damping: 12, mass: 1 },
69
+ natural: { stiffness: 200, damping: 20, mass: 1 },
70
+ snappy: { stiffness: 400, damping: 28, mass: 0.8 },
71
+ stiff: { stiffness: 500, damping: 30, mass: 1 }
72
+ };
73
+
74
+ // src/presets.ts
75
+ var PRESETS = {
76
+ snappy: {
77
+ transition: { ease: easings.snappy, duration: durations.snappy },
78
+ drag: {
79
+ stiffness: springs.stiff.stiffness,
80
+ damping: springs.stiff.damping,
81
+ restDelta: 0.01
82
+ }
83
+ },
84
+ smooth: {
85
+ transition: { ease: easings.customGentle, duration: durations.moderate },
86
+ drag: {
87
+ stiffness: springs.natural.stiffness,
88
+ damping: springs.natural.damping,
89
+ restDelta: 0.01
90
+ }
91
+ },
92
+ bouncy: {
93
+ transition: {
94
+ type: "spring",
95
+ stiffness: springs.bouncy.stiffness,
96
+ damping: springs.bouncy.damping,
97
+ mass: springs.bouncy.mass
98
+ },
99
+ drag: {
100
+ stiffness: springs.snappy.stiffness,
101
+ damping: springs.snappy.damping,
102
+ restDelta: 0.01
103
+ }
104
+ },
105
+ reduced: {
106
+ transition: { ease: "linear", duration: 0.01 },
107
+ drag: { stiffness: 1e3, damping: 50, restDelta: 0.1 }
108
+ }
109
+ };
110
+ function resolvePreset(componentType, componentMotion, globalPreset, variants, reduceMotion) {
111
+ if (reduceMotion) {
112
+ return PRESETS.reduced;
113
+ }
114
+ if (componentMotion) {
115
+ return PRESETS[componentMotion];
116
+ }
117
+ if (variants?.[componentType]) {
118
+ return PRESETS[variants[componentType]];
119
+ }
120
+ return PRESETS[globalPreset];
121
+ }
122
+ function getDragSpring(preset) {
123
+ return preset.drag;
124
+ }
125
+
126
+ // src/aperto-content.tsx
127
+ import { jsx } from "react/jsx-runtime";
128
+ var ApertoContent = forwardRef(
129
+ ({
130
+ children,
131
+ className,
132
+ motion: motionOverride,
133
+ placement = "center",
134
+ sharedLayoutId,
135
+ style,
136
+ ...props
137
+ }, ref) => {
138
+ const ctx = useApertoContext();
139
+ const [isDragging, setIsDragging] = useState(false);
140
+ const resolved = resolvePreset(
141
+ "content",
142
+ motionOverride,
143
+ ctx.presetName,
144
+ ctx.variants,
145
+ ctx.reduceMotion
146
+ );
147
+ const contentLayoutId = sharedLayoutId === false ? void 0 : sharedLayoutId ?? `${ctx.layoutId}-shared`;
148
+ const dragSpring = getDragSpring(resolved);
149
+ const isDismissible = ctx.dismissible !== false;
150
+ const dismissConfig = typeof ctx.dismissible === "object" ? ctx.dismissible : {};
151
+ const distanceThreshold = dismissConfig.threshold ?? DEFAULT_DISTANCE_THRESHOLD;
152
+ const velocityThreshold = dismissConfig.velocity ?? DEFAULT_VELOCITY_THRESHOLD;
153
+ const dragX = useMotionValue(0);
154
+ const dragY = useMotionValue(0);
155
+ const springX = useSpring(dragX, dragSpring);
156
+ const springY = useSpring(dragY, dragSpring);
157
+ const distance = useTransform(
158
+ () => Math.hypot(springX.get(), springY.get())
159
+ );
160
+ const scaleMotion = useTransform(distance, SCALE_INPUT, SCALE_OUTPUT);
161
+ const opacityMotion = useTransform(distance, OPACITY_INPUT, OPACITY_OUTPUT);
162
+ const resistance = useTransform(distance, calculateResistance);
163
+ const handleDrag = useCallback(
164
+ (_event, info) => {
165
+ if (!isDragging) {
166
+ setIsDragging(true);
167
+ }
168
+ const r = resistance.get();
169
+ dragX.set(info.offset.x * r);
170
+ dragY.set(info.offset.y * r);
171
+ },
172
+ [dragX, dragY, isDragging, resistance]
173
+ );
174
+ const handleDragEnd = useCallback(
175
+ (_event, info) => {
176
+ setIsDragging(false);
177
+ const speed = Math.hypot(info.velocity.x, info.velocity.y);
178
+ const dist = Math.hypot(info.offset.x, info.offset.y);
179
+ if (speed > velocityThreshold || dist > distanceThreshold) {
180
+ ctx.onOpenChange(false);
181
+ return;
182
+ }
183
+ dragX.set(0);
184
+ dragY.set(0);
185
+ },
186
+ [ctx.onOpenChange, distanceThreshold, dragX, dragY, velocityThreshold]
187
+ );
188
+ const motionStyle = useMemo(
189
+ () => ({
190
+ cursor: isDragging ? "grabbing" : isDismissible ? "grab" : void 0,
191
+ opacity: opacityMotion,
192
+ scale: scaleMotion,
193
+ x: springX,
194
+ y: springY,
195
+ ...placement === "center" ? {
196
+ left: "50%",
197
+ top: "50%",
198
+ translate: "-50% -50%"
199
+ } : {},
200
+ ...style
201
+ }),
202
+ [
203
+ isDismissible,
204
+ isDragging,
205
+ opacityMotion,
206
+ placement,
207
+ scaleMotion,
208
+ springX,
209
+ springY,
210
+ style
211
+ ]
212
+ );
213
+ return /* @__PURE__ */ jsx(Dialog.Content, { asChild: true, forceMount: true, ...props, children: /* @__PURE__ */ jsx(
214
+ motion.div,
215
+ {
216
+ className,
217
+ "data-slot": "aperto-content",
218
+ drag: isDismissible,
219
+ dragConstraints: { bottom: 0, left: 0, right: 0, top: 0 },
220
+ dragElastic: 0,
221
+ dragMomentum: false,
222
+ dragSnapToOrigin: true,
223
+ layout: contentLayoutId ? true : void 0,
224
+ layoutId: contentLayoutId,
225
+ onDrag: isDismissible ? handleDrag : void 0,
226
+ onDragEnd: isDismissible ? handleDragEnd : void 0,
227
+ ref,
228
+ style: motionStyle,
229
+ transition: resolved.transition,
230
+ children
231
+ }
232
+ ) });
233
+ }
234
+ );
235
+ ApertoContent.displayName = "ApertoContent";
236
+
237
+ // src/aperto-description.tsx
238
+ import * as Dialog2 from "@radix-ui/react-dialog";
239
+ import { forwardRef as forwardRef2 } from "react";
240
+ import { jsx as jsx2 } from "react/jsx-runtime";
241
+ var ApertoDescription = forwardRef2((props, ref) => {
242
+ return /* @__PURE__ */ jsx2(Dialog2.Description, { "data-slot": "aperto-description", ref, ...props });
243
+ });
244
+ ApertoDescription.displayName = "ApertoDescription";
245
+
246
+ // src/aperto-group-context.ts
247
+ import { createContext as createContext2, useContext as useContext2 } from "react";
248
+ var ApertoGroupContext = createContext2(
249
+ null
250
+ );
251
+ function useApertoGroup() {
252
+ const ctx = useContext2(ApertoGroupContext);
253
+ if (!ctx) {
254
+ throw new Error("Aperto.Thumbnail must be used within <Aperto.Group>");
255
+ }
256
+ return ctx;
257
+ }
258
+
259
+ // src/aperto-overlay.tsx
260
+ import * as Dialog3 from "@radix-ui/react-dialog";
261
+ import { motion as motion2 } from "motion/react";
262
+ import { forwardRef as forwardRef3 } from "react";
263
+ import { jsx as jsx3 } from "react/jsx-runtime";
264
+ var OVERLAY_TRANSITION = {
265
+ duration: 0.3,
266
+ ease: easings.customGentle
267
+ };
268
+ var OVERLAY_TRANSITION_REDUCED = {
269
+ duration: 0.01,
270
+ ease: "linear"
271
+ };
272
+ var ApertoOverlay = forwardRef3(
273
+ ({ className, fadeOut, style }, ref) => {
274
+ const ctx = useApertoContext();
275
+ const transition = ctx.reduceMotion ? OVERLAY_TRANSITION_REDUCED : OVERLAY_TRANSITION;
276
+ return /* @__PURE__ */ jsx3(Dialog3.Overlay, { asChild: true, forceMount: true, children: /* @__PURE__ */ jsx3(
277
+ motion2.div,
278
+ {
279
+ animate: { opacity: fadeOut ? 0 : 1 },
280
+ className,
281
+ "data-slot": "aperto-overlay",
282
+ exit: { opacity: 0 },
283
+ initial: { opacity: 0 },
284
+ ref,
285
+ style,
286
+ transition
287
+ }
288
+ ) });
289
+ }
290
+ );
291
+ ApertoOverlay.displayName = "ApertoOverlay";
292
+
293
+ // src/aperto-portal.tsx
294
+ import * as Dialog4 from "@radix-ui/react-dialog";
295
+ import { AnimatePresence } from "motion/react";
296
+ import { jsx as jsx4 } from "react/jsx-runtime";
297
+ function ApertoPortal({ children, container }) {
298
+ const { open } = useApertoContext();
299
+ return /* @__PURE__ */ jsx4(AnimatePresence, { children: open && /* @__PURE__ */ jsx4(Dialog4.Portal, { container, forceMount: true, children }) });
300
+ }
301
+
302
+ // src/aperto-root.tsx
303
+ import * as Dialog5 from "@radix-ui/react-dialog";
304
+ import { LayoutGroup, useReducedMotion } from "motion/react";
305
+ import {
306
+ useCallback as useCallback2,
307
+ useId,
308
+ useState as useState2
309
+ } from "react";
310
+ import { jsx as jsx5 } from "react/jsx-runtime";
311
+ function ApertoRoot({
312
+ children,
313
+ dismissible = true,
314
+ layoutId: layoutIdProp,
315
+ motion: motionProp = "smooth",
316
+ open: controlledOpen,
317
+ onOpenChange: controlledOnOpenChange,
318
+ reduceMotion: reduceMotionProp,
319
+ ...dialogProps
320
+ }) {
321
+ const generatedId = useId();
322
+ const layoutId = layoutIdProp ?? `aperto-${generatedId}`;
323
+ const systemReducedMotion = useReducedMotion() ?? false;
324
+ const shouldReduce = reduceMotionProp ?? systemReducedMotion;
325
+ const [internalOpen, setInternalOpen] = useState2(false);
326
+ const isControlled = controlledOpen !== void 0;
327
+ const open = isControlled ? controlledOpen : internalOpen;
328
+ const onOpenChange = useCallback2(
329
+ (nextOpen) => {
330
+ if (!isControlled) {
331
+ setInternalOpen(nextOpen);
332
+ }
333
+ controlledOnOpenChange?.(nextOpen);
334
+ },
335
+ [isControlled, controlledOnOpenChange]
336
+ );
337
+ const globalPresetName = typeof motionProp === "string" ? motionProp : "smooth";
338
+ const variants = typeof motionProp === "object" ? motionProp : void 0;
339
+ const presetName = shouldReduce ? "reduced" : globalPresetName;
340
+ const preset = PRESETS[presetName];
341
+ return /* @__PURE__ */ jsx5(
342
+ ApertoContext.Provider,
343
+ {
344
+ value: {
345
+ dismissible,
346
+ layoutId,
347
+ onOpenChange,
348
+ open,
349
+ preset,
350
+ presetName,
351
+ reduceMotion: shouldReduce,
352
+ variants
353
+ },
354
+ children: /* @__PURE__ */ jsx5(Dialog5.Root, { onOpenChange, open, ...dialogProps, children: /* @__PURE__ */ jsx5(LayoutGroup, { id: layoutId, children }) })
355
+ }
356
+ );
357
+ }
358
+
359
+ // src/aperto-title.tsx
360
+ import * as Dialog6 from "@radix-ui/react-dialog";
361
+ import { forwardRef as forwardRef4 } from "react";
362
+ import { jsx as jsx6 } from "react/jsx-runtime";
363
+ var ApertoTitle = forwardRef4((props, ref) => {
364
+ return /* @__PURE__ */ jsx6(Dialog6.Title, { "data-slot": "aperto-title", ref, ...props });
365
+ });
366
+ ApertoTitle.displayName = "ApertoTitle";
367
+
368
+ // src/expanded-media-stage.tsx
369
+ import {
370
+ AnimatePresence as AnimatePresence2,
371
+ motion as motion3
372
+ } from "motion/react";
373
+
374
+ // src/media-rendering.tsx
375
+ import { jsx as jsx7 } from "react/jsx-runtime";
376
+ function getMediaLabel(item) {
377
+ return item.title ?? item.alt ?? "media";
378
+ }
379
+ function getMediaKey(item, index) {
380
+ return item.id ?? `${item.type}:${item.src}:${index}`;
381
+ }
382
+ function getDescriptionProps(item) {
383
+ return item.description ? {} : { "aria-describedby": void 0 };
384
+ }
385
+ function renderDefaultImage(props) {
386
+ const { item: _item, variant: _variant, ...imageProps } = props;
387
+ return /* @__PURE__ */ jsx7("img", { ...imageProps, alt: imageProps.alt ?? "" });
388
+ }
389
+ function renderDefaultVideo(props) {
390
+ const { item: _item, variant, ...videoProps } = props;
391
+ if (variant === "thumbnail") {
392
+ return /* @__PURE__ */ jsx7("img", { alt: videoProps["aria-label"] ?? "", src: props.item.thumbnailSrc });
393
+ }
394
+ return /* @__PURE__ */ jsx7("video", { ...videoProps });
395
+ }
396
+ function renderThumbnail(item, renderImage, renderVideo) {
397
+ if (item.type === "image") {
398
+ const imageProps = {
399
+ alt: "",
400
+ height: item.height,
401
+ item,
402
+ src: item.thumbnailSrc ?? item.src,
403
+ variant: "thumbnail",
404
+ width: item.width
405
+ };
406
+ return (renderImage ?? renderDefaultImage)(imageProps);
407
+ }
408
+ const videoProps = {
409
+ "aria-label": item.alt ?? item.title ?? "Video thumbnail",
410
+ height: item.height,
411
+ item,
412
+ poster: item.poster,
413
+ src: item.thumbnailSrc,
414
+ variant: "thumbnail",
415
+ width: item.width
416
+ };
417
+ return (renderVideo ?? renderDefaultVideo)(videoProps);
418
+ }
419
+ function renderExpandedMedia(item, renderImage, renderVideo) {
420
+ if (item.type === "image") {
421
+ const imageProps = {
422
+ alt: item.alt,
423
+ height: item.height,
424
+ item,
425
+ src: item.src,
426
+ variant: "expanded",
427
+ width: item.width
428
+ };
429
+ return (renderImage ?? renderDefaultImage)(imageProps);
430
+ }
431
+ const videoProps = {
432
+ "aria-label": item.alt ?? item.title ?? "Video",
433
+ controls: true,
434
+ height: item.height,
435
+ item,
436
+ poster: item.poster,
437
+ src: item.src,
438
+ variant: "expanded",
439
+ width: item.width
440
+ };
441
+ return (renderVideo ?? renderDefaultVideo)(videoProps);
442
+ }
443
+ function renderTransitionMedia(item) {
444
+ if (item.type === "image") {
445
+ return /* @__PURE__ */ jsx7("img", { alt: "", src: item.src });
446
+ }
447
+ return /* @__PURE__ */ jsx7("img", { alt: "", src: item.poster ?? item.thumbnailSrc });
448
+ }
449
+
450
+ // src/expanded-media-stage.tsx
451
+ import { jsx as jsx8 } from "react/jsx-runtime";
452
+ var NAVIGATION_MOTION_PRESETS = {
453
+ float: {
454
+ offset: 96,
455
+ scale: 0.97,
456
+ transition: {
457
+ opacity: { duration: 0.34, ease: [0.45, 0, 0.2, 1] },
458
+ scale: { damping: 26, mass: 1.15, stiffness: 80, type: "spring" },
459
+ x: { damping: 26, mass: 1.15, stiffness: 80, type: "spring" }
460
+ }
461
+ },
462
+ glide: {
463
+ offset: 58,
464
+ scale: 0.984,
465
+ transition: {
466
+ opacity: { duration: 0.24, ease: [0.4, 0, 0.2, 1] },
467
+ scale: { damping: 28, mass: 0.95, stiffness: 150, type: "spring" },
468
+ x: { damping: 28, mass: 0.95, stiffness: 150, type: "spring" }
469
+ }
470
+ },
471
+ snap: {
472
+ offset: 38,
473
+ scale: 0.996,
474
+ transition: {
475
+ opacity: { duration: 0.12, ease: [0.2, 0, 0, 1] },
476
+ scale: { damping: 34, mass: 0.7, stiffness: 420, type: "spring" },
477
+ x: { damping: 34, mass: 0.7, stiffness: 420, type: "spring" }
478
+ }
479
+ }
480
+ };
481
+ var REDUCED_EXPANDED_MEDIA_TRANSITION = {
482
+ duration: 0.12,
483
+ ease: "linear"
484
+ };
485
+ var expandedMediaVariants = {
486
+ center: {
487
+ opacity: 1,
488
+ scale: 1,
489
+ x: 0
490
+ },
491
+ enter: ({
492
+ direction,
493
+ offset,
494
+ scale
495
+ }) => ({
496
+ opacity: 0,
497
+ scale: direction === 0 ? 1 : scale,
498
+ x: direction * offset
499
+ }),
500
+ exit: ({
501
+ direction,
502
+ offset,
503
+ scale
504
+ }) => ({
505
+ opacity: 0,
506
+ scale: direction === 0 ? 1 : scale,
507
+ x: direction * -offset
508
+ })
509
+ };
510
+ var reducedExpandedMediaVariants = {
511
+ center: {
512
+ opacity: 1,
513
+ x: 0
514
+ },
515
+ enter: {
516
+ opacity: 0,
517
+ x: 0
518
+ },
519
+ exit: {
520
+ opacity: 0,
521
+ x: 0
522
+ }
523
+ };
524
+ function ApertoExpandedMediaStage({
525
+ direction,
526
+ index,
527
+ item,
528
+ navigationMotion,
529
+ renderImage,
530
+ renderVideo
531
+ }) {
532
+ const ctx = useApertoContext();
533
+ const preset = NAVIGATION_MOTION_PRESETS[navigationMotion];
534
+ const transition = ctx.reduceMotion ? REDUCED_EXPANDED_MEDIA_TRANSITION : preset.transition;
535
+ const variants = ctx.reduceMotion ? reducedExpandedMediaVariants : expandedMediaVariants;
536
+ const animationCustom = {
537
+ direction,
538
+ offset: preset.offset,
539
+ scale: preset.scale
540
+ };
541
+ return /* @__PURE__ */ jsx8(AnimatePresence2, { custom: animationCustom, initial: false, mode: "sync", children: /* @__PURE__ */ jsx8(
542
+ motion3.div,
543
+ {
544
+ animate: "center",
545
+ custom: animationCustom,
546
+ "data-navigation-direction": direction,
547
+ "data-navigation-motion": navigationMotion,
548
+ "data-slot": "aperto-media-stage",
549
+ exit: "exit",
550
+ initial: "enter",
551
+ transition,
552
+ variants,
553
+ children: renderExpandedMedia(item, renderImage, renderVideo)
554
+ },
555
+ getMediaKey(item, index)
556
+ ) });
557
+ }
558
+
559
+ // src/media-transition.tsx
560
+ import {
561
+ motion as motion4
562
+ } from "motion/react";
563
+ import { useEffect } from "react";
564
+ import { jsx as jsx9 } from "react/jsx-runtime";
565
+ function rectFromElement(element) {
566
+ if (!element) {
567
+ return null;
568
+ }
569
+ const rect = element.getBoundingClientRect();
570
+ return {
571
+ height: rect.height,
572
+ left: rect.left,
573
+ top: rect.top,
574
+ width: rect.width
575
+ };
576
+ }
577
+ function rectTarget(rect) {
578
+ return {
579
+ height: rect.height,
580
+ left: rect.left,
581
+ top: rect.top,
582
+ width: rect.width
583
+ };
584
+ }
585
+ function transitionDurationMs(transition) {
586
+ return typeof transition.duration === "number" ? transition.duration * 1e3 : 450;
587
+ }
588
+ function ApertoMediaTransitionClone({
589
+ onComplete,
590
+ transition
591
+ }) {
592
+ const ctx = useApertoContext();
593
+ useEffect(() => {
594
+ if (!transition?.to) {
595
+ return;
596
+ }
597
+ const timer = setTimeout(
598
+ onComplete,
599
+ transitionDurationMs(ctx.preset.transition)
600
+ );
601
+ return () => clearTimeout(timer);
602
+ }, [ctx.preset.transition, onComplete, transition]);
603
+ if (!transition?.to) {
604
+ return null;
605
+ }
606
+ return /* @__PURE__ */ jsx9(
607
+ motion4.div,
608
+ {
609
+ animate: rectTarget(transition.to),
610
+ "data-slot": "aperto-transition-media",
611
+ initial: rectTarget(transition.from),
612
+ style: {
613
+ borderRadius: "var(--aperto-radius, 0.5rem)",
614
+ overflow: "hidden",
615
+ pointerEvents: "none",
616
+ position: "fixed",
617
+ willChange: "left, top, width, height",
618
+ zIndex: 1002
619
+ },
620
+ transition: ctx.preset.transition,
621
+ children: renderTransitionMedia(transition.item)
622
+ }
623
+ );
624
+ }
625
+
626
+ // src/Aperto/ApertoKeyboard.ts
627
+ function shouldIgnoreKeyboardNavigationTarget(target) {
628
+ if (!(target instanceof Element)) {
629
+ return false;
630
+ }
631
+ return target.isContentEditable || Boolean(
632
+ target.closest(
633
+ 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], audio, video'
634
+ )
635
+ );
636
+ }
637
+
638
+ // src/Aperto/ApertoGroup.tsx
639
+ import { Fragment, jsx as jsx10, jsxs } from "react/jsx-runtime";
640
+ function ApertoGroup({
641
+ children,
642
+ classNames,
643
+ dismissible,
644
+ index: controlledIndex,
645
+ initialIndex = 0,
646
+ media,
647
+ motion: motionProp,
648
+ navigationMotion = "glide",
649
+ onIndexChange,
650
+ renderImage,
651
+ renderVideo
652
+ }) {
653
+ const generatedId = useId2();
654
+ const [internalIndex, setInternalIndex] = useState3(initialIndex);
655
+ const [open, setOpen] = useState3(false);
656
+ const [closing, setClosing] = useState3(false);
657
+ const [mediaTransition, setMediaTransition] = useState3(null);
658
+ const [navigationDirection, setNavigationDirection] = useState3(0);
659
+ const isControlled = controlledIndex !== void 0;
660
+ const index = isControlled ? controlledIndex : internalIndex;
661
+ const activeMedia = media[index] ?? media[0];
662
+ const [layoutSourceIndex, setLayoutSourceIndex] = useState3(index);
663
+ const expandedMediaRef = useRef(null);
664
+ const thumbnailRefs = useRef(/* @__PURE__ */ new Map());
665
+ const sharedLayoutIdForIndex = useCallback3(
666
+ (nextIndex) => `aperto-group-${generatedId}-${nextIndex}-shared`,
667
+ [generatedId]
668
+ );
669
+ const sharedLayoutId = sharedLayoutIdForIndex(layoutSourceIndex);
670
+ const measureOpeningTarget = useCallback3((node) => {
671
+ const targetRect = rectFromElement(node);
672
+ if (!targetRect) {
673
+ return;
674
+ }
675
+ setMediaTransition((current) => {
676
+ if (current?.phase !== "opening" || current.to) {
677
+ return current;
678
+ }
679
+ return { ...current, to: targetRect };
680
+ });
681
+ }, []);
682
+ const setExpandedMediaNode = useCallback3(
683
+ (node) => {
684
+ expandedMediaRef.current = node;
685
+ if (node) {
686
+ measureOpeningTarget(node);
687
+ }
688
+ },
689
+ [measureOpeningTarget]
690
+ );
691
+ const registerThumbnail = useCallback3(
692
+ (thumbIndex, node) => {
693
+ if (node) {
694
+ thumbnailRefs.current.set(thumbIndex, node);
695
+ return;
696
+ }
697
+ thumbnailRefs.current.delete(thumbIndex);
698
+ },
699
+ []
700
+ );
701
+ useLayoutEffect(() => {
702
+ if (mediaTransition?.phase !== "opening" || mediaTransition.to || !expandedMediaRef.current) {
703
+ return;
704
+ }
705
+ measureOpeningTarget(expandedMediaRef.current);
706
+ }, [measureOpeningTarget, mediaTransition]);
707
+ const setIndex = useCallback3(
708
+ (nextIndex) => {
709
+ if (!isControlled) {
710
+ setInternalIndex(nextIndex);
711
+ }
712
+ onIndexChange?.(nextIndex);
713
+ },
714
+ [isControlled, onIndexChange]
715
+ );
716
+ const openAtIndex = useCallback3(
717
+ (thumbIndex) => {
718
+ const sourceRect = rectFromElement(
719
+ thumbnailRefs.current.get(thumbIndex) ?? null
720
+ );
721
+ const item = media[thumbIndex];
722
+ flushSync(() => {
723
+ setClosing(false);
724
+ setMediaTransition(
725
+ sourceRect && item ? { from: sourceRect, item, phase: "opening" } : null
726
+ );
727
+ setNavigationDirection(0);
728
+ setLayoutSourceIndex(thumbIndex);
729
+ setIndex(thumbIndex);
730
+ setOpen(true);
731
+ });
732
+ },
733
+ [media, setIndex]
734
+ );
735
+ const startClose = useCallback3(() => {
736
+ if (!open || closing) {
737
+ return;
738
+ }
739
+ const sourceRect = rectFromElement(expandedMediaRef.current);
740
+ const targetRect = rectFromElement(
741
+ thumbnailRefs.current.get(index) ?? null
742
+ );
743
+ if (!activeMedia || !sourceRect || !targetRect) {
744
+ setMediaTransition(null);
745
+ setClosing(false);
746
+ setOpen(false);
747
+ return;
748
+ }
749
+ flushSync(() => {
750
+ setClosing(true);
751
+ setMediaTransition({
752
+ from: sourceRect,
753
+ item: activeMedia,
754
+ phase: "closing",
755
+ to: targetRect
756
+ });
757
+ });
758
+ }, [activeMedia, closing, index, open]);
759
+ const handleMediaTransitionComplete = useCallback3(() => {
760
+ if (mediaTransition?.phase === "closing") {
761
+ setOpen(false);
762
+ setClosing(false);
763
+ }
764
+ setMediaTransition(null);
765
+ }, [mediaTransition?.phase]);
766
+ const handleOpenChange = useCallback3(
767
+ (nextOpen) => {
768
+ if (nextOpen) {
769
+ setClosing(false);
770
+ setOpen(true);
771
+ return;
772
+ }
773
+ startClose();
774
+ },
775
+ [startClose]
776
+ );
777
+ const value = useMemo2(
778
+ () => ({
779
+ classNames,
780
+ index,
781
+ media,
782
+ open,
783
+ openAtIndex,
784
+ registerThumbnail,
785
+ renderImage,
786
+ renderVideo,
787
+ setIndex,
788
+ sharedLayoutId,
789
+ sharedLayoutIdForIndex
790
+ }),
791
+ [
792
+ classNames,
793
+ index,
794
+ media,
795
+ open,
796
+ openAtIndex,
797
+ registerThumbnail,
798
+ renderImage,
799
+ renderVideo,
800
+ setIndex,
801
+ sharedLayoutId,
802
+ sharedLayoutIdForIndex
803
+ ]
804
+ );
805
+ const hasNavigation = media.length > 1;
806
+ const navigateToIndex = useCallback3(
807
+ (nextIndex, direction) => {
808
+ setNavigationDirection(direction);
809
+ setLayoutSourceIndex(nextIndex);
810
+ setIndex(nextIndex);
811
+ },
812
+ [setIndex]
813
+ );
814
+ const goToPrevious = useCallback3(() => {
815
+ navigateToIndex((index - 1 + media.length) % media.length, -1);
816
+ }, [index, media.length, navigateToIndex]);
817
+ const goToNext = useCallback3(() => {
818
+ navigateToIndex((index + 1) % media.length, 1);
819
+ }, [index, media.length, navigateToIndex]);
820
+ const handleContentKeyDown = useCallback3(
821
+ (event) => {
822
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || shouldIgnoreKeyboardNavigationTarget(event.target)) {
823
+ return;
824
+ }
825
+ if (event.key === "ArrowLeft") {
826
+ event.preventDefault();
827
+ goToPrevious();
828
+ }
829
+ if (event.key === "ArrowRight") {
830
+ event.preventDefault();
831
+ goToNext();
832
+ }
833
+ },
834
+ [goToNext, goToPrevious]
835
+ );
836
+ return /* @__PURE__ */ jsx10(ApertoGroupContext.Provider, { value, children: /* @__PURE__ */ jsxs(
837
+ ApertoRoot,
838
+ {
839
+ dismissible,
840
+ motion: motionProp,
841
+ onOpenChange: handleOpenChange,
842
+ open,
843
+ children: [
844
+ children,
845
+ activeMedia ? /* @__PURE__ */ jsxs(ApertoPortal, { children: [
846
+ /* @__PURE__ */ jsx10(ApertoOverlay, { className: classNames?.overlay, fadeOut: closing }),
847
+ /* @__PURE__ */ jsxs(
848
+ ApertoContent,
849
+ {
850
+ className: classNames?.content,
851
+ "data-aperto-transition": mediaTransition?.phase,
852
+ onKeyDown: hasNavigation ? handleContentKeyDown : void 0,
853
+ sharedLayoutId: false,
854
+ ...getDescriptionProps(activeMedia),
855
+ children: [
856
+ /* @__PURE__ */ jsxs("div", { "data-slot": "aperto-media", ref: setExpandedMediaNode, children: [
857
+ /* @__PURE__ */ jsx10(
858
+ ApertoExpandedMediaStage,
859
+ {
860
+ direction: navigationDirection,
861
+ index,
862
+ item: activeMedia,
863
+ navigationMotion,
864
+ renderImage,
865
+ renderVideo
866
+ }
867
+ ),
868
+ hasNavigation ? /* @__PURE__ */ jsxs(Fragment, { children: [
869
+ /* @__PURE__ */ jsx10(
870
+ "button",
871
+ {
872
+ "aria-label": "Previous media",
873
+ className: classNames?.previousButton,
874
+ "data-slot": "aperto-previous-button",
875
+ onClick: goToPrevious,
876
+ type: "button",
877
+ children: /* @__PURE__ */ jsx10(
878
+ ChevronLeft,
879
+ {
880
+ "aria-hidden": "true",
881
+ size: 18,
882
+ strokeWidth: 2
883
+ }
884
+ )
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx10(
888
+ "button",
889
+ {
890
+ "aria-label": "Next media",
891
+ className: classNames?.nextButton,
892
+ "data-slot": "aperto-next-button",
893
+ onClick: goToNext,
894
+ type: "button",
895
+ children: /* @__PURE__ */ jsx10(
896
+ ChevronRight,
897
+ {
898
+ "aria-hidden": "true",
899
+ size: 18,
900
+ strokeWidth: 2
901
+ }
902
+ )
903
+ }
904
+ )
905
+ ] }) : null
906
+ ] }),
907
+ /* @__PURE__ */ jsx10(ApertoTitle, { children: activeMedia.title ?? getMediaLabel(activeMedia) }),
908
+ activeMedia.description ? /* @__PURE__ */ jsx10(ApertoDescription, { children: activeMedia.description }) : null,
909
+ hasNavigation ? /* @__PURE__ */ jsxs("div", { className: classNames?.counter, "data-slot": "aperto-counter", children: [
910
+ index + 1,
911
+ " / ",
912
+ media.length
913
+ ] }) : null,
914
+ /* @__PURE__ */ jsx10(
915
+ "button",
916
+ {
917
+ "aria-label": "Close",
918
+ className: classNames?.closeButton,
919
+ "data-slot": "aperto-close",
920
+ onClick: startClose,
921
+ type: "button",
922
+ children: /* @__PURE__ */ jsx10(X, { "aria-hidden": "true", size: 17, strokeWidth: 2 })
923
+ }
924
+ )
925
+ ]
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx10(
929
+ ApertoMediaTransitionClone,
930
+ {
931
+ onComplete: handleMediaTransitionComplete,
932
+ transition: mediaTransition
933
+ }
934
+ )
935
+ ] }) : null
936
+ ]
937
+ }
938
+ ) });
939
+ }
940
+
941
+ // src/aperto-close.tsx
942
+ import * as Dialog7 from "@radix-ui/react-dialog";
943
+ import { X as X2 } from "lucide-react";
944
+ import { forwardRef as forwardRef5 } from "react";
945
+ import { jsx as jsx11 } from "react/jsx-runtime";
946
+ var ApertoClose = forwardRef5(({ children, ...props }, ref) => {
947
+ return /* @__PURE__ */ jsx11(Dialog7.Close, { "data-slot": "aperto-close", ref, ...props, children: children ?? /* @__PURE__ */ jsx11(X2, { "aria-hidden": "true", size: 17, strokeWidth: 2 }) });
948
+ });
949
+ ApertoClose.displayName = "ApertoClose";
950
+
951
+ // src/aperto-trigger.tsx
952
+ import * as Dialog8 from "@radix-ui/react-dialog";
953
+ import { motion as motion5 } from "motion/react";
954
+ import {
955
+ forwardRef as forwardRef6,
956
+ useCallback as useCallback4,
957
+ useEffect as useEffect2,
958
+ useState as useState4
959
+ } from "react";
960
+ import { jsx as jsx12 } from "react/jsx-runtime";
961
+ var TRIGGER_OPEN_Z_INDEX = 1e3;
962
+ var ApertoTrigger = forwardRef6(
963
+ ({ active, children, motion: motionOverride, sharedLayoutId, ...props }, ref) => {
964
+ const ctx = useApertoContext();
965
+ const shouldRaise = ctx.open && (active ?? true);
966
+ const [zIndex, setZIndex] = useState4(
967
+ shouldRaise ? TRIGGER_OPEN_Z_INDEX : 0
968
+ );
969
+ useEffect2(() => {
970
+ if (shouldRaise) {
971
+ setZIndex(TRIGGER_OPEN_Z_INDEX);
972
+ } else if (ctx.open) {
973
+ setZIndex(0);
974
+ }
975
+ }, [ctx.open, shouldRaise]);
976
+ const handleLayoutAnimationComplete = useCallback4(() => {
977
+ if (!shouldRaise) {
978
+ setZIndex(0);
979
+ }
980
+ }, [shouldRaise]);
981
+ const resolved = resolvePreset(
982
+ "trigger",
983
+ motionOverride,
984
+ ctx.presetName,
985
+ ctx.variants,
986
+ ctx.reduceMotion
987
+ );
988
+ const layoutId = sharedLayoutId === false ? void 0 : sharedLayoutId ?? `${ctx.layoutId}-shared`;
989
+ return /* @__PURE__ */ jsx12(Dialog8.Trigger, { asChild: true, ref, ...props, children: /* @__PURE__ */ jsx12(
990
+ motion5.button,
991
+ {
992
+ "data-aperto-active": shouldRaise ? "" : void 0,
993
+ "data-slot": "aperto-trigger",
994
+ layout: true,
995
+ layoutCrossfade: false,
996
+ layoutId,
997
+ onLayoutAnimationComplete: handleLayoutAnimationComplete,
998
+ style: { zIndex },
999
+ transition: resolved.transition,
1000
+ type: "button",
1001
+ children
1002
+ },
1003
+ layoutId ?? "unshared"
1004
+ ) });
1005
+ }
1006
+ );
1007
+ ApertoTrigger.displayName = "ApertoTrigger";
1008
+
1009
+ // src/Aperto/ApertoSingle.tsx
1010
+ import { jsx as jsx13, jsxs as jsxs2 } from "react/jsx-runtime";
1011
+ function ApertoSingle({
1012
+ classNames,
1013
+ dismissible,
1014
+ media,
1015
+ renderImage,
1016
+ renderVideo
1017
+ }) {
1018
+ const label = getMediaLabel(media);
1019
+ const title = media.title ?? label;
1020
+ return /* @__PURE__ */ jsxs2(ApertoRoot, { dismissible, children: [
1021
+ /* @__PURE__ */ jsx13(
1022
+ ApertoTrigger,
1023
+ {
1024
+ "aria-label": `Open ${label}`,
1025
+ className: classNames?.thumbnail,
1026
+ children: renderThumbnail(media, renderImage, renderVideo)
1027
+ }
1028
+ ),
1029
+ /* @__PURE__ */ jsxs2(ApertoPortal, { children: [
1030
+ /* @__PURE__ */ jsx13(ApertoOverlay, { className: classNames?.overlay }),
1031
+ /* @__PURE__ */ jsxs2(
1032
+ ApertoContent,
1033
+ {
1034
+ className: classNames?.content,
1035
+ ...getDescriptionProps(media),
1036
+ children: [
1037
+ renderExpandedMedia(media, renderImage, renderVideo),
1038
+ /* @__PURE__ */ jsx13(ApertoTitle, { children: title }),
1039
+ media.description ? /* @__PURE__ */ jsx13(ApertoDescription, { children: media.description }) : null,
1040
+ /* @__PURE__ */ jsx13(
1041
+ ApertoClose,
1042
+ {
1043
+ "aria-label": "Close",
1044
+ className: classNames?.closeButton,
1045
+ type: "button"
1046
+ }
1047
+ )
1048
+ ]
1049
+ }
1050
+ )
1051
+ ] })
1052
+ ] });
1053
+ }
1054
+
1055
+ // src/Aperto/ApertoThumbnail.tsx
1056
+ import { jsx as jsx14 } from "react/jsx-runtime";
1057
+ function ApertoThumbnail({
1058
+ children,
1059
+ className,
1060
+ index
1061
+ }) {
1062
+ const group = useApertoGroup();
1063
+ const media = group.media[index];
1064
+ if (!media) {
1065
+ return null;
1066
+ }
1067
+ return /* @__PURE__ */ jsx14(
1068
+ ApertoTrigger,
1069
+ {
1070
+ active: group.index === index,
1071
+ "aria-label": `Open ${getMediaLabel(media)}`,
1072
+ className: className ?? group.classNames?.thumbnail,
1073
+ onClick: () => group.openAtIndex(index),
1074
+ ref: (node) => group.registerThumbnail(index, node),
1075
+ sharedLayoutId: false,
1076
+ children: children ?? renderThumbnail(media, group.renderImage, group.renderVideo)
1077
+ }
1078
+ );
1079
+ }
1080
+
1081
+ // src/index.ts
1082
+ var ApertoPrimitive = {
1083
+ Close: ApertoClose,
1084
+ Content: ApertoContent,
1085
+ Description: ApertoDescription,
1086
+ Overlay: ApertoOverlay,
1087
+ Portal: ApertoPortal,
1088
+ Root: ApertoRoot,
1089
+ Title: ApertoTitle,
1090
+ Trigger: ApertoTrigger
1091
+ };
1092
+ var Aperto = Object.assign(ApertoSingle, {
1093
+ Group: ApertoGroup,
1094
+ Primitive: ApertoPrimitive,
1095
+ Thumbnail: ApertoThumbnail
1096
+ });
1097
+ export {
1098
+ Aperto,
1099
+ ApertoClose,
1100
+ ApertoContent,
1101
+ ApertoDescription,
1102
+ ApertoGroup,
1103
+ ApertoOverlay,
1104
+ ApertoPortal,
1105
+ ApertoPrimitive,
1106
+ ApertoRoot,
1107
+ ApertoThumbnail,
1108
+ ApertoTitle,
1109
+ ApertoTrigger,
1110
+ PRESETS,
1111
+ resolvePreset
1112
+ };
1113
+ //# sourceMappingURL=index.js.map