react-css-marquee 0.0.12 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ The MIT License (MIT)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,68 +1,129 @@
1
1
  # React CSS Marquee
2
2
 
3
- [Demo](https://codesandbox.io/s/react-css-marquee-demo-577f1)
3
+ [![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://github.com/ellerbrock/typescript-badges/)
4
+ [![npm](https://img.shields.io/npm/v/react-css-marquee)](https://www.npmjs.com/package/react-css-marquee)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/react-css-marquee)](https://bundlephobia.com/package/react-css-marquee)
4
6
 
7
+ A lightweight React marquee component powered by CSS animations. Supports horizontal and vertical scrolling, gradient fade edges, pause controls, auto-fill, loop count, and more -- with zero dependencies.
5
8
 
6
- [![Build Status](https://travis-ci.org/samuelweckstrom/react-css-marquee.svg?branch=master)](https://travis-ci.org/samuelweckstrom/react-css-marquee)
7
- [![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://github.com/ellerbrock/typescript-badges/)
9
+ ## Install
10
+
11
+ ```
12
+ npm install react-css-marquee
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```tsx
18
+ import { Marquee } from 'react-css-marquee'
19
+
20
+ <Marquee>
21
+ <img src="logo1.png" />
22
+ <img src="logo2.png" />
23
+ <img src="logo3.png" />
24
+ </Marquee>
25
+ ```
26
+
27
+ ## Examples
8
28
 
9
- React marquee component for horizontal and vertical scrolling text (see demo link above for example use cases).
29
+ ### Gradient fade edges
10
30
 
11
- <div align="center">
12
- <img style="width: 200px" src="https://s3.eu-central-1.amazonaws.com/samuel.weckstrom.xyz/github/marquee.gif">
13
- </div>
31
+ Uses CSS `mask-image` so it works over any background without specifying a color.
14
32
 
33
+ ```tsx
34
+ <Marquee gradient gradientWidth={200}>
35
+ <div>Item 1</div>
36
+ <div>Item 2</div>
37
+ <div>Item 3</div>
38
+ </Marquee>
39
+ ```
40
+
41
+ ### Vertical scrolling
42
+
43
+ ```tsx
44
+ <Marquee direction="vertical" style={{ height: 400 }}>
45
+ <p>Line one</p>
46
+ <p>Line two</p>
47
+ <p>Line three</p>
48
+ </Marquee>
49
+ ```
50
+
51
+ ### Sparse scrolling (no repeat)
15
52
 
16
- ## How to
53
+ Items scroll across individually without duplicating to fill the viewport.
17
54
 
18
- ### Install
55
+ ```tsx
56
+ <Marquee repeat={false} speed={8}>
57
+ <h2>Breaking news headline</h2>
58
+ </Marquee>
59
+ ```
60
+
61
+ ### Pause on hover
19
62
 
63
+ ```tsx
64
+ <Marquee isPausedOnHover>
65
+ <Card />
66
+ <Card />
67
+ <Card />
68
+ </Marquee>
20
69
  ```
21
- yarn add react-css-marquee
70
+
71
+ ### Drag to scroll
72
+
73
+ Let users grab and scrub through the marquee content. The animation resumes from where the user left off.
74
+
75
+ ```tsx
76
+ <Marquee draggable>
77
+ <img src="photo1.jpg" />
78
+ <img src="photo2.jpg" />
79
+ <img src="photo3.jpg" />
80
+ </Marquee>
22
81
  ```
23
82
 
24
- ### Use
83
+ ### Click to toggle pause
25
84
 
85
+ ```tsx
86
+ <Marquee isPausedOnClick>
87
+ <span>Click anywhere on the marquee to pause / resume</span>
88
+ </Marquee>
26
89
  ```
27
- import Marquee from 'react-css-marquee'
28
90
 
29
- ...
91
+ ### Finite loop with callback
30
92
 
31
- <Marquee text="Your scrolling text here..." />
93
+ ```tsx
94
+ <Marquee loop={3} onFinish={() => console.log('done')}>
95
+ <span>This plays 3 times</span>
96
+ </Marquee>
32
97
  ```
33
98
 
34
- ### Styling
35
-
36
- |Default CSS-classes|
37
- | ------------- |
38
- |`react-css-marquee__container`|
39
- |`react-css-marquee__text`|
40
-
41
- <br>
42
- You can style the components using the default CSS-classes. If you prefer you can also pass your own namespace for CSS classes via props ie. `my-marquee-namespace__container`.
43
- <br>
44
-
45
-
46
- ### Props
47
-
48
- ||
49
- | ------------- |
50
- |`text: string`|
51
- |`cssNamespace?: string`|
52
- |`flip?: boolean`|
53
- |`hoverStop?: boolean`|
54
- |`reverse?: boolean`|
55
- |`size?: number`|
56
- |`spacing?: number`|
57
- |`speed?: number`|
58
- |`vertical?: boolean`|
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
99
+ ## Props
100
+
101
+ | Prop | Type | Default | Description |
102
+ |------|------|---------|-------------|
103
+ | `children` | `ReactNode` | -- | Content to scroll |
104
+ | `className` | `string` | -- | CSS class for the container element |
105
+ | `delay` | `number` | `0` | Delay before animation starts, in seconds |
106
+ | `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Scroll direction |
107
+ | `draggable` | `boolean` | `false` | Let users grab and drag to scrub through the marquee. Animation resumes on release |
108
+ | `repeat` | `boolean` | `true` | Duplicate children to fill the viewport for a seamless loop. Set `false` for spaced-out items that scroll across individually |
109
+ | `gap` | `number` | `40` | Gap between items in pixels |
110
+ | `gradient` | `boolean` | `false` | Fade out edges with a gradient overlay |
111
+ | `gradientWidth` | `number \| string` | `200` | Width of the gradient fade zone (number for px, string for any CSS unit) |
112
+ | `isPaused` | `boolean` | `false` | Pause the animation |
113
+ | `isPausedOnClick` | `boolean` | `false` | Click the marquee to toggle between paused and running |
114
+ | `isPausedOnHover` | `boolean` | `false` | Pause when hovering over the marquee |
115
+ | `isPausedOnMouseDown` | `boolean` | `false` | Pause while the mouse button is held down on the marquee |
116
+ | `loop` | `number` | `0` | Number of times to play the animation. `0` means infinite |
117
+ | `onCycleComplete` | `() => void` | -- | Called each time the animation completes one cycle |
118
+ | `onFinish` | `() => void` | -- | Called when a finite loop animation ends |
119
+ | `reverse` | `boolean` | `false` | Reverse the scroll direction |
120
+ | `speed` | `number` | `10` | Animation duration in seconds (higher = slower) |
121
+ | `style` | `CSSProperties` | -- | Inline styles for the container element |
122
+
123
+ ## Accessibility
124
+
125
+ The component automatically pauses animation when the user has `prefers-reduced-motion: reduce` enabled. Duplicate items rendered for the seamless loop are marked with `aria-hidden` to prevent screen readers from reading them multiple times.
126
+
127
+ ## License
128
+
129
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,16 +1,126 @@
1
- export declare const useWindowSize: () => {
2
- [T: string]: number;
3
- };
4
- declare type MarqueeProps = {
5
- text: string;
6
- cssNamespace?: string;
7
- flip?: boolean;
8
- hoverStop?: boolean;
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+
4
+ type MarqueeProps = {
5
+ /** Content to scroll. Can be any React nodes — images, cards, text, etc. */
6
+ children: ReactNode;
7
+ /** CSS class applied to the outer container element. */
8
+ className?: string;
9
+ /** Seconds to wait before the animation begins. Default: `0`. */
10
+ delay?: number;
11
+ /**
12
+ * Axis along which content scrolls.
13
+ * - `'horizontal'` — scrolls left (or right when `reverse` is true). Default.
14
+ * - `'vertical'` — scrolls up (or down when `reverse` is true).
15
+ * For vertical marquees give the container a fixed height via `style`.
16
+ */
17
+ direction?: 'horizontal' | 'vertical';
18
+ /**
19
+ * Allow users to click-and-drag to scrub through the marquee.
20
+ * The animation resumes from the dragged position on release.
21
+ * Default: `false`.
22
+ */
23
+ draggable?: boolean;
24
+ /**
25
+ * Duplicate children enough times to fill the viewport, creating a
26
+ * seamless infinite loop. Set to `false` for a "ticker" effect where
27
+ * items scroll across the screen individually without repeating.
28
+ * Default: `true`.
29
+ */
30
+ repeat?: boolean;
31
+ /** Gap between items (and between repeated copies) in pixels. Default: `40`. */
32
+ gap?: number;
33
+ /**
34
+ * Fade the leading and trailing edges of the marquee using a CSS
35
+ * `mask-image` gradient. Works over any background colour without
36
+ * needing to know it. Default: `false`.
37
+ */
38
+ gradient?: boolean;
39
+ /**
40
+ * Width of the gradient fade zone on each edge.
41
+ * Pass a `number` for pixels or a CSS string (e.g. `'10%'`).
42
+ * Only applies when `gradient` is `true`. Default: `200` (px).
43
+ */
44
+ gradientWidth?: number | string;
45
+ /** Programmatically pause the animation. Default: `false`. */
46
+ isPaused?: boolean;
47
+ /** Clicking anywhere on the marquee toggles between paused and running. Default: `false`. */
48
+ isPausedOnClick?: boolean;
49
+ /** Pause while the pointer hovers over the marquee. Works on touch via pointer events. Default: `false`. */
50
+ isPausedOnHover?: boolean;
51
+ /** Pause while the mouse button (or touch) is held down on the marquee. Default: `false`. */
52
+ isPausedOnMouseDown?: boolean;
53
+ /**
54
+ * Number of full animation cycles to play before stopping.
55
+ * `0` = infinite (default). Pair with `onFinish` to react when it ends.
56
+ */
57
+ loop?: number;
58
+ /** Called each time the marquee completes one full cycle. */
59
+ onCycleComplete?: () => void;
60
+ /** Called once when a finite (`loop > 0`) animation finishes all its cycles. */
61
+ onFinish?: () => void;
62
+ /**
63
+ * Reverse the scroll direction.
64
+ * Horizontal: scrolls right instead of left.
65
+ * Vertical: scrolls down instead of up.
66
+ * Default: `false`.
67
+ */
9
68
  reverse?: boolean;
10
- size?: number;
11
- spacing?: number;
69
+ /**
70
+ * Controls scroll speed as an animation duration in seconds.
71
+ * A **higher** number means **slower** movement (it's a CSS `animation-duration`).
72
+ * Default: `10` (one full cycle takes 10 seconds).
73
+ */
12
74
  speed?: number;
13
- vertical?: boolean;
75
+ /** Inline styles applied to the outer container element. */
76
+ style?: CSSProperties;
14
77
  };
15
- declare const Marquee: (props: MarqueeProps) => JSX.Element;
16
- export default Marquee;
78
+ /**
79
+ * A lightweight, zero-dependency React marquee powered by CSS animations.
80
+ *
81
+ * Renders children in a continuously scrolling track. By default children are
82
+ * duplicated to fill the viewport for a seamless infinite loop. Set
83
+ * `repeat={false}` for a "breaking news" ticker where each item crosses once.
84
+ *
85
+ * @example Basic horizontal logo strip
86
+ * ```tsx
87
+ * <Marquee gap={32} speed={15}>
88
+ * <img src="logo-a.svg" />
89
+ * <img src="logo-b.svg" />
90
+ * <img src="logo-c.svg" />
91
+ * </Marquee>
92
+ * ```
93
+ *
94
+ * @example Vertical feed with gradient edges, paused on hover
95
+ * ```tsx
96
+ * <Marquee direction="vertical" gradient gradientWidth={80} isPausedOnHover style={{ height: 400 }}>
97
+ * <Card title="Item 1" />
98
+ * <Card title="Item 2" />
99
+ * </Marquee>
100
+ * ```
101
+ *
102
+ * @example Ticker — items scroll across once, no repeat
103
+ * ```tsx
104
+ * <Marquee repeat={false} speed={8}>
105
+ * <h2>Breaking: major update shipped</h2>
106
+ * </Marquee>
107
+ * ```
108
+ *
109
+ * @example Finite loop with completion callback
110
+ * ```tsx
111
+ * <Marquee loop={3} onFinish={() => setDone(true)}>
112
+ * <span>Plays exactly three times</span>
113
+ * </Marquee>
114
+ * ```
115
+ *
116
+ * @example Draggable photo strip
117
+ * ```tsx
118
+ * <Marquee draggable gap={16}>
119
+ * <img src="photo1.jpg" />
120
+ * <img src="photo2.jpg" />
121
+ * </Marquee>
122
+ * ```
123
+ */
124
+ declare const Marquee: ({ children, className, delay, direction, draggable, repeat, gap, gradient, gradientWidth, isPaused, isPausedOnClick, isPausedOnHover, isPausedOnMouseDown, loop, onCycleComplete, onFinish, reverse, speed, style: userStyle, }: MarqueeProps) => react_jsx_runtime.JSX.Element;
125
+
126
+ export { Marquee, type MarqueeProps };
package/dist/index.js CHANGED
@@ -1,88 +1,237 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- exports.__esModule = true;
6
- exports.useWindowSize = void 0;
7
- var react_1 = __importDefault(require("react"));
8
- var createHash = function () { return "" + Math.random().toString(36).substring(7); };
9
- exports.useWindowSize = function () {
10
- var getSize = function () { return ({
11
- width: window.innerWidth,
12
- height: window.innerHeight
13
- }); };
14
- var _a = react_1["default"].useState(getSize), windowSize = _a[0], setWindowSize = _a[1];
15
- react_1["default"].useLayoutEffect(function () {
16
- var onResize = function () { return setWindowSize(getSize); };
17
- window.addEventListener('resize', onResize);
18
- return function () { return window.removeEventListener('resize', onResize); };
19
- }, []);
20
- return windowSize;
21
- };
22
- var DIRECTION = {
23
- LEFT: 'left',
24
- REVERSE: 'reverse'
25
- };
26
- var ORIENTATION = {
27
- RIGHT_TOP: 'right top',
28
- TOP_LEFT: 'top left',
29
- LEFT_TOP: 'left top'
30
- };
31
- var Marquee = function (props) {
32
- var _a = react_1["default"].useState(0), height = _a[0], setHeight = _a[1];
33
- var _b = react_1["default"].useState(0), width = _b[0], setWidth = _b[1];
34
- var _c = react_1["default"].useState(false), isMouseOver = _c[0], setIsMouseOver = _c[1];
35
- var hash = react_1["default"].useMemo(createHash, []);
36
- var windowSize = exports.useWindowSize();
37
- var ref = react_1["default"].useCallback(function (node) {
38
- if (node) {
39
- var parentHeight = parseInt(window.getComputedStyle(node.parentNode).height.slice(0, -2));
40
- var parentWidth = parseInt(window.getComputedStyle(node.parentNode).width.slice(0, -2));
41
- var size = parseInt(window.getComputedStyle(node).fontSize.slice(0, -2));
42
- setHeight(parentHeight);
43
- setWidth(parentWidth);
44
- }
45
- }, [windowSize]);
46
- var SPEED = props.speed || 5;
47
- var CSS_NAMESPACE = props.cssNamespace || 'react-css-marquee';
48
- var ANIMATION_DIRECTION = props.reverse
49
- ? DIRECTION.REVERSE
50
- : DIRECTION.LEFT;
51
- var ROTATION = props.vertical && !props.flip
52
- ? 'rotate(90deg)'
53
- : props.vertical && props.flip
54
- ? 'rotate(-90deg)'
55
- : 'rotate(0deg)';
56
- var MAX_DURATION = 10;
57
- var ANIMATION_DURATION = (props.vertical ? height : width) / SPEED / MAX_DURATION;
58
- var CONTAINER_ALIGN = props.vertical && !props.flip
59
- ? 'translateX(5%)'
60
- : props.vertical && props.flip
61
- ? "translateX(-" + height + "px)"
62
- : 'translateX(0)';
63
- var TRANSFORM_ORIGIN = props.vertical && !props.flip
64
- ? ''
65
- : props.vertical && props.flip
66
- ? ORIENTATION.TOP_LEFT
67
- : '';
68
- var ITEM_WIDTH = props.vertical ? height + "px" : width + "px";
69
- var ITEM_HEIGHT = 'auto';
70
- var SPACING = props.spacing || 4;
71
- var TEXT = props.text || 'REACT CSS MARQUEE';
72
- var marqueeText = ("" + TEXT + ' '.repeat(SPACING)).repeat((props.vertical ? height : width) / (SPACING + TEXT.length) / 6);
73
- var handleMouseOver = function (event) {
74
- if (props.hoverStop) {
75
- var isMouseEnter = event.type === 'mouseenter';
76
- setIsMouseOver(isMouseEnter);
1
+ import { useState, useRef, useEffect, useMemo } from 'react';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/index.tsx
5
+ var instanceCounter = 0;
6
+ var Marquee = ({
7
+ children,
8
+ className,
9
+ delay = 0,
10
+ direction = "horizontal",
11
+ draggable = false,
12
+ repeat = true,
13
+ gap = 40,
14
+ gradient = false,
15
+ gradientWidth = 200,
16
+ isPaused = false,
17
+ isPausedOnClick = false,
18
+ isPausedOnHover = false,
19
+ isPausedOnMouseDown = false,
20
+ loop = 0,
21
+ onCycleComplete,
22
+ onFinish,
23
+ reverse = false,
24
+ speed = 10,
25
+ style: userStyle
26
+ }) => {
27
+ const [uid] = useState(() => `rcm${++instanceCounter}`);
28
+ const horiz = direction === "horizontal";
29
+ const containerRef = useRef(null);
30
+ const groupRef = useRef(null);
31
+ const trackRef = useRef(null);
32
+ const [sizes, setSizes] = useState({ container: 0, group: 0 });
33
+ const [isDragging, setIsDragging] = useState(false);
34
+ const [clickPaused, setClickPaused] = useState(false);
35
+ const [touchHoverPaused, setTouchHoverPaused] = useState(false);
36
+ const scrubRef = useRef(0);
37
+ useEffect(() => {
38
+ const container = containerRef.current;
39
+ const group = groupRef.current;
40
+ if (!container || !group) return;
41
+ const ro = new ResizeObserver((entries) => {
42
+ setSizes((prev) => {
43
+ let next = prev;
44
+ for (const entry of entries) {
45
+ const s = horiz ? entry.contentRect.width : entry.contentRect.height;
46
+ if (entry.target === container) next = { ...next, container: s };
47
+ if (entry.target === group) next = { ...next, group: s };
77
48
  }
49
+ return next;
50
+ });
51
+ });
52
+ ro.observe(container);
53
+ ro.observe(group);
54
+ return () => ro.disconnect();
55
+ }, [horiz]);
56
+ useEffect(() => {
57
+ const container = containerRef.current;
58
+ if (!container || !isPausedOnHover) return;
59
+ const onEnter = (e) => {
60
+ if (e.pointerType !== "touch") return;
61
+ setTouchHoverPaused(true);
62
+ };
63
+ const onLeave = (e) => {
64
+ if (e.pointerType !== "touch") return;
65
+ setTouchHoverPaused(false);
66
+ };
67
+ container.addEventListener("pointerenter", onEnter);
68
+ container.addEventListener("pointerleave", onLeave);
69
+ return () => {
70
+ container.removeEventListener("pointerenter", onEnter);
71
+ container.removeEventListener("pointerleave", onLeave);
72
+ };
73
+ }, [isPausedOnHover]);
74
+ useEffect(() => {
75
+ const track = trackRef.current;
76
+ if (!track || !onCycleComplete && !onFinish) return;
77
+ const onIteration = onCycleComplete ? () => onCycleComplete() : void 0;
78
+ const onEnd = onFinish ? () => onFinish() : void 0;
79
+ if (onIteration) track.addEventListener("animationiteration", onIteration);
80
+ if (onEnd) track.addEventListener("animationend", onEnd);
81
+ return () => {
82
+ if (onIteration)
83
+ track.removeEventListener("animationiteration", onIteration);
84
+ if (onEnd) track.removeEventListener("animationend", onEnd);
85
+ };
86
+ }, [onCycleComplete, onFinish]);
87
+ const totalDistance = repeat ? sizes.group + gap : sizes.container + sizes.group;
88
+ useEffect(() => {
89
+ const container = containerRef.current;
90
+ if (!container) return;
91
+ if (!draggable && !isPausedOnClick) return;
92
+ let dragging = false;
93
+ let startPos = 0;
94
+ let startScrub = 0;
95
+ let wasDragged = false;
96
+ const onPointerDown = (e) => {
97
+ if (!draggable) return;
98
+ dragging = true;
99
+ wasDragged = false;
100
+ startPos = horiz ? e.clientX : e.clientY;
101
+ startScrub = scrubRef.current;
102
+ container.setPointerCapture(e.pointerId);
103
+ setIsDragging(true);
104
+ };
105
+ const onPointerMove = (e) => {
106
+ if (!dragging || totalDistance <= 0) return;
107
+ const pos = horiz ? e.clientX : e.clientY;
108
+ const delta = pos - startPos;
109
+ if (Math.abs(delta) > 3) wasDragged = true;
110
+ const timeDelta = -delta / totalDistance * speed;
111
+ scrubRef.current = startScrub + timeDelta;
112
+ if (trackRef.current) {
113
+ trackRef.current.style.animationDelay = `${delay - scrubRef.current}s`;
114
+ }
115
+ };
116
+ const onPointerUp = () => {
117
+ if (!dragging) return;
118
+ dragging = false;
119
+ setIsDragging(false);
120
+ };
121
+ const onClick = () => {
122
+ if (wasDragged) {
123
+ wasDragged = false;
124
+ return;
125
+ }
126
+ if (isPausedOnClick) setClickPaused((p) => !p);
127
+ };
128
+ container.addEventListener("pointerdown", onPointerDown);
129
+ container.addEventListener("pointermove", onPointerMove);
130
+ container.addEventListener("pointerup", onPointerUp);
131
+ container.addEventListener("pointercancel", onPointerUp);
132
+ container.addEventListener("click", onClick);
133
+ return () => {
134
+ container.removeEventListener("pointerdown", onPointerDown);
135
+ container.removeEventListener("pointermove", onPointerMove);
136
+ container.removeEventListener("pointerup", onPointerUp);
137
+ container.removeEventListener("pointercancel", onPointerUp);
138
+ container.removeEventListener("click", onClick);
78
139
  };
79
- return (react_1["default"].createElement(react_1["default"].Fragment, null,
80
- react_1["default"].createElement("style", null, "\n @keyframes " + CSS_NAMESPACE + "__animation-" + hash + " {\n 0% {\n transform: translateX(0%);\n }\n 100% {\n transform: translateX(-100%);\n }\n }\n\n ." + CSS_NAMESPACE + "__wrapper-" + hash + " { \n box-sizing: border-box;\n user-select: none;\n }\n\n ." + CSS_NAMESPACE + "__rotation-" + hash + " {\n transform: " + ROTATION + " " + CONTAINER_ALIGN + ";\n transform-origin: " + TRANSFORM_ORIGIN + ";\n will-change: transform;\n pointer-events: none;\n }\n\n ." + CSS_NAMESPACE + "__container-" + hash + " {\n height: " + ITEM_HEIGHT + ";\n width: " + ITEM_WIDTH + ";\n display: flex;\n flex-flow: row nowrap;\n backface-visibility: hidden;\n perspective: 1000px;\n overflow: hidden;\n font-size: 16px;\n pointer-events: none; \n }\n\n ." + CSS_NAMESPACE + "__text-" + hash + " {\n align-self: center;\n text-rendering: optimizeLegibility;\n transform: translateZ(0);\n animation-name: " + CSS_NAMESPACE + "__animation-" + hash + ";\n animation-timing-function: linear;\n animation-iteration-count: infinite;\n animation-direction: " + ANIMATION_DIRECTION + ";\n animation-duration: " + ANIMATION_DURATION + "s;\n animation-play-state: " + (isMouseOver ? 'paused' : 'initial') + ";\n white-space: pre;\n will-change: transform;\n pointer-events: none;\n }\n "),
81
- react_1["default"].createElement("div", { ref: ref, onMouseEnter: handleMouseOver, onMouseOut: handleMouseOver, className: CSS_NAMESPACE + "__wrapper-" + hash + " " + CSS_NAMESPACE + "__wrapper" },
82
- react_1["default"].createElement("div", { className: CSS_NAMESPACE + "__rotation-" + hash },
83
- react_1["default"].createElement("div", { className: CSS_NAMESPACE + "__container-" + hash + " " + CSS_NAMESPACE + "__container" },
84
- react_1["default"].createElement("div", { className: CSS_NAMESPACE + "__text-" + hash + " " + CSS_NAMESPACE + "__text" }, marqueeText),
85
- react_1["default"].createElement("div", { className: CSS_NAMESPACE + "__text-" + hash + " " + CSS_NAMESPACE + "__text" }, marqueeText))))));
140
+ }, [draggable, isPausedOnClick, horiz, totalDistance, speed, delay]);
141
+ const copies = repeat && sizes.group > 0 ? Math.ceil(sizes.container / sizes.group) + 1 : 1;
142
+ const active = sizes.group > 0 && sizes.container > 0;
143
+ const paused = isPaused || isPausedOnClick && clickPaused || isDragging || touchHoverPaused;
144
+ const css = useMemo(() => {
145
+ let s;
146
+ if (repeat) {
147
+ const dist = sizes.group + gap;
148
+ const dx = horiz ? `-${dist}px` : "0";
149
+ const dy = horiz ? "0" : `-${dist}px`;
150
+ s = `@keyframes ${uid}{to{transform:translate(${dx},${dy})}}`;
151
+ } else {
152
+ const fromVal = `${sizes.container}px`;
153
+ const toVal = `-${sizes.group}px`;
154
+ const fromX = horiz ? fromVal : "0";
155
+ const fromY = horiz ? "0" : fromVal;
156
+ const toX = horiz ? toVal : "0";
157
+ const toY = horiz ? "0" : toVal;
158
+ s = `@keyframes ${uid}{from{transform:translate(${fromX},${fromY})}to{transform:translate(${toX},${toY})}}`;
159
+ }
160
+ if (isPausedOnHover) {
161
+ s += `[data-rcm="${uid}"]:hover [data-rcm-t]{animation-play-state:paused!important}`;
162
+ }
163
+ if (isPausedOnMouseDown) {
164
+ s += `[data-rcm="${uid}"]:active [data-rcm-t]{animation-play-state:paused!important}`;
165
+ }
166
+ s += `@media(prefers-reduced-motion:reduce){[data-rcm="${uid}"] [data-rcm-t]{animation-play-state:paused!important}}`;
167
+ return s;
168
+ }, [uid, isPausedOnHover, isPausedOnMouseDown, sizes, gap, horiz, repeat]);
169
+ const gapStyle = `${gap}px`;
170
+ const gwPx = typeof gradientWidth === "number" ? `${gradientWidth}px` : gradientWidth;
171
+ const maskDir = horiz ? "to right" : "to bottom";
172
+ const mask = gradient ? `linear-gradient(${maskDir}, transparent, black ${gwPx}, black calc(100% - ${gwPx}), transparent)` : void 0;
173
+ const animDelay = delay - scrubRef.current;
174
+ return /* @__PURE__ */ jsxs(
175
+ "div",
176
+ {
177
+ ref: containerRef,
178
+ "data-rcm": uid,
179
+ className,
180
+ style: {
181
+ overflow: "hidden",
182
+ width: "100%",
183
+ height: "100%",
184
+ ...mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0,
185
+ ...draggable ? {
186
+ cursor: isDragging ? "grabbing" : "grab",
187
+ userSelect: "none",
188
+ // Allow scrolling in the perpendicular axis so the page
189
+ // remains scrollable when the marquee is draggable.
190
+ touchAction: horiz ? "pan-y" : "pan-x"
191
+ } : isPausedOnClick ? { cursor: "pointer" } : void 0,
192
+ ...userStyle
193
+ },
194
+ children: [
195
+ /* @__PURE__ */ jsx("style", { children: css }),
196
+ /* @__PURE__ */ jsx(
197
+ "div",
198
+ {
199
+ ref: trackRef,
200
+ "data-rcm-t": "",
201
+ style: {
202
+ display: "flex",
203
+ flexDirection: horiz ? "row" : "column",
204
+ alignItems: "center",
205
+ width: horiz ? "max-content" : "100%",
206
+ height: horiz ? "100%" : "max-content",
207
+ gap: gapStyle,
208
+ animation: active ? `${uid} ${speed}s linear ${loop === 0 ? "infinite" : loop}` : "none",
209
+ animationPlayState: paused ? "paused" : "running",
210
+ animationDirection: reverse ? "reverse" : "normal",
211
+ animationDelay: animDelay ? `${animDelay}s` : void 0,
212
+ willChange: active ? "transform" : void 0
213
+ },
214
+ children: Array.from({ length: copies }, (_, i) => /* @__PURE__ */ jsx(
215
+ "div",
216
+ {
217
+ ref: i === 0 ? groupRef : void 0,
218
+ "aria-hidden": i > 0 || void 0,
219
+ style: {
220
+ display: "flex",
221
+ flexDirection: horiz ? "row" : "column",
222
+ alignItems: "center",
223
+ flexShrink: 0,
224
+ gap: gapStyle
225
+ },
226
+ children
227
+ },
228
+ i
229
+ ))
230
+ }
231
+ )
232
+ ]
233
+ }
234
+ );
86
235
  };
87
- exports["default"] = Marquee;
88
- //# sourceMappingURL=index.js.map
236
+
237
+ export { Marquee };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-css-marquee",
3
- "description": "Scrolling text / marquee component for React",
4
- "version": "0.0.12",
3
+ "description": "Lightweight React marquee component powered by CSS animations. Horizontal and vertical scrolling, gradient fade, pause on hover/click, auto-fill, and more.",
4
+ "version": "1.0.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "url": "https://github.com/samuelweckstrom/react-css-marquee",
@@ -11,25 +11,45 @@
11
11
  },
12
12
  "license": "MIT",
13
13
  "keywords": [
14
+ "react",
14
15
  "marquee",
16
+ "react-marquee",
17
+ "scroll",
18
+ "scrolling",
19
+ "infinite-scroll",
15
20
  "ticker",
21
+ "news-ticker",
16
22
  "scroller",
17
- "scrolling",
18
- "scrolling text",
23
+ "horizontal-scroll",
24
+ "vertical-scroll",
25
+ "auto-scroll",
26
+ "css-animation",
19
27
  "banner",
20
- "react",
21
- "css"
28
+ "carousel",
29
+ "react-component",
30
+ "typescript",
31
+ "gradient",
32
+ "pause-on-hover",
33
+ "infinite-loop",
34
+ "lightweight"
22
35
  ],
23
36
  "files": [
24
37
  "dist",
25
38
  "README.md"
26
39
  ],
27
- "scripts": {
28
- "start": "tsc -w",
29
- "build": "tsc -b"
30
- },
31
40
  "peerDependencies": {
32
- "react": "^16.8",
33
- "react-dom": "^16.8"
41
+ "react": "^16.8 || ^17 || ^18 || ^19"
42
+ },
43
+ "sideEffects": false,
44
+ "type": "module",
45
+ "exports": {
46
+ ".": {
47
+ "import": "./dist/index.js",
48
+ "types": "./dist/index.d.ts"
49
+ }
50
+ },
51
+ "scripts": {
52
+ "dev": "tsup --watch",
53
+ "build": "tsup"
34
54
  }
35
- }
55
+ }
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}