r3f-motion 1.0.6 → 1.0.7
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/cjs/index.js +94 -17
- package/dist/es/components/Carousel/index.mjs +96 -19
- package/dist/es/index.mjs +1 -1
- package/dist/index.d.ts +13 -4
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -1134,7 +1134,15 @@ const VELOCITY_PROJECTION = 0.2;
|
|
|
1134
1134
|
const DRAG_THRESHOLD_RATIO = 0.25;
|
|
1135
1135
|
const FLICK_VELOCITY = 1.0;
|
|
1136
1136
|
const mod = (n, m) => ((n % m) + m) % m;
|
|
1137
|
-
|
|
1137
|
+
let isDragging = false;
|
|
1138
|
+
const CarouselSlotContext = react.createContext(null);
|
|
1139
|
+
const useCarouselSlot = () => {
|
|
1140
|
+
const ctx = react.useContext(CarouselSlotContext);
|
|
1141
|
+
if (!ctx)
|
|
1142
|
+
throw new Error('useCarouselSlot must be used inside <Carousel>');
|
|
1143
|
+
return ctx;
|
|
1144
|
+
};
|
|
1145
|
+
const CarouselSlot = react.memo(({ item, itemWidth, slotRef, visible }) => {
|
|
1138
1146
|
const contentRef = react.useRef(null);
|
|
1139
1147
|
const coverRef = react.useRef(null);
|
|
1140
1148
|
react.useLayoutEffect(() => {
|
|
@@ -1145,22 +1153,22 @@ const CarouselSlot = react.memo(({ hadDragRef, item, itemWidth, slotRef }) => {
|
|
|
1145
1153
|
const box = new THREE.Box3().setFromObject(content);
|
|
1146
1154
|
if (box.isEmpty())
|
|
1147
1155
|
return;
|
|
1156
|
+
const w = Math.max(box.max.x - box.min.x, itemWidth);
|
|
1148
1157
|
const h = box.max.y - box.min.y;
|
|
1149
1158
|
cover.geometry.dispose();
|
|
1150
|
-
cover.geometry = new THREE.PlaneGeometry(
|
|
1151
|
-
cover.position.
|
|
1152
|
-
}, [
|
|
1159
|
+
cover.geometry = new THREE.PlaneGeometry(w, h);
|
|
1160
|
+
cover.position.set((box.max.x + box.min.x) / 2, (box.max.y + box.min.y) / 2, box.max.z + 0.001);
|
|
1161
|
+
}, [itemWidth]);
|
|
1153
1162
|
// Dispose the generated geometry on unmount — useLayoutEffect's replacement
|
|
1154
1163
|
// logic only disposes the *previous* geometry, not the final one.
|
|
1155
1164
|
react.useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
}, children: [jsxRuntime.jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
1165
|
+
// No onClick on the cover — the carousel's document-level capture listener
|
|
1166
|
+
// handles click suppression. The cover stays as a drag hit-target so swiping
|
|
1167
|
+
// works on slot regions where user content has gaps.
|
|
1168
|
+
return (jsxRuntime.jsxs("group", { ref: slotRef, children: [jsxRuntime.jsx("group", { ref: contentRef, children: visible ? item : null }), jsxRuntime.jsxs("mesh", { ref: coverRef, children: [jsxRuntime.jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
1162
1169
|
});
|
|
1163
|
-
const Carousel = (
|
|
1170
|
+
const Carousel = (_a) => {
|
|
1171
|
+
var { items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY } = _a, props = tslib.__rest(_a, ["items", "itemWidth", "gap", "defaultValue", "transition", "onSwitch", "onDragStart", "onDrag", "onDragEnd", "renderThreshold", "dragThreshold", "flickVelocity"]);
|
|
1164
1172
|
const slideWidth = itemWidth + gap;
|
|
1165
1173
|
const count = items.length;
|
|
1166
1174
|
// Render twice the items and center on the first item of the second copy.
|
|
@@ -1173,31 +1181,71 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
1173
1181
|
const startSlot = count + defaultValue;
|
|
1174
1182
|
const groupRef = react.useRef(null);
|
|
1175
1183
|
const itemRefs = react.useRef([]);
|
|
1184
|
+
// Slot mount/unmount state — mirrors `ref.visible` but propagates through
|
|
1185
|
+
// React so children that rely on lifecycle (e.g. Drei's <Html>, which
|
|
1186
|
+
// doesn't reliably react to mid-frame `visible` mutations) update cleanly.
|
|
1187
|
+
// Only flips when a slot crosses the threshold, so re-renders are limited
|
|
1188
|
+
// to ~2-4 per swipe instead of every frame.
|
|
1189
|
+
const [visibleMask, setVisibleMask] = react.useState(() => {
|
|
1190
|
+
if (renderThreshold === undefined)
|
|
1191
|
+
return new Array(slotCount).fill(true);
|
|
1192
|
+
const mask = new Array(slotCount).fill(false);
|
|
1193
|
+
for (let i = 0; i < slotCount; i++) {
|
|
1194
|
+
mask[i] = Math.abs(i - startSlot) <= renderThreshold + 0.5;
|
|
1195
|
+
}
|
|
1196
|
+
return mask;
|
|
1197
|
+
});
|
|
1198
|
+
const visibleMaskRef = react.useRef(visibleMask);
|
|
1199
|
+
visibleMaskRef.current = visibleMask;
|
|
1176
1200
|
// Single source of truth: a fractional slot index that grows up/down
|
|
1177
1201
|
// infinitely as the user navigates. group.position.x = -currIndex * slideWidth
|
|
1178
1202
|
// at rest. Snap animations animate currIndex toward integer targets;
|
|
1179
1203
|
// useFrame applies it to the group every frame (when not dragging).
|
|
1180
1204
|
const currIndex = react$1.useMotionValue(startSlot);
|
|
1181
1205
|
const isDraggingRef = react.useRef(false);
|
|
1182
|
-
const hadDragRef = react.useRef(false);
|
|
1183
1206
|
const dragStartIndexRef = react.useRef(startSlot);
|
|
1184
1207
|
const snapAnimRef = react.useRef(null);
|
|
1208
|
+
// performance.now() of the most recent frame in which the carousel was
|
|
1209
|
+
// moving (drag in progress or off-snap). Click handler uses this to
|
|
1210
|
+
// suppress clicks during/just-after motion.
|
|
1211
|
+
const lastMotionAtRef = react.useRef(0);
|
|
1212
|
+
// Wrapped physical slot index (0..slotCount-1) — drives per-slot context so
|
|
1213
|
+
// children can read isActive/distance without the consumer having to lift
|
|
1214
|
+
// state. Updates the moment a snap commits, not while dragging.
|
|
1215
|
+
const [currentSlot, setCurrentSlot] = react.useState(() => mod(startSlot, slotCount));
|
|
1185
1216
|
const initial = react.useMemo(() => ({ x: -startSlot * slideWidth }),
|
|
1186
1217
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1187
1218
|
[]);
|
|
1188
1219
|
const goTo = react.useCallback((slot) => {
|
|
1189
1220
|
var _a;
|
|
1190
1221
|
onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
|
|
1222
|
+
setCurrentSlot(mod(slot, slotCount));
|
|
1191
1223
|
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
1192
1224
|
snapAnimRef.current = react$1.animate(currIndex, slot, transition);
|
|
1193
|
-
}, [count, transition, onSwitch, currIndex]);
|
|
1225
|
+
}, [count, slotCount, transition, onSwitch, currIndex]);
|
|
1194
1226
|
react.useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
|
|
1227
|
+
// Document-level capture click listener — fires before R3F's listener (and
|
|
1228
|
+
// any descendant DOM handlers) anywhere in the tree. While the carousel is
|
|
1229
|
+
// moving (or just settled), swallow the event so it never reaches user
|
|
1230
|
+
// content (R3F meshes or HTML portaled by Drei). No-op outside the cooldown
|
|
1231
|
+
// window, so true taps propagate normally.
|
|
1232
|
+
react.useEffect(() => {
|
|
1233
|
+
const onClickCapture = (e) => {
|
|
1234
|
+
if (isDragging) {
|
|
1235
|
+
e.stopImmediatePropagation();
|
|
1236
|
+
e.stopPropagation();
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
document.addEventListener('click', onClickCapture, true);
|
|
1240
|
+
return () => document.removeEventListener('click', onClickCapture, true);
|
|
1241
|
+
}, []);
|
|
1195
1242
|
// Drag start: stop any in-flight snap and capture the slot we started on
|
|
1196
1243
|
// so an under-threshold release can elastic back to it.
|
|
1197
1244
|
const handleDragStart = react.useCallback(() => {
|
|
1198
1245
|
var _a;
|
|
1246
|
+
isDragging = false;
|
|
1199
1247
|
isDraggingRef.current = true;
|
|
1200
|
-
|
|
1248
|
+
lastMotionAtRef.current = performance.now();
|
|
1201
1249
|
dragStartIndexRef.current = Math.round(currIndex.get());
|
|
1202
1250
|
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
1203
1251
|
snapAnimRef.current = null;
|
|
@@ -1222,7 +1270,6 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
1222
1270
|
goTo(startIndex);
|
|
1223
1271
|
return;
|
|
1224
1272
|
}
|
|
1225
|
-
hadDragRef.current = true;
|
|
1226
1273
|
const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
|
|
1227
1274
|
const targetSlot = Math.round(-projectedX / slideWidth);
|
|
1228
1275
|
goTo(targetSlot);
|
|
@@ -1240,6 +1287,12 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
1240
1287
|
group.position.x = -currIndex.get() * slideWidth;
|
|
1241
1288
|
}
|
|
1242
1289
|
const groupX = group.position.x;
|
|
1290
|
+
const snapDelta = Math.abs(groupX - Math.round(groupX / slideWidth) * slideWidth);
|
|
1291
|
+
if (isDraggingRef.current || snapDelta > 0.001) {
|
|
1292
|
+
lastMotionAtRef.current = performance.now();
|
|
1293
|
+
isDragging = snapDelta > 0.001;
|
|
1294
|
+
}
|
|
1295
|
+
let nextMask = null;
|
|
1243
1296
|
for (let i = 0; i < slotCount; i++) {
|
|
1244
1297
|
const ref = itemRefs.current[i];
|
|
1245
1298
|
if (!ref)
|
|
@@ -1250,10 +1303,33 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
1250
1303
|
// one fades out — keeps the loop seamless mid-drag instead of waiting
|
|
1251
1304
|
// for snap to settle on an integer slot.
|
|
1252
1305
|
const slotDist = (groupX + ref.position.x) / slideWidth;
|
|
1253
|
-
|
|
1306
|
+
const shouldBeVisible = renderThreshold ? Math.abs(slotDist) <= renderThreshold + 0.5 : true;
|
|
1307
|
+
ref.visible = shouldBeVisible;
|
|
1308
|
+
if (visibleMaskRef.current[i] !== shouldBeVisible) {
|
|
1309
|
+
if (!nextMask)
|
|
1310
|
+
nextMask = visibleMaskRef.current.slice();
|
|
1311
|
+
nextMask[i] = shouldBeVisible;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (nextMask) {
|
|
1315
|
+
visibleMaskRef.current = nextMask;
|
|
1316
|
+
setVisibleMask(nextMask);
|
|
1254
1317
|
}
|
|
1255
1318
|
});
|
|
1256
|
-
return (jsxRuntime.jsx(motion.group, { ref: groupRef, drag: "x", dragMomentum: false, initial: initial, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) =>
|
|
1319
|
+
return (jsxRuntime.jsx(motion.group, Object.assign({ ref: groupRef, drag: "x", dragMomentum: false, initial: initial }, props, { onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => {
|
|
1320
|
+
var _a;
|
|
1321
|
+
const raw = i - currentSlot;
|
|
1322
|
+
const halfWrap = (((raw + slotCount / 2) % slotCount) + slotCount) % slotCount;
|
|
1323
|
+
const distance = Math.abs(halfWrap - slotCount / 2);
|
|
1324
|
+
return (jsxRuntime.jsx(CarouselSlotContext.Provider, { value: {
|
|
1325
|
+
slotIndex: i,
|
|
1326
|
+
itemIndex: i % count,
|
|
1327
|
+
distance,
|
|
1328
|
+
isActive: distance === 0,
|
|
1329
|
+
isNearby: distance <= 1,
|
|
1330
|
+
currIndex,
|
|
1331
|
+
}, children: jsxRuntime.jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, visible: (_a = visibleMask[i]) !== null && _a !== void 0 ? _a : false }) }, `carouselItem-${i}`));
|
|
1332
|
+
}) })));
|
|
1257
1333
|
};
|
|
1258
1334
|
var index = react.memo(Carousel);
|
|
1259
1335
|
|
|
@@ -1261,4 +1337,5 @@ exports.AnimatePresence = AnimatePresence;
|
|
|
1261
1337
|
exports.Carousel = index;
|
|
1262
1338
|
exports.MotionCamera = MotionCamera;
|
|
1263
1339
|
exports.motion = motion;
|
|
1340
|
+
exports.useCarouselSlot = useCarouselSlot;
|
|
1264
1341
|
exports.usePresence = usePresence;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { __rest } from 'tslib';
|
|
2
3
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { memo, useMemo, useRef, useCallback, useEffect, useLayoutEffect } from 'react';
|
|
4
|
+
import { memo, useMemo, useRef, useState, useCallback, useEffect, useContext, createContext, useLayoutEffect } from 'react';
|
|
4
5
|
import { useFrame } from '@react-three/fiber';
|
|
5
6
|
import { Box3, PlaneGeometry } from 'three';
|
|
6
7
|
import { useMotionValue, animate } from 'motion/react';
|
|
@@ -21,7 +22,15 @@ const VELOCITY_PROJECTION = 0.2;
|
|
|
21
22
|
const DRAG_THRESHOLD_RATIO = 0.25;
|
|
22
23
|
const FLICK_VELOCITY = 1.0;
|
|
23
24
|
const mod = (n, m) => ((n % m) + m) % m;
|
|
24
|
-
|
|
25
|
+
let isDragging = false;
|
|
26
|
+
const CarouselSlotContext = createContext(null);
|
|
27
|
+
const useCarouselSlot = () => {
|
|
28
|
+
const ctx = useContext(CarouselSlotContext);
|
|
29
|
+
if (!ctx)
|
|
30
|
+
throw new Error('useCarouselSlot must be used inside <Carousel>');
|
|
31
|
+
return ctx;
|
|
32
|
+
};
|
|
33
|
+
const CarouselSlot = memo(({ item, itemWidth, slotRef, visible }) => {
|
|
25
34
|
const contentRef = useRef(null);
|
|
26
35
|
const coverRef = useRef(null);
|
|
27
36
|
useLayoutEffect(() => {
|
|
@@ -32,22 +41,22 @@ const CarouselSlot = memo(({ hadDragRef, item, itemWidth, slotRef }) => {
|
|
|
32
41
|
const box = new Box3().setFromObject(content);
|
|
33
42
|
if (box.isEmpty())
|
|
34
43
|
return;
|
|
44
|
+
const w = Math.max(box.max.x - box.min.x, itemWidth);
|
|
35
45
|
const h = box.max.y - box.min.y;
|
|
36
46
|
cover.geometry.dispose();
|
|
37
|
-
cover.geometry = new PlaneGeometry(
|
|
38
|
-
cover.position.
|
|
39
|
-
}, [
|
|
47
|
+
cover.geometry = new PlaneGeometry(w, h);
|
|
48
|
+
cover.position.set((box.max.x + box.min.x) / 2, (box.max.y + box.min.y) / 2, box.max.z + 0.001);
|
|
49
|
+
}, [itemWidth]);
|
|
40
50
|
// Dispose the generated geometry on unmount — useLayoutEffect's replacement
|
|
41
51
|
// logic only disposes the *previous* geometry, not the final one.
|
|
42
52
|
useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
}, children: [jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
53
|
+
// No onClick on the cover — the carousel's document-level capture listener
|
|
54
|
+
// handles click suppression. The cover stays as a drag hit-target so swiping
|
|
55
|
+
// works on slot regions where user content has gaps.
|
|
56
|
+
return (jsxs("group", { ref: slotRef, children: [jsx("group", { ref: contentRef, children: visible ? item : null }), jsxs("mesh", { ref: coverRef, children: [jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
49
57
|
});
|
|
50
|
-
const Carousel = (
|
|
58
|
+
const Carousel = (_a) => {
|
|
59
|
+
var { items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY } = _a, props = __rest(_a, ["items", "itemWidth", "gap", "defaultValue", "transition", "onSwitch", "onDragStart", "onDrag", "onDragEnd", "renderThreshold", "dragThreshold", "flickVelocity"]);
|
|
51
60
|
const slideWidth = itemWidth + gap;
|
|
52
61
|
const count = items.length;
|
|
53
62
|
// Render twice the items and center on the first item of the second copy.
|
|
@@ -60,31 +69,71 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
60
69
|
const startSlot = count + defaultValue;
|
|
61
70
|
const groupRef = useRef(null);
|
|
62
71
|
const itemRefs = useRef([]);
|
|
72
|
+
// Slot mount/unmount state — mirrors `ref.visible` but propagates through
|
|
73
|
+
// React so children that rely on lifecycle (e.g. Drei's <Html>, which
|
|
74
|
+
// doesn't reliably react to mid-frame `visible` mutations) update cleanly.
|
|
75
|
+
// Only flips when a slot crosses the threshold, so re-renders are limited
|
|
76
|
+
// to ~2-4 per swipe instead of every frame.
|
|
77
|
+
const [visibleMask, setVisibleMask] = useState(() => {
|
|
78
|
+
if (renderThreshold === undefined)
|
|
79
|
+
return new Array(slotCount).fill(true);
|
|
80
|
+
const mask = new Array(slotCount).fill(false);
|
|
81
|
+
for (let i = 0; i < slotCount; i++) {
|
|
82
|
+
mask[i] = Math.abs(i - startSlot) <= renderThreshold + 0.5;
|
|
83
|
+
}
|
|
84
|
+
return mask;
|
|
85
|
+
});
|
|
86
|
+
const visibleMaskRef = useRef(visibleMask);
|
|
87
|
+
visibleMaskRef.current = visibleMask;
|
|
63
88
|
// Single source of truth: a fractional slot index that grows up/down
|
|
64
89
|
// infinitely as the user navigates. group.position.x = -currIndex * slideWidth
|
|
65
90
|
// at rest. Snap animations animate currIndex toward integer targets;
|
|
66
91
|
// useFrame applies it to the group every frame (when not dragging).
|
|
67
92
|
const currIndex = useMotionValue(startSlot);
|
|
68
93
|
const isDraggingRef = useRef(false);
|
|
69
|
-
const hadDragRef = useRef(false);
|
|
70
94
|
const dragStartIndexRef = useRef(startSlot);
|
|
71
95
|
const snapAnimRef = useRef(null);
|
|
96
|
+
// performance.now() of the most recent frame in which the carousel was
|
|
97
|
+
// moving (drag in progress or off-snap). Click handler uses this to
|
|
98
|
+
// suppress clicks during/just-after motion.
|
|
99
|
+
const lastMotionAtRef = useRef(0);
|
|
100
|
+
// Wrapped physical slot index (0..slotCount-1) — drives per-slot context so
|
|
101
|
+
// children can read isActive/distance without the consumer having to lift
|
|
102
|
+
// state. Updates the moment a snap commits, not while dragging.
|
|
103
|
+
const [currentSlot, setCurrentSlot] = useState(() => mod(startSlot, slotCount));
|
|
72
104
|
const initial = useMemo(() => ({ x: -startSlot * slideWidth }),
|
|
73
105
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
106
|
[]);
|
|
75
107
|
const goTo = useCallback((slot) => {
|
|
76
108
|
var _a;
|
|
77
109
|
onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
|
|
110
|
+
setCurrentSlot(mod(slot, slotCount));
|
|
78
111
|
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
79
112
|
snapAnimRef.current = animate(currIndex, slot, transition);
|
|
80
|
-
}, [count, transition, onSwitch, currIndex]);
|
|
113
|
+
}, [count, slotCount, transition, onSwitch, currIndex]);
|
|
81
114
|
useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
|
|
115
|
+
// Document-level capture click listener — fires before R3F's listener (and
|
|
116
|
+
// any descendant DOM handlers) anywhere in the tree. While the carousel is
|
|
117
|
+
// moving (or just settled), swallow the event so it never reaches user
|
|
118
|
+
// content (R3F meshes or HTML portaled by Drei). No-op outside the cooldown
|
|
119
|
+
// window, so true taps propagate normally.
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const onClickCapture = (e) => {
|
|
122
|
+
if (isDragging) {
|
|
123
|
+
e.stopImmediatePropagation();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
document.addEventListener('click', onClickCapture, true);
|
|
128
|
+
return () => document.removeEventListener('click', onClickCapture, true);
|
|
129
|
+
}, []);
|
|
82
130
|
// Drag start: stop any in-flight snap and capture the slot we started on
|
|
83
131
|
// so an under-threshold release can elastic back to it.
|
|
84
132
|
const handleDragStart = useCallback(() => {
|
|
85
133
|
var _a;
|
|
134
|
+
isDragging = false;
|
|
86
135
|
isDraggingRef.current = true;
|
|
87
|
-
|
|
136
|
+
lastMotionAtRef.current = performance.now();
|
|
88
137
|
dragStartIndexRef.current = Math.round(currIndex.get());
|
|
89
138
|
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
90
139
|
snapAnimRef.current = null;
|
|
@@ -109,7 +158,6 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
109
158
|
goTo(startIndex);
|
|
110
159
|
return;
|
|
111
160
|
}
|
|
112
|
-
hadDragRef.current = true;
|
|
113
161
|
const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
|
|
114
162
|
const targetSlot = Math.round(-projectedX / slideWidth);
|
|
115
163
|
goTo(targetSlot);
|
|
@@ -127,6 +175,12 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
127
175
|
group.position.x = -currIndex.get() * slideWidth;
|
|
128
176
|
}
|
|
129
177
|
const groupX = group.position.x;
|
|
178
|
+
const snapDelta = Math.abs(groupX - Math.round(groupX / slideWidth) * slideWidth);
|
|
179
|
+
if (isDraggingRef.current || snapDelta > 0.001) {
|
|
180
|
+
lastMotionAtRef.current = performance.now();
|
|
181
|
+
isDragging = snapDelta > 0.001;
|
|
182
|
+
}
|
|
183
|
+
let nextMask = null;
|
|
130
184
|
for (let i = 0; i < slotCount; i++) {
|
|
131
185
|
const ref = itemRefs.current[i];
|
|
132
186
|
if (!ref)
|
|
@@ -137,11 +191,34 @@ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transit
|
|
|
137
191
|
// one fades out — keeps the loop seamless mid-drag instead of waiting
|
|
138
192
|
// for snap to settle on an integer slot.
|
|
139
193
|
const slotDist = (groupX + ref.position.x) / slideWidth;
|
|
140
|
-
|
|
194
|
+
const shouldBeVisible = renderThreshold ? Math.abs(slotDist) <= renderThreshold + 0.5 : true;
|
|
195
|
+
ref.visible = shouldBeVisible;
|
|
196
|
+
if (visibleMaskRef.current[i] !== shouldBeVisible) {
|
|
197
|
+
if (!nextMask)
|
|
198
|
+
nextMask = visibleMaskRef.current.slice();
|
|
199
|
+
nextMask[i] = shouldBeVisible;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (nextMask) {
|
|
203
|
+
visibleMaskRef.current = nextMask;
|
|
204
|
+
setVisibleMask(nextMask);
|
|
141
205
|
}
|
|
142
206
|
});
|
|
143
|
-
return (jsx(motion.group, { ref: groupRef, drag: "x", dragMomentum: false, initial: initial, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) =>
|
|
207
|
+
return (jsx(motion.group, Object.assign({ ref: groupRef, drag: "x", dragMomentum: false, initial: initial }, props, { onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => {
|
|
208
|
+
var _a;
|
|
209
|
+
const raw = i - currentSlot;
|
|
210
|
+
const halfWrap = (((raw + slotCount / 2) % slotCount) + slotCount) % slotCount;
|
|
211
|
+
const distance = Math.abs(halfWrap - slotCount / 2);
|
|
212
|
+
return (jsx(CarouselSlotContext.Provider, { value: {
|
|
213
|
+
slotIndex: i,
|
|
214
|
+
itemIndex: i % count,
|
|
215
|
+
distance,
|
|
216
|
+
isActive: distance === 0,
|
|
217
|
+
isNearby: distance <= 1,
|
|
218
|
+
currIndex,
|
|
219
|
+
}, children: jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, visible: (_a = visibleMask[i]) !== null && _a !== void 0 ? _a : false }) }, `carouselItem-${i}`));
|
|
220
|
+
}) })));
|
|
144
221
|
};
|
|
145
222
|
var index = memo(Carousel);
|
|
146
223
|
|
|
147
|
-
export { index as default };
|
|
224
|
+
export { index as default, useCarouselSlot };
|
package/dist/es/index.mjs
CHANGED
|
@@ -3,4 +3,4 @@ export { motion } from './render/motion.mjs';
|
|
|
3
3
|
export { default as MotionCamera } from './components/MotionCamera.mjs';
|
|
4
4
|
export { AnimatePresence } from './components/AnimatePresence/index.mjs';
|
|
5
5
|
export { usePresence } from './components/AnimatePresence/PresenceContext.mjs';
|
|
6
|
-
export { default as Carousel } from './components/Carousel/index.mjs';
|
|
6
|
+
export { default as Carousel, useCarouselSlot } from './components/Carousel/index.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Vector3, Euler, Color, ReactThreeFiber } from '@react-three/fiber';
|
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, ReactNode } from 'react';
|
|
5
|
-
import { MotionValue, MotionProps, Transition, ResolvedValues } from 'motion/react';
|
|
5
|
+
import { MotionValue, MotionProps, Transition, ResolvedValues, useMotionValue } from 'motion/react';
|
|
6
6
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
8
|
type ThreeElement = InstanceType<typeof THREE.Object3D> & Record<string, unknown>;
|
|
@@ -141,8 +141,17 @@ interface CarouselProps {
|
|
|
141
141
|
dragThreshold?: number;
|
|
142
142
|
flickVelocity?: number;
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
interface CarouselSlotInfo {
|
|
145
|
+
slotIndex: number;
|
|
146
|
+
itemIndex: number;
|
|
147
|
+
currIndex: ReturnType<typeof useMotionValue>;
|
|
148
|
+
distance: number;
|
|
149
|
+
isActive: boolean;
|
|
150
|
+
isNearby: boolean;
|
|
151
|
+
}
|
|
152
|
+
declare const useCarouselSlot: () => CarouselSlotInfo;
|
|
153
|
+
declare const Carousel: ({ items, itemWidth, gap, defaultValue, transition, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold, flickVelocity, ...props }: CarouselProps) => react_jsx_runtime.JSX.Element;
|
|
145
154
|
declare const _default: typeof Carousel;
|
|
146
155
|
|
|
147
|
-
export { AnimatePresence, _default as Carousel, MotionCamera, motion, usePresence };
|
|
148
|
-
export type { AcceptMotionValues, CarouselProps, DragConstraints, DragInfo, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
|
|
156
|
+
export { AnimatePresence, _default as Carousel, MotionCamera, motion, useCarouselSlot, usePresence };
|
|
157
|
+
export type { AcceptMotionValues, CarouselProps, CarouselSlotInfo, DragConstraints, DragInfo, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
|
package/package.json
CHANGED