react-sway 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ReactSway: () => ReactSway_default
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/ReactSway.tsx
28
+ var import_react = require("react");
29
+ var import_jsx_runtime = require("react/jsx-runtime");
30
+ var SCROLL_SPEED = 0.5;
31
+ var INACTIVITY_DELAY = 2e3;
32
+ var FRICTION = 0.95;
33
+ var MAX_DELTA_TIME = 3;
34
+ function ReactSway({ children }) {
35
+ const [position, setPosition] = (0, import_react.useState)(0);
36
+ const [, setVelocity] = (0, import_react.useState)(0);
37
+ const [isDragging, setIsDragging] = (0, import_react.useState)(false);
38
+ const [isPaused, setIsPaused] = (0, import_react.useState)(false);
39
+ const [autoScrollEnabled, setAutoScrollEnabled] = (0, import_react.useState)(true);
40
+ const [isTabActive, setIsTabActive] = (0, import_react.useState)(true);
41
+ const [contentHeight, setContentHeight] = (0, import_react.useState)(0);
42
+ const [loopPoint, setLoopPoint] = (0, import_react.useState)(0);
43
+ const [, setContainerHeight] = (0, import_react.useState)(0);
44
+ const containerRef = (0, import_react.useRef)(null);
45
+ const animationFrameRef = (0, import_react.useRef)(null);
46
+ const inactivityTimerRef = (0, import_react.useRef)(null);
47
+ const lastTouchYRef = (0, import_react.useRef)(0);
48
+ const lastMouseYRef = (0, import_react.useRef)(0);
49
+ const lastFrameTimeRef = (0, import_react.useRef)(0);
50
+ let visualPosition = position % (loopPoint || 1);
51
+ if (visualPosition > 0 && loopPoint > 0) visualPosition -= loopPoint;
52
+ (0, import_react.useEffect)(() => {
53
+ const calculateDimensions = () => {
54
+ if (containerRef.current) {
55
+ containerRef.current.offsetHeight;
56
+ const currentContentHeight = containerRef.current.scrollHeight;
57
+ const calculatedLoopPoint = currentContentHeight / 3;
58
+ console.log("Calculating dimensions:", { currentContentHeight, calculatedLoopPoint });
59
+ if (currentContentHeight > 0) {
60
+ setContentHeight(currentContentHeight);
61
+ setLoopPoint(calculatedLoopPoint);
62
+ }
63
+ setContainerHeight(window.innerHeight);
64
+ }
65
+ };
66
+ const rafId = requestAnimationFrame(() => {
67
+ calculateDimensions();
68
+ });
69
+ return () => {
70
+ cancelAnimationFrame(rafId);
71
+ };
72
+ }, [children]);
73
+ const pauseAutoScroll = (0, import_react.useCallback)(() => {
74
+ setAutoScrollEnabled(false);
75
+ if (inactivityTimerRef.current) {
76
+ clearTimeout(inactivityTimerRef.current);
77
+ }
78
+ }, []);
79
+ const scheduleAutoScrollResume = (0, import_react.useCallback)(() => {
80
+ if (inactivityTimerRef.current) {
81
+ clearTimeout(inactivityTimerRef.current);
82
+ }
83
+ inactivityTimerRef.current = setTimeout(() => {
84
+ setAutoScrollEnabled(true);
85
+ }, INACTIVITY_DELAY);
86
+ }, []);
87
+ const handleMouseDown = (0, import_react.useCallback)((e) => {
88
+ e.preventDefault();
89
+ setIsDragging(true);
90
+ lastMouseYRef.current = e.clientY;
91
+ setVelocity(0);
92
+ pauseAutoScroll();
93
+ }, [pauseAutoScroll]);
94
+ const handleMouseMove = (0, import_react.useCallback)((e) => {
95
+ if (!isDragging) return;
96
+ e.preventDefault();
97
+ const deltaY = e.clientY - lastMouseYRef.current;
98
+ setPosition((prev) => prev + deltaY);
99
+ setVelocity(deltaY);
100
+ lastMouseYRef.current = e.clientY;
101
+ }, [isDragging]);
102
+ const handleMouseUp = (0, import_react.useCallback)((e) => {
103
+ if (!isDragging) return;
104
+ e.preventDefault();
105
+ setIsDragging(false);
106
+ scheduleAutoScrollResume();
107
+ }, [isDragging, scheduleAutoScrollResume]);
108
+ const handleTouchStart = (0, import_react.useCallback)((e) => {
109
+ if (e.touches.length === 1) {
110
+ setIsDragging(true);
111
+ lastTouchYRef.current = e.touches[0].clientY;
112
+ setVelocity(0);
113
+ pauseAutoScroll();
114
+ }
115
+ }, [pauseAutoScroll]);
116
+ const handleTouchMove = (0, import_react.useCallback)((e) => {
117
+ if (!isDragging || e.touches.length !== 1) return;
118
+ e.preventDefault();
119
+ const touch = e.touches[0];
120
+ const deltaY = touch.clientY - lastTouchYRef.current;
121
+ setPosition((prev) => prev + deltaY);
122
+ setVelocity(deltaY);
123
+ lastTouchYRef.current = touch.clientY;
124
+ }, [isDragging]);
125
+ const handleTouchEnd = (0, import_react.useCallback)((_e) => {
126
+ if (!isDragging) return;
127
+ setIsDragging(false);
128
+ scheduleAutoScrollResume();
129
+ }, [isDragging, scheduleAutoScrollResume]);
130
+ const handleWheel = (0, import_react.useCallback)((e) => {
131
+ e.preventDefault();
132
+ setVelocity((prev) => prev - e.deltaY * 0.3);
133
+ pauseAutoScroll();
134
+ scheduleAutoScrollResume();
135
+ }, [pauseAutoScroll, scheduleAutoScrollResume]);
136
+ const togglePause = (0, import_react.useCallback)(() => {
137
+ setIsPaused((prev) => {
138
+ const newPausedState = !prev;
139
+ if (newPausedState) {
140
+ pauseAutoScroll();
141
+ } else {
142
+ setAutoScrollEnabled(true);
143
+ }
144
+ return newPausedState;
145
+ });
146
+ }, [pauseAutoScroll]);
147
+ const handleKeyDown = (0, import_react.useCallback)((e) => {
148
+ switch (e.key) {
149
+ case " ":
150
+ e.preventDefault();
151
+ togglePause();
152
+ break;
153
+ case "ArrowUp":
154
+ e.preventDefault();
155
+ setVelocity((prev) => prev + 15);
156
+ pauseAutoScroll();
157
+ scheduleAutoScrollResume();
158
+ break;
159
+ case "ArrowDown":
160
+ e.preventDefault();
161
+ setVelocity((prev) => prev - 15);
162
+ pauseAutoScroll();
163
+ scheduleAutoScrollResume();
164
+ break;
165
+ case "Home":
166
+ e.preventDefault();
167
+ setPosition(0);
168
+ setVelocity(0);
169
+ pauseAutoScroll();
170
+ scheduleAutoScrollResume();
171
+ break;
172
+ case "End":
173
+ e.preventDefault();
174
+ if (loopPoint > 0) {
175
+ setPosition(-loopPoint);
176
+ }
177
+ setVelocity(0);
178
+ pauseAutoScroll();
179
+ scheduleAutoScrollResume();
180
+ break;
181
+ default:
182
+ break;
183
+ }
184
+ }, [togglePause, pauseAutoScroll, scheduleAutoScrollResume, loopPoint]);
185
+ const handleResize = (0, import_react.useCallback)(() => {
186
+ setContainerHeight(window.innerHeight);
187
+ if (containerRef.current) {
188
+ const currentContentHeight = containerRef.current.scrollHeight;
189
+ setContentHeight(currentContentHeight);
190
+ setLoopPoint(currentContentHeight / 3);
191
+ }
192
+ }, []);
193
+ (0, import_react.useEffect)(() => {
194
+ const currentContainer = containerRef.current;
195
+ if (!currentContainer) return;
196
+ const boundHandlers = {
197
+ mouseDown: handleMouseDown,
198
+ mouseMove: handleMouseMove,
199
+ mouseUp: handleMouseUp,
200
+ touchStart: handleTouchStart,
201
+ touchMove: handleTouchMove,
202
+ touchEnd: handleTouchEnd,
203
+ wheel: handleWheel
204
+ };
205
+ currentContainer.addEventListener("mousedown", boundHandlers.mouseDown);
206
+ window.addEventListener("mousemove", boundHandlers.mouseMove);
207
+ window.addEventListener("mouseup", boundHandlers.mouseUp);
208
+ currentContainer.addEventListener("touchstart", boundHandlers.touchStart, { passive: true });
209
+ window.addEventListener("touchmove", boundHandlers.touchMove, { passive: false });
210
+ window.addEventListener("touchend", boundHandlers.touchEnd, { passive: true });
211
+ currentContainer.addEventListener("wheel", boundHandlers.wheel, { passive: false });
212
+ return () => {
213
+ currentContainer.removeEventListener("mousedown", boundHandlers.mouseDown);
214
+ window.removeEventListener("mousemove", boundHandlers.mouseMove);
215
+ window.removeEventListener("mouseup", boundHandlers.mouseUp);
216
+ currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
217
+ window.removeEventListener("touchmove", boundHandlers.touchMove);
218
+ window.removeEventListener("touchend", boundHandlers.touchEnd);
219
+ currentContainer.removeEventListener("wheel", boundHandlers.wheel);
220
+ };
221
+ }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd, handleWheel]);
222
+ (0, import_react.useEffect)(() => {
223
+ document.addEventListener("keydown", handleKeyDown);
224
+ window.addEventListener("resize", handleResize);
225
+ return () => {
226
+ document.removeEventListener("keydown", handleKeyDown);
227
+ window.removeEventListener("resize", handleResize);
228
+ };
229
+ }, [handleKeyDown, handleResize]);
230
+ (0, import_react.useEffect)(() => {
231
+ const handleVisibilityChange = () => {
232
+ setIsTabActive(!document.hidden);
233
+ if (!document.hidden) {
234
+ lastFrameTimeRef.current = performance.now();
235
+ }
236
+ };
237
+ document.addEventListener("visibilitychange", handleVisibilityChange);
238
+ return () => {
239
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
240
+ };
241
+ }, []);
242
+ (0, import_react.useEffect)(() => {
243
+ if (!isTabActive || isPaused) {
244
+ if (animationFrameRef.current) {
245
+ cancelAnimationFrame(animationFrameRef.current);
246
+ animationFrameRef.current = null;
247
+ }
248
+ return;
249
+ }
250
+ const animate = (currentTime) => {
251
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / 16.667 : 1;
252
+ deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
253
+ lastFrameTimeRef.current = currentTime;
254
+ setPosition((prevPosition) => {
255
+ let newPosition = prevPosition;
256
+ if (autoScrollEnabled && !isDragging && !isPaused) {
257
+ newPosition -= SCROLL_SPEED * deltaTime;
258
+ }
259
+ return newPosition;
260
+ });
261
+ setVelocity((prevVelocity) => {
262
+ let newVelocity = prevVelocity;
263
+ if (Math.abs(newVelocity) > 0.1) {
264
+ if (!isDragging) {
265
+ setPosition((prev) => prev + newVelocity * deltaTime);
266
+ }
267
+ newVelocity *= Math.pow(FRICTION, deltaTime);
268
+ } else {
269
+ newVelocity = 0;
270
+ }
271
+ return newVelocity;
272
+ });
273
+ setPosition((prevPosition) => {
274
+ let newPosition = prevPosition;
275
+ if (loopPoint > 0) {
276
+ while (newPosition > 0) {
277
+ newPosition -= loopPoint;
278
+ }
279
+ while (newPosition < -loopPoint * 2) {
280
+ newPosition += loopPoint;
281
+ }
282
+ }
283
+ return newPosition;
284
+ });
285
+ animationFrameRef.current = requestAnimationFrame(animate);
286
+ };
287
+ animationFrameRef.current = requestAnimationFrame(animate);
288
+ return () => {
289
+ if (animationFrameRef.current) {
290
+ cancelAnimationFrame(animationFrameRef.current);
291
+ }
292
+ };
293
+ }, [isTabActive, autoScrollEnabled, isDragging, isPaused, loopPoint]);
294
+ (0, import_react.useEffect)(() => {
295
+ if (!containerRef.current) return;
296
+ const observer = new IntersectionObserver(
297
+ (entries) => {
298
+ entries.forEach((entry) => {
299
+ if (entry.isIntersecting) {
300
+ entry.target.classList.add("visible");
301
+ }
302
+ });
303
+ },
304
+ {
305
+ root: null,
306
+ rootMargin: "100px",
307
+ threshold: 0.01
308
+ }
309
+ );
310
+ const items = containerRef.current.querySelectorAll(".content-item");
311
+ items.forEach((item) => observer.observe(item));
312
+ return () => {
313
+ items.forEach((item) => observer.unobserve(item));
314
+ observer.disconnect();
315
+ };
316
+ }, [children, contentHeight]);
317
+ (0, import_react.useEffect)(() => {
318
+ const originalBodyStyle = {
319
+ touchAction: document.body.style.touchAction,
320
+ overflow: document.body.style.overflow
321
+ };
322
+ document.body.style.touchAction = "none";
323
+ document.body.style.overflow = "hidden";
324
+ return () => {
325
+ document.body.style.touchAction = originalBodyStyle.touchAction;
326
+ document.body.style.overflow = originalBodyStyle.overflow;
327
+ };
328
+ }, []);
329
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
330
+ "div",
331
+ {
332
+ className: "react-sway-container scroller-content",
333
+ ref: containerRef,
334
+ style: {
335
+ transform: `translate3d(0, ${visualPosition}px, 0)`,
336
+ cursor: isDragging ? "grabbing" : "grab",
337
+ position: "absolute",
338
+ width: "100%",
339
+ willChange: "transform",
340
+ WebkitTransform: "translateZ(0)",
341
+ touchAction: "none",
342
+ userSelect: "none",
343
+ WebkitUserSelect: "none",
344
+ msUserSelect: "none",
345
+ MozUserSelect: "none",
346
+ overscrollBehavior: "contain",
347
+ // Ensure it's on top and can receive events
348
+ pointerEvents: "auto",
349
+ zIndex: 1
350
+ },
351
+ tabIndex: 0,
352
+ children: [
353
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "content-group original", children }),
354
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children }),
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children })
356
+ ]
357
+ }
358
+ );
359
+ }
360
+ var ReactSway_default = ReactSway;
361
+ // Annotate the CommonJS export names for ESM import in node:
362
+ 0 && (module.exports = {
363
+ ReactSway
364
+ });
@@ -0,0 +1,9 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface ReactSwayProps {
5
+ children: ReactNode;
6
+ }
7
+ declare function ReactSway({ children }: ReactSwayProps): react_jsx_runtime.JSX.Element;
8
+
9
+ export { ReactSway };
@@ -0,0 +1,9 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface ReactSwayProps {
5
+ children: ReactNode;
6
+ }
7
+ declare function ReactSway({ children }: ReactSwayProps): react_jsx_runtime.JSX.Element;
8
+
9
+ export { ReactSway };
package/dist/index.js ADDED
@@ -0,0 +1,337 @@
1
+ // src/ReactSway.tsx
2
+ import { useState, useRef, useEffect, useCallback } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var SCROLL_SPEED = 0.5;
5
+ var INACTIVITY_DELAY = 2e3;
6
+ var FRICTION = 0.95;
7
+ var MAX_DELTA_TIME = 3;
8
+ function ReactSway({ children }) {
9
+ const [position, setPosition] = useState(0);
10
+ const [, setVelocity] = useState(0);
11
+ const [isDragging, setIsDragging] = useState(false);
12
+ const [isPaused, setIsPaused] = useState(false);
13
+ const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
14
+ const [isTabActive, setIsTabActive] = useState(true);
15
+ const [contentHeight, setContentHeight] = useState(0);
16
+ const [loopPoint, setLoopPoint] = useState(0);
17
+ const [, setContainerHeight] = useState(0);
18
+ const containerRef = useRef(null);
19
+ const animationFrameRef = useRef(null);
20
+ const inactivityTimerRef = useRef(null);
21
+ const lastTouchYRef = useRef(0);
22
+ const lastMouseYRef = useRef(0);
23
+ const lastFrameTimeRef = useRef(0);
24
+ let visualPosition = position % (loopPoint || 1);
25
+ if (visualPosition > 0 && loopPoint > 0) visualPosition -= loopPoint;
26
+ useEffect(() => {
27
+ const calculateDimensions = () => {
28
+ if (containerRef.current) {
29
+ containerRef.current.offsetHeight;
30
+ const currentContentHeight = containerRef.current.scrollHeight;
31
+ const calculatedLoopPoint = currentContentHeight / 3;
32
+ console.log("Calculating dimensions:", { currentContentHeight, calculatedLoopPoint });
33
+ if (currentContentHeight > 0) {
34
+ setContentHeight(currentContentHeight);
35
+ setLoopPoint(calculatedLoopPoint);
36
+ }
37
+ setContainerHeight(window.innerHeight);
38
+ }
39
+ };
40
+ const rafId = requestAnimationFrame(() => {
41
+ calculateDimensions();
42
+ });
43
+ return () => {
44
+ cancelAnimationFrame(rafId);
45
+ };
46
+ }, [children]);
47
+ const pauseAutoScroll = useCallback(() => {
48
+ setAutoScrollEnabled(false);
49
+ if (inactivityTimerRef.current) {
50
+ clearTimeout(inactivityTimerRef.current);
51
+ }
52
+ }, []);
53
+ const scheduleAutoScrollResume = useCallback(() => {
54
+ if (inactivityTimerRef.current) {
55
+ clearTimeout(inactivityTimerRef.current);
56
+ }
57
+ inactivityTimerRef.current = setTimeout(() => {
58
+ setAutoScrollEnabled(true);
59
+ }, INACTIVITY_DELAY);
60
+ }, []);
61
+ const handleMouseDown = useCallback((e) => {
62
+ e.preventDefault();
63
+ setIsDragging(true);
64
+ lastMouseYRef.current = e.clientY;
65
+ setVelocity(0);
66
+ pauseAutoScroll();
67
+ }, [pauseAutoScroll]);
68
+ const handleMouseMove = useCallback((e) => {
69
+ if (!isDragging) return;
70
+ e.preventDefault();
71
+ const deltaY = e.clientY - lastMouseYRef.current;
72
+ setPosition((prev) => prev + deltaY);
73
+ setVelocity(deltaY);
74
+ lastMouseYRef.current = e.clientY;
75
+ }, [isDragging]);
76
+ const handleMouseUp = useCallback((e) => {
77
+ if (!isDragging) return;
78
+ e.preventDefault();
79
+ setIsDragging(false);
80
+ scheduleAutoScrollResume();
81
+ }, [isDragging, scheduleAutoScrollResume]);
82
+ const handleTouchStart = useCallback((e) => {
83
+ if (e.touches.length === 1) {
84
+ setIsDragging(true);
85
+ lastTouchYRef.current = e.touches[0].clientY;
86
+ setVelocity(0);
87
+ pauseAutoScroll();
88
+ }
89
+ }, [pauseAutoScroll]);
90
+ const handleTouchMove = useCallback((e) => {
91
+ if (!isDragging || e.touches.length !== 1) return;
92
+ e.preventDefault();
93
+ const touch = e.touches[0];
94
+ const deltaY = touch.clientY - lastTouchYRef.current;
95
+ setPosition((prev) => prev + deltaY);
96
+ setVelocity(deltaY);
97
+ lastTouchYRef.current = touch.clientY;
98
+ }, [isDragging]);
99
+ const handleTouchEnd = useCallback((_e) => {
100
+ if (!isDragging) return;
101
+ setIsDragging(false);
102
+ scheduleAutoScrollResume();
103
+ }, [isDragging, scheduleAutoScrollResume]);
104
+ const handleWheel = useCallback((e) => {
105
+ e.preventDefault();
106
+ setVelocity((prev) => prev - e.deltaY * 0.3);
107
+ pauseAutoScroll();
108
+ scheduleAutoScrollResume();
109
+ }, [pauseAutoScroll, scheduleAutoScrollResume]);
110
+ const togglePause = useCallback(() => {
111
+ setIsPaused((prev) => {
112
+ const newPausedState = !prev;
113
+ if (newPausedState) {
114
+ pauseAutoScroll();
115
+ } else {
116
+ setAutoScrollEnabled(true);
117
+ }
118
+ return newPausedState;
119
+ });
120
+ }, [pauseAutoScroll]);
121
+ const handleKeyDown = useCallback((e) => {
122
+ switch (e.key) {
123
+ case " ":
124
+ e.preventDefault();
125
+ togglePause();
126
+ break;
127
+ case "ArrowUp":
128
+ e.preventDefault();
129
+ setVelocity((prev) => prev + 15);
130
+ pauseAutoScroll();
131
+ scheduleAutoScrollResume();
132
+ break;
133
+ case "ArrowDown":
134
+ e.preventDefault();
135
+ setVelocity((prev) => prev - 15);
136
+ pauseAutoScroll();
137
+ scheduleAutoScrollResume();
138
+ break;
139
+ case "Home":
140
+ e.preventDefault();
141
+ setPosition(0);
142
+ setVelocity(0);
143
+ pauseAutoScroll();
144
+ scheduleAutoScrollResume();
145
+ break;
146
+ case "End":
147
+ e.preventDefault();
148
+ if (loopPoint > 0) {
149
+ setPosition(-loopPoint);
150
+ }
151
+ setVelocity(0);
152
+ pauseAutoScroll();
153
+ scheduleAutoScrollResume();
154
+ break;
155
+ default:
156
+ break;
157
+ }
158
+ }, [togglePause, pauseAutoScroll, scheduleAutoScrollResume, loopPoint]);
159
+ const handleResize = useCallback(() => {
160
+ setContainerHeight(window.innerHeight);
161
+ if (containerRef.current) {
162
+ const currentContentHeight = containerRef.current.scrollHeight;
163
+ setContentHeight(currentContentHeight);
164
+ setLoopPoint(currentContentHeight / 3);
165
+ }
166
+ }, []);
167
+ useEffect(() => {
168
+ const currentContainer = containerRef.current;
169
+ if (!currentContainer) return;
170
+ const boundHandlers = {
171
+ mouseDown: handleMouseDown,
172
+ mouseMove: handleMouseMove,
173
+ mouseUp: handleMouseUp,
174
+ touchStart: handleTouchStart,
175
+ touchMove: handleTouchMove,
176
+ touchEnd: handleTouchEnd,
177
+ wheel: handleWheel
178
+ };
179
+ currentContainer.addEventListener("mousedown", boundHandlers.mouseDown);
180
+ window.addEventListener("mousemove", boundHandlers.mouseMove);
181
+ window.addEventListener("mouseup", boundHandlers.mouseUp);
182
+ currentContainer.addEventListener("touchstart", boundHandlers.touchStart, { passive: true });
183
+ window.addEventListener("touchmove", boundHandlers.touchMove, { passive: false });
184
+ window.addEventListener("touchend", boundHandlers.touchEnd, { passive: true });
185
+ currentContainer.addEventListener("wheel", boundHandlers.wheel, { passive: false });
186
+ return () => {
187
+ currentContainer.removeEventListener("mousedown", boundHandlers.mouseDown);
188
+ window.removeEventListener("mousemove", boundHandlers.mouseMove);
189
+ window.removeEventListener("mouseup", boundHandlers.mouseUp);
190
+ currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
191
+ window.removeEventListener("touchmove", boundHandlers.touchMove);
192
+ window.removeEventListener("touchend", boundHandlers.touchEnd);
193
+ currentContainer.removeEventListener("wheel", boundHandlers.wheel);
194
+ };
195
+ }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd, handleWheel]);
196
+ useEffect(() => {
197
+ document.addEventListener("keydown", handleKeyDown);
198
+ window.addEventListener("resize", handleResize);
199
+ return () => {
200
+ document.removeEventListener("keydown", handleKeyDown);
201
+ window.removeEventListener("resize", handleResize);
202
+ };
203
+ }, [handleKeyDown, handleResize]);
204
+ useEffect(() => {
205
+ const handleVisibilityChange = () => {
206
+ setIsTabActive(!document.hidden);
207
+ if (!document.hidden) {
208
+ lastFrameTimeRef.current = performance.now();
209
+ }
210
+ };
211
+ document.addEventListener("visibilitychange", handleVisibilityChange);
212
+ return () => {
213
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
214
+ };
215
+ }, []);
216
+ useEffect(() => {
217
+ if (!isTabActive || isPaused) {
218
+ if (animationFrameRef.current) {
219
+ cancelAnimationFrame(animationFrameRef.current);
220
+ animationFrameRef.current = null;
221
+ }
222
+ return;
223
+ }
224
+ const animate = (currentTime) => {
225
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / 16.667 : 1;
226
+ deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
227
+ lastFrameTimeRef.current = currentTime;
228
+ setPosition((prevPosition) => {
229
+ let newPosition = prevPosition;
230
+ if (autoScrollEnabled && !isDragging && !isPaused) {
231
+ newPosition -= SCROLL_SPEED * deltaTime;
232
+ }
233
+ return newPosition;
234
+ });
235
+ setVelocity((prevVelocity) => {
236
+ let newVelocity = prevVelocity;
237
+ if (Math.abs(newVelocity) > 0.1) {
238
+ if (!isDragging) {
239
+ setPosition((prev) => prev + newVelocity * deltaTime);
240
+ }
241
+ newVelocity *= Math.pow(FRICTION, deltaTime);
242
+ } else {
243
+ newVelocity = 0;
244
+ }
245
+ return newVelocity;
246
+ });
247
+ setPosition((prevPosition) => {
248
+ let newPosition = prevPosition;
249
+ if (loopPoint > 0) {
250
+ while (newPosition > 0) {
251
+ newPosition -= loopPoint;
252
+ }
253
+ while (newPosition < -loopPoint * 2) {
254
+ newPosition += loopPoint;
255
+ }
256
+ }
257
+ return newPosition;
258
+ });
259
+ animationFrameRef.current = requestAnimationFrame(animate);
260
+ };
261
+ animationFrameRef.current = requestAnimationFrame(animate);
262
+ return () => {
263
+ if (animationFrameRef.current) {
264
+ cancelAnimationFrame(animationFrameRef.current);
265
+ }
266
+ };
267
+ }, [isTabActive, autoScrollEnabled, isDragging, isPaused, loopPoint]);
268
+ useEffect(() => {
269
+ if (!containerRef.current) return;
270
+ const observer = new IntersectionObserver(
271
+ (entries) => {
272
+ entries.forEach((entry) => {
273
+ if (entry.isIntersecting) {
274
+ entry.target.classList.add("visible");
275
+ }
276
+ });
277
+ },
278
+ {
279
+ root: null,
280
+ rootMargin: "100px",
281
+ threshold: 0.01
282
+ }
283
+ );
284
+ const items = containerRef.current.querySelectorAll(".content-item");
285
+ items.forEach((item) => observer.observe(item));
286
+ return () => {
287
+ items.forEach((item) => observer.unobserve(item));
288
+ observer.disconnect();
289
+ };
290
+ }, [children, contentHeight]);
291
+ useEffect(() => {
292
+ const originalBodyStyle = {
293
+ touchAction: document.body.style.touchAction,
294
+ overflow: document.body.style.overflow
295
+ };
296
+ document.body.style.touchAction = "none";
297
+ document.body.style.overflow = "hidden";
298
+ return () => {
299
+ document.body.style.touchAction = originalBodyStyle.touchAction;
300
+ document.body.style.overflow = originalBodyStyle.overflow;
301
+ };
302
+ }, []);
303
+ return /* @__PURE__ */ jsxs(
304
+ "div",
305
+ {
306
+ className: "react-sway-container scroller-content",
307
+ ref: containerRef,
308
+ style: {
309
+ transform: `translate3d(0, ${visualPosition}px, 0)`,
310
+ cursor: isDragging ? "grabbing" : "grab",
311
+ position: "absolute",
312
+ width: "100%",
313
+ willChange: "transform",
314
+ WebkitTransform: "translateZ(0)",
315
+ touchAction: "none",
316
+ userSelect: "none",
317
+ WebkitUserSelect: "none",
318
+ msUserSelect: "none",
319
+ MozUserSelect: "none",
320
+ overscrollBehavior: "contain",
321
+ // Ensure it's on top and can receive events
322
+ pointerEvents: "auto",
323
+ zIndex: 1
324
+ },
325
+ tabIndex: 0,
326
+ children: [
327
+ /* @__PURE__ */ jsx("div", { className: "content-group original", children }),
328
+ /* @__PURE__ */ jsx("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children }),
329
+ /* @__PURE__ */ jsx("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children })
330
+ ]
331
+ }
332
+ );
333
+ }
334
+ var ReactSway_default = ReactSway;
335
+ export {
336
+ ReactSway_default as ReactSway
337
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "author": {
3
+ "name": "Mehdy Lafitte",
4
+ "url": "https://github.com/lafittemehdy"
5
+ },
6
+ "bugs": {
7
+ "url": "https://github.com/lafittemehdy/sway/issues"
8
+ },
9
+ "description": "A React component for smooth infinite scrolling, designed for creating engaging, continuous content streams with minimal configuration.",
10
+ "devDependencies": {
11
+ "@types/react": "^18.3.3",
12
+ "@types/react-dom": "^18.3.0",
13
+ "eslint": "^8.57.0",
14
+ "eslint-plugin-react": "^7.35.0",
15
+ "eslint-plugin-react-hooks": "^4.6.2",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1",
18
+ "tsup": "^8.2.3",
19
+ "typescript": "^5.5.4"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "homepage": "https://github.com/lafittemehdy/sway#readme",
26
+ "keywords": [
27
+ "content-stream",
28
+ "continuous-scroll",
29
+ "frontend",
30
+ "infinite-scroll",
31
+ "javascript",
32
+ "react",
33
+ "react-component",
34
+ "scroller",
35
+ "smooth-scroll",
36
+ "typescript",
37
+ "ui-component"
38
+ ],
39
+ "license": "MIT",
40
+ "main": "dist/index.cjs",
41
+ "module": "dist/index.js",
42
+ "name": "react-sway",
43
+ "peerDependencies": {
44
+ "react": ">=16.8.0",
45
+ "react-dom": ">=16.8.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/lafittemehdy/sway.git"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format cjs,esm --dts --external react",
56
+ "dev": "tsup src/index.ts --format cjs,esm --dts --external react --watch",
57
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
58
+ "prepublishOnly": "npm run build"
59
+ },
60
+ "type": "module",
61
+ "types": "dist/index.d.ts",
62
+ "version": "0.1.0"
63
+ }
@@ -0,0 +1,411 @@
1
+ import { useState, useRef, useEffect, useCallback, ReactNode } from 'react';
2
+
3
+ // Constants (hardcoded as per current plan)
4
+ const SCROLL_SPEED = 0.5; // pixels per frame at 60fps
5
+ const INACTIVITY_DELAY = 2000; // ms
6
+ const FRICTION = 0.95;
7
+ const MAX_DELTA_TIME = 3; // Cap deltaTime to prevent physics breaking
8
+
9
+ interface ReactSwayProps {
10
+ children: ReactNode;
11
+ }
12
+
13
+ function ReactSway({ children }: ReactSwayProps) {
14
+ const [position, setPosition] = useState(0);
15
+ const [, setVelocity] = useState(0); // velocity is set but never used directly for rendering, only for physics calcs
16
+ const [isDragging, setIsDragging] = useState(false);
17
+ const [isPaused, setIsPaused] = useState(false);
18
+ const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
19
+ const [isTabActive, setIsTabActive] = useState(true);
20
+
21
+ // Content dimensions
22
+ const [contentHeight, setContentHeight] = useState(0);
23
+ const [loopPoint, setLoopPoint] = useState(0);
24
+ const [, setContainerHeight] = useState(0); // containerHeight is set but never used
25
+
26
+ const containerRef = useRef<HTMLDivElement>(null);
27
+ const animationFrameRef = useRef<number | null>(null);
28
+ const inactivityTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
29
+ const lastTouchYRef = useRef(0);
30
+ const lastMouseYRef = useRef(0);
31
+ const lastFrameTimeRef = useRef(0);
32
+
33
+ // Visual position calculation
34
+ let visualPosition = position % (loopPoint || 1);
35
+ if (visualPosition > 0 && loopPoint > 0) visualPosition -= loopPoint;
36
+
37
+ useEffect(() => {
38
+ const calculateDimensions = () => {
39
+ if (containerRef.current) {
40
+ // Force a reflow to ensure accurate measurements
41
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
42
+ containerRef.current.offsetHeight;
43
+
44
+ const currentContentHeight = containerRef.current.scrollHeight;
45
+ const calculatedLoopPoint = currentContentHeight / 3;
46
+
47
+ console.log('Calculating dimensions:', { currentContentHeight, calculatedLoopPoint });
48
+
49
+ if (currentContentHeight > 0) {
50
+ setContentHeight(currentContentHeight);
51
+ setLoopPoint(calculatedLoopPoint);
52
+ }
53
+
54
+ setContainerHeight(window.innerHeight);
55
+ }
56
+ };
57
+
58
+ // Use RAF to ensure layout is complete
59
+ const rafId = requestAnimationFrame(() => {
60
+ calculateDimensions();
61
+ });
62
+
63
+ return () => {
64
+ cancelAnimationFrame(rafId);
65
+ };
66
+ }, [children]);
67
+
68
+ const pauseAutoScroll = useCallback(() => {
69
+ setAutoScrollEnabled(false);
70
+ if (inactivityTimerRef.current) {
71
+ clearTimeout(inactivityTimerRef.current);
72
+ }
73
+ }, []);
74
+
75
+ const scheduleAutoScrollResume = useCallback(() => {
76
+ if (inactivityTimerRef.current) {
77
+ clearTimeout(inactivityTimerRef.current);
78
+ }
79
+ inactivityTimerRef.current = setTimeout(() => {
80
+ setAutoScrollEnabled(true);
81
+ }, INACTIVITY_DELAY);
82
+ }, []);
83
+
84
+ const handleMouseDown = useCallback((e: globalThis.MouseEvent) => {
85
+ e.preventDefault();
86
+ setIsDragging(true);
87
+ lastMouseYRef.current = e.clientY;
88
+ setVelocity(0);
89
+ pauseAutoScroll();
90
+ }, [pauseAutoScroll]);
91
+
92
+ const handleMouseMove = useCallback((e: globalThis.MouseEvent) => {
93
+ if (!isDragging) return;
94
+ e.preventDefault();
95
+ const deltaY = e.clientY - lastMouseYRef.current;
96
+ setPosition(prev => prev + deltaY);
97
+ setVelocity(deltaY);
98
+ lastMouseYRef.current = e.clientY;
99
+ }, [isDragging]);
100
+
101
+ const handleMouseUp = useCallback((e: globalThis.MouseEvent) => {
102
+ if (!isDragging) return;
103
+ e.preventDefault();
104
+ setIsDragging(false);
105
+ scheduleAutoScrollResume();
106
+ }, [isDragging, scheduleAutoScrollResume]);
107
+
108
+ const handleTouchStart = useCallback((e: globalThis.TouchEvent) => {
109
+ if (e.touches.length === 1) {
110
+ setIsDragging(true);
111
+ lastTouchYRef.current = e.touches[0].clientY;
112
+ setVelocity(0);
113
+ pauseAutoScroll();
114
+ }
115
+ }, [pauseAutoScroll]);
116
+
117
+ const handleTouchMove = useCallback((e: globalThis.TouchEvent) => {
118
+ if (!isDragging || e.touches.length !== 1) return;
119
+ e.preventDefault();
120
+ const touch = e.touches[0];
121
+ const deltaY = touch.clientY - lastTouchYRef.current;
122
+ setPosition(prev => prev + deltaY);
123
+ setVelocity(deltaY);
124
+ lastTouchYRef.current = touch.clientY;
125
+ }, [isDragging]);
126
+
127
+ const handleTouchEnd = useCallback((_e: globalThis.TouchEvent) => { // e is not used
128
+ if (!isDragging) return;
129
+ setIsDragging(false);
130
+ scheduleAutoScrollResume();
131
+ }, [isDragging, scheduleAutoScrollResume]);
132
+
133
+ const handleWheel = useCallback((e: globalThis.WheelEvent) => {
134
+ e.preventDefault();
135
+ setVelocity(prev => prev - e.deltaY * 0.3);
136
+ pauseAutoScroll();
137
+ scheduleAutoScrollResume();
138
+ }, [pauseAutoScroll, scheduleAutoScrollResume]);
139
+
140
+ const togglePause = useCallback(() => {
141
+ setIsPaused(prev => {
142
+ const newPausedState = !prev;
143
+ if (newPausedState) {
144
+ pauseAutoScroll();
145
+ } else {
146
+ setAutoScrollEnabled(true);
147
+ }
148
+ return newPausedState;
149
+ });
150
+ }, [pauseAutoScroll]);
151
+
152
+ const handleKeyDown = useCallback((e: globalThis.KeyboardEvent) => {
153
+ switch (e.key) {
154
+ case ' ':
155
+ e.preventDefault();
156
+ togglePause();
157
+ break;
158
+ case 'ArrowUp':
159
+ e.preventDefault();
160
+ setVelocity(prev => prev + 15);
161
+ pauseAutoScroll();
162
+ scheduleAutoScrollResume();
163
+ break;
164
+ case 'ArrowDown':
165
+ e.preventDefault();
166
+ setVelocity(prev => prev - 15);
167
+ pauseAutoScroll();
168
+ scheduleAutoScrollResume();
169
+ break;
170
+ case 'Home':
171
+ e.preventDefault();
172
+ setPosition(0);
173
+ setVelocity(0);
174
+ pauseAutoScroll();
175
+ scheduleAutoScrollResume();
176
+ break;
177
+ case 'End':
178
+ e.preventDefault();
179
+ if (loopPoint > 0) {
180
+ setPosition(-loopPoint);
181
+ }
182
+ setVelocity(0);
183
+ pauseAutoScroll();
184
+ scheduleAutoScrollResume();
185
+ break;
186
+ default:
187
+ break;
188
+ }
189
+ }, [togglePause, pauseAutoScroll, scheduleAutoScrollResume, loopPoint]);
190
+
191
+ const handleResize = useCallback(() => {
192
+ setContainerHeight(window.innerHeight);
193
+ if (containerRef.current) {
194
+ const currentContentHeight = containerRef.current.scrollHeight;
195
+ setContentHeight(currentContentHeight);
196
+ setLoopPoint(currentContentHeight / 3);
197
+ }
198
+ }, []);
199
+
200
+ useEffect(() => {
201
+ const currentContainer = containerRef.current;
202
+ if (!currentContainer) return;
203
+
204
+ // Create bound event handlers that maintain proper context
205
+ const boundHandlers = {
206
+ mouseDown: handleMouseDown,
207
+ mouseMove: handleMouseMove,
208
+ mouseUp: handleMouseUp,
209
+ touchStart: handleTouchStart,
210
+ touchMove: handleTouchMove,
211
+ touchEnd: handleTouchEnd,
212
+ wheel: handleWheel
213
+ };
214
+
215
+ // Add event listeners
216
+ currentContainer.addEventListener('mousedown', boundHandlers.mouseDown);
217
+ window.addEventListener('mousemove', boundHandlers.mouseMove);
218
+ window.addEventListener('mouseup', boundHandlers.mouseUp);
219
+ currentContainer.addEventListener('touchstart', boundHandlers.touchStart, { passive: true });
220
+ window.addEventListener('touchmove', boundHandlers.touchMove, { passive: false });
221
+ window.addEventListener('touchend', boundHandlers.touchEnd, { passive: true });
222
+ currentContainer.addEventListener('wheel', boundHandlers.wheel, { passive: false });
223
+
224
+ return () => {
225
+ currentContainer.removeEventListener('mousedown', boundHandlers.mouseDown);
226
+ window.removeEventListener('mousemove', boundHandlers.mouseMove);
227
+ window.removeEventListener('mouseup', boundHandlers.mouseUp);
228
+ currentContainer.removeEventListener('touchstart', boundHandlers.touchStart);
229
+ window.removeEventListener('touchmove', boundHandlers.touchMove);
230
+ window.removeEventListener('touchend', boundHandlers.touchEnd);
231
+ currentContainer.removeEventListener('wheel', boundHandlers.wheel);
232
+ };
233
+ }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd, handleWheel]);
234
+
235
+ useEffect(() => {
236
+ document.addEventListener('keydown', handleKeyDown);
237
+ window.addEventListener('resize', handleResize);
238
+
239
+ return () => {
240
+ document.removeEventListener('keydown', handleKeyDown);
241
+ window.removeEventListener('resize', handleResize);
242
+ };
243
+ }, [handleKeyDown, handleResize]);
244
+
245
+ // Tab Visibility Handling
246
+ useEffect(() => {
247
+ const handleVisibilityChange = () => {
248
+ setIsTabActive(!document.hidden);
249
+ if (!document.hidden) {
250
+ lastFrameTimeRef.current = performance.now();
251
+ }
252
+ };
253
+
254
+ document.addEventListener('visibilitychange', handleVisibilityChange);
255
+ return () => {
256
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
257
+ };
258
+ }, []);
259
+
260
+ // Animation Loop
261
+ useEffect(() => {
262
+ if (!isTabActive || isPaused) {
263
+ if (animationFrameRef.current) {
264
+ cancelAnimationFrame(animationFrameRef.current);
265
+ animationFrameRef.current = null;
266
+ }
267
+ return;
268
+ }
269
+
270
+ const animate = (currentTime: number) => {
271
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / 16.667 : 1;
272
+ deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
273
+ lastFrameTimeRef.current = currentTime;
274
+
275
+ setPosition(prevPosition => {
276
+ let newPosition = prevPosition;
277
+
278
+ // Auto-scroll when enabled
279
+ if (autoScrollEnabled && !isDragging && !isPaused) {
280
+ newPosition -= SCROLL_SPEED * deltaTime;
281
+ }
282
+
283
+ return newPosition;
284
+ });
285
+
286
+ setVelocity(prevVelocity => {
287
+ let newVelocity = prevVelocity;
288
+
289
+ if (Math.abs(newVelocity) > 0.1) {
290
+ if (!isDragging) {
291
+ setPosition(prev => prev + newVelocity * deltaTime);
292
+ }
293
+ newVelocity *= Math.pow(FRICTION, deltaTime);
294
+ } else {
295
+ newVelocity = 0;
296
+ }
297
+
298
+ return newVelocity;
299
+ });
300
+
301
+ // Handle position wrapping
302
+ setPosition(prevPosition => {
303
+ let newPosition = prevPosition;
304
+
305
+ if (loopPoint > 0) {
306
+ while (newPosition > 0) {
307
+ newPosition -= loopPoint;
308
+ }
309
+ while (newPosition < -loopPoint * 2) {
310
+ newPosition += loopPoint;
311
+ }
312
+ }
313
+
314
+ return newPosition;
315
+ });
316
+
317
+ animationFrameRef.current = requestAnimationFrame(animate);
318
+ };
319
+
320
+ animationFrameRef.current = requestAnimationFrame(animate);
321
+
322
+ return () => {
323
+ if (animationFrameRef.current) {
324
+ cancelAnimationFrame(animationFrameRef.current);
325
+ }
326
+ };
327
+ }, [isTabActive, autoScrollEnabled, isDragging, isPaused, loopPoint]);
328
+
329
+ // Intersection Observer for lazy loading
330
+ useEffect(() => {
331
+ if (!containerRef.current) return;
332
+
333
+ const observer = new IntersectionObserver(
334
+ (entries) => {
335
+ entries.forEach((entry) => {
336
+ if (entry.isIntersecting) {
337
+ entry.target.classList.add('visible');
338
+ }
339
+ });
340
+ },
341
+ {
342
+ root: null,
343
+ rootMargin: '100px',
344
+ threshold: 0.01,
345
+ }
346
+ );
347
+
348
+ const items = containerRef.current.querySelectorAll('.content-item');
349
+ items.forEach((item) => observer.observe(item));
350
+
351
+ return () => {
352
+ items.forEach((item) => observer.unobserve(item));
353
+ observer.disconnect();
354
+ };
355
+ }, [children, contentHeight]);
356
+
357
+ // Apply styles to override conflicting CSS
358
+ useEffect(() => {
359
+ const originalBodyStyle = {
360
+ touchAction: document.body.style.touchAction,
361
+ overflow: document.body.style.overflow
362
+ };
363
+
364
+ // Override body styles that might conflict
365
+ document.body.style.touchAction = 'none';
366
+ document.body.style.overflow = 'hidden';
367
+
368
+ return () => {
369
+ // Restore original styles
370
+ document.body.style.touchAction = originalBodyStyle.touchAction;
371
+ document.body.style.overflow = originalBodyStyle.overflow;
372
+ };
373
+ }, []);
374
+
375
+ return (
376
+ <div
377
+ className="react-sway-container scroller-content"
378
+ ref={containerRef}
379
+ style={{
380
+ transform: `translate3d(0, ${visualPosition}px, 0)`,
381
+ cursor: isDragging ? 'grabbing' : 'grab',
382
+ position: 'absolute',
383
+ width: '100%',
384
+ willChange: 'transform',
385
+ WebkitTransform: 'translateZ(0)',
386
+ touchAction: 'none',
387
+ userSelect: 'none',
388
+ WebkitUserSelect: 'none',
389
+ msUserSelect: 'none',
390
+ MozUserSelect: 'none',
391
+ overscrollBehavior: 'contain',
392
+ // Ensure it's on top and can receive events
393
+ pointerEvents: 'auto',
394
+ zIndex: 1
395
+ }}
396
+ tabIndex={0}
397
+ >
398
+ <div className="content-group original">
399
+ {children}
400
+ </div>
401
+ <aside className="content-group duplicate" aria-hidden="true" data-duplicate="true" role="presentation">
402
+ {children}
403
+ </aside>
404
+ <aside className="content-group duplicate" aria-hidden="true" data-duplicate="true" role="presentation">
405
+ {children}
406
+ </aside>
407
+ </div>
408
+ );
409
+ }
410
+
411
+ export default ReactSway;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import ReactSway from './ReactSway';
2
+
3
+ export { ReactSway };