r3f-motion 1.0.6 → 1.0.8

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 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
- const CarouselSlot = react.memo(({ hadDragRef, item, itemWidth, slotRef }) => {
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(itemWidth, h);
1151
- cover.position.z = box.max.z + 0.001;
1152
- }, [item, itemWidth]);
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
- return (jsxRuntime.jsxs("group", { ref: slotRef, children: [jsxRuntime.jsx("group", { ref: contentRef, children: item }), jsxRuntime.jsxs("mesh", { ref: coverRef, onClick: (e) => {
1157
- if (hadDragRef.current) {
1158
- e.stopPropagation();
1159
- hadDragRef.current = false;
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 = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold = 1, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY, }) => {
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
- hadDragRef.current = false;
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
- ref.visible = Math.abs(slotDist) <= renderThreshold + 0.5;
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) => (jsxRuntime.jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, hadDragRef: hadDragRef }, `carouselItem-${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
- const CarouselSlot = memo(({ hadDragRef, item, itemWidth, slotRef }) => {
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(itemWidth, h);
38
- cover.position.z = box.max.z + 0.001;
39
- }, [item, itemWidth]);
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
- return (jsxs("group", { ref: slotRef, children: [jsx("group", { ref: contentRef, children: item }), jsxs("mesh", { ref: coverRef, onClick: (e) => {
44
- if (hadDragRef.current) {
45
- e.stopPropagation();
46
- hadDragRef.current = false;
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 = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold = 1, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY, }) => {
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
- hadDragRef.current = false;
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
- ref.visible = Math.abs(slotDist) <= renderThreshold + 0.5;
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) => (jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, hadDragRef: hadDragRef }, `carouselItem-${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
- declare const Carousel: ({ items, itemWidth, gap, defaultValue, transition, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold, flickVelocity, }: CarouselProps) => react_jsx_runtime.JSX.Element;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "r3f-motion",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "A simple and powerful React animation library for @react-three/fiber leveraging motion.dev",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/es/index.mjs",