rocket-cursor-component 1.1.1 → 2.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Tomáš Dinh
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tomáš Dinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,85 +1,122 @@
1
- # Rocket Cursor Component
2
-
3
- A customizable React component that replaces the mouse cursor with an animated rocket that rotates based on movement and displays a flame effect when in motion.
4
-
5
- ## Installation
6
-
7
- Install the package via npm:
8
-
9
- ```bash
10
- npm install rocket-cursor-component
11
- ```
12
-
13
- ## Usage
14
-
15
- Here's an example of how to use the `RocketCursor` component in your React app:
16
-
17
- ```tsx
18
- import React from "react";
19
- import RocketCursor from "rocket-cursor-component";
20
-
21
- function App() {
22
- return (
23
- <div>
24
- <h1>Your app content here</h1>
25
- <RocketCursor size={60} threshold={15} flameHideTimeout={300} />
26
- </div>
27
- );
28
- }
29
-
30
- export default App;
31
- ```
32
-
33
- ### Props
34
-
35
- | Prop | Type | Default | Description |
36
- | ------------------ | ------- | ------- | ---------------------------------------------------------- |
37
- | `size` | number | `50` | The size of the rocket cursor in pixels. |
38
- | `threshold` | number | `10` | Minimum distance (pixels) to move before the rocket rotates. |
39
- | `isVisible` | boolean | `true` | Initial visibility state of the rocket cursor. |
40
- | `flameHideTimeout` | number | `300` | Time in milliseconds before the flame hides after stopping.|
41
-
42
- ## Features
43
-
44
- - **Custom Cursor**: Replaces the default mouse cursor with a rocket that follows the cursor.
45
- - **Rotation**: The rocket rotates in the direction of the cursor movement.
46
- - **Flame Effect**: The rocket displays a flame animation when the cursor is moving.
47
- - **Customizable**: Easily adjust the size, rotation threshold, visibility, and flame disappearance time.
48
- - **Element-Specific Visibility**: Automatically hides the rocket cursor over elements with the class `no-rocket-cursor`.
49
-
50
- ## Demo
51
-
52
- Here's a demo of the Rocket Cursor in action:
53
-
54
- ![Rocket Cursor Demo](https://github.com/No898/RocketCursor/raw/main/assets/rocket-cursor-demo.gif)
55
-
56
- ## Changelog
57
-
58
-
59
- ### 1.1.1
60
- - Fixed a typo in README.md.
61
-
62
- ### 1.1.0
63
- - Refactored SVG into separate components.
64
- - Added `flameHideTimeout` prop for configurable flame duration.
65
- - Improved code structure and efficiency.
66
-
67
- ### 1.0.9
68
-
69
- - Added support to hide the Rocket Cursor on elements with the class `no-rocket-cursor`.
70
-
71
- ### 1.0.2
72
-
73
- - Added demo GIF in the README file.
74
-
75
- ### 1.0.1
76
-
77
- - Initial release of the Rocket Cursor component.
78
-
79
- ## Author
80
-
81
- [No898](https://github.com/No898)
82
-
83
- ## License
84
-
85
- This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
1
+ # Rocket Cursor Component
2
+
3
+ A customizable React 19+ component that replaces the mouse cursor with an animated rocket that rotates based on movement and displays a flame effect when in motion.
4
+
5
+ ## Installation
6
+
7
+ Install the package via npm:
8
+
9
+ ```bash
10
+ npm install rocket-cursor-component
11
+ ```
12
+
13
+ ## Requirements
14
+
15
+ - **React >= 19.0.0** (Required)
16
+ - **React DOM >= 19.0.0** (Required)
17
+ - Node.js >= 16.0.0
18
+
19
+ > This package is built specifically for React 19+ and uses the latest React features including `useId()` for better performance and collision prevention.
20
+
21
+ ## Usage
22
+
23
+ Here's an example of how to use the `RocketCursor` component in your React app:
24
+
25
+ ```tsx
26
+ import React from "react";
27
+ import RocketCursor from "rocket-cursor-component";
28
+
29
+ function App() {
30
+ return (
31
+ <div>
32
+ <h1>Your app content here</h1>
33
+ {/* Basic usage - cursor replaced with rocket */}
34
+ <RocketCursor />
35
+
36
+ {/* Advanced usage - rocket centered on cursor with normal cursor visible */}
37
+ <RocketCursor
38
+ size={60}
39
+ threshold={15}
40
+ flameHideTimeout={300}
41
+ hideCursor={false} // Keep normal cursor visible
42
+ offsetX={-15} // Fine-tune rocket position
43
+ offsetY={0}
44
+ />
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export default App;
50
+ ```
51
+
52
+ ### Props
53
+
54
+ | Prop | Type | Default | Description |
55
+ | ------------------ | ------- | ------- | ---------------------------------------------------------- |
56
+ | `size` | number | `50` | The size of the rocket cursor in pixels. |
57
+ | `threshold` | number | `10` | Minimum distance (pixels) to move before the rocket rotates. |
58
+ | `isVisible` | boolean | `true` | Initial visibility state of the rocket cursor. |
59
+ | `flameHideTimeout` | number | `300` | Time in milliseconds before the flame hides after stopping.|
60
+ | `hideCursor` | boolean | `false` | Whether to hide the normal cursor (true) or show both. |
61
+ | `offsetX` | number | `-15` | Horizontal offset in pixels from cursor position. |
62
+ | `offsetY` | number | `0` | Vertical offset in pixels from cursor position. |
63
+
64
+ ## Features
65
+
66
+ - **React 19+ Optimized**: Built specifically for React 19+ with latest performance optimizations
67
+ - **Dual Cursor Mode**: Choose to replace cursor completely or show rocket alongside normal cursor
68
+ - **Custom Cursor**: Replaces the default mouse cursor with a rocket that follows the cursor
69
+ - **Smart Rotation**: The rocket rotates in the direction of cursor movement with configurable threshold
70
+ - **Flame Effect**: Dynamic flame animation when the cursor is moving
71
+ - **Collision-Free**: Uses React 19's `useId()` to prevent SVG gradient ID collisions
72
+ - **Customizable**: Easily adjust size, rotation threshold, visibility, positioning, and flame duration
73
+ - **Element-Specific Visibility**: Automatically hides the rocket cursor over elements with the class `no-rocket-cursor`
74
+ - **Performance Optimized**: Uses `requestAnimationFrame` and hardware acceleration for smooth animations
75
+ - **TypeScript**: Full TypeScript support with proper type definitions
76
+
77
+ ## Demo
78
+
79
+ Here's a demo of the Rocket Cursor in action:
80
+
81
+ ![Rocket Cursor Demo](https://github.com/No898/RocketCursor/raw/main/assets/rocket-cursor-demo.gif)
82
+
83
+ ## Changelog
84
+
85
+ ### 2.0.0 (React 19+ Only)
86
+ - **BREAKING**: Now requires React 19.0.0 or higher
87
+ - **NEW**: Added `useId()` for unique SVG gradient IDs (prevents collisions)
88
+ - **NEW**: Added `hideCursor` prop for dual cursor mode
89
+ - **NEW**: Added `offsetX` and `offsetY` props for precise positioning
90
+ - Fixed all TypeScript type issues and removed unnecessary type casting
91
+ - Improved performance with better dependency management
92
+ - Removed unused props (`followDistance`, `followSpeed`)
93
+ - Added SSR safety checks for `window` object
94
+ - Enhanced code structure with React 19 best practices
95
+
96
+ ### 1.1.1
97
+ - Fixed a typo in README.md.
98
+
99
+ ### 1.1.0
100
+ - Refactored SVG into separate components.
101
+ - Added `flameHideTimeout` prop for configurable flame duration.
102
+ - Improved code structure and efficiency.
103
+
104
+ ### 1.0.9
105
+
106
+ - Added support to hide the Rocket Cursor on elements with the class `no-rocket-cursor`.
107
+
108
+ ### 1.0.2
109
+
110
+ - Added demo GIF in the README file.
111
+
112
+ ### 1.0.1
113
+
114
+ - Initial release of the Rocket Cursor component.
115
+
116
+ ## Author
117
+
118
+ [No898](https://github.com/No898)
119
+
120
+ ## License
121
+
122
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
@@ -1,9 +1,12 @@
1
- import React from "react";
2
- interface RocketCursorProps {
3
- size?: number;
4
- threshold?: number;
5
- isVisible?: boolean;
6
- flameHideTimeout?: number;
7
- }
8
- declare const RocketCursor: React.FC<RocketCursorProps>;
9
- export default RocketCursor;
1
+ import React from "react";
2
+ type Props = {
3
+ size?: number;
4
+ threshold?: number;
5
+ flameHideTimeout?: number;
6
+ isVisible?: boolean;
7
+ hideCursor?: boolean;
8
+ offsetX?: number;
9
+ offsetY?: number;
10
+ };
11
+ declare const RocketCursor: React.FC<Props>;
12
+ export default RocketCursor;
@@ -1,109 +1,135 @@
1
- import { useState, useEffect, useCallback, useRef } from "react";
2
- import React from "react";
3
- // Komponenta pro SVG Plamene
4
- const FlameSvg = () => (React.createElement("g", { transform: "translate(1.2245, 350.449) rotate(45) scale(0.5, -0.5)" },
5
- React.createElement("defs", null,
6
- React.createElement("linearGradient", { id: "fireGradient", gradientUnits: "userSpaceOnUse", x1: "94.141", y1: "255", x2: "94.141", y2: "0.188" },
7
- React.createElement("stop", { offset: "0", stopColor: "#ff4c0d" }),
8
- React.createElement("stop", { offset: "1", stopColor: "#fc9502" }))),
9
- React.createElement("path", { d: "M187.899,164.809 C185.803,214.868 144.574,254.812 94.000,254.812 C42.085,254.812 -0.000,211.312 -0.000,160.812 C-0.000,154.062 -0.121,140.572 10.000,117.812 C16.057,104.191 19.856,95.634 22.000,87.812 C23.178,83.513 25.469,76.683 32.000,87.812 C35.851,94.374 36.000,103.812 36.000,103.812 C36.000,103.812 50.328,92.817 60.000,71.812 C74.179,41.019 62.866,22.612 59.000,9.812 C57.662,5.384 56.822,-2.574 66.000,0.812 C75.352,4.263 100.076,21.570 113.000,39.812 C131.445,65.847 138.000,90.812 138.000,90.812 C138.000,90.812 143.906,83.482 146.000,75.812 C148.365,67.151 148.400,58.573 155.999,67.813 C163.226,76.600 173.959,93.113 180.000,108.812 C190.969,137.321 187.899,164.809 187.899,164.809 Z", fill: "url(#fireGradient)", fillRule: "evenodd" }),
10
- React.createElement("path", { d: "M94.000,254.812 C58.101,254.812 29.000,225.711 29.000,189.812 C29.000,168.151 37.729,155.000 55.896,137.166 C67.528,125.747 78.415,111.722 83.042,102.172 C83.953,100.292 86.026,90.495 94.019,101.966 C98.212,107.982 104.785,118.681 109.000,127.812 C116.266,143.555 118.000,158.812 118.000,158.812 C118.000,158.812 125.121,154.616 130.000,143.812 C131.573,140.330 134.753,127.148 143.643,140.328 C150.166,150.000 159.127,167.390 159.000,189.812 C159.000,225.711 129.898,254.812 94.000,254.812 Z", fill: "#fc9502", fillRule: "evenodd" }),
11
- React.createElement("path", { d: "M95.000,183.812 C104.250,183.812 104.250,200.941 116.000,223.812 C123.824,239.041 112.121,254.812 95.000,254.812 C77.879,254.812 69.000,240.933 69.000,223.812 C69.000,206.692 85.750,183.812 95.000,183.812 Z", fill: "#fce202", fillRule: "evenodd" })));
12
- // Komponenta pro SVG Rakety
13
- const RocketSvg = () => (React.createElement("g", { transform: "translate(0, 0)" },
14
- React.createElement("path", { style: { fill: "#FF7124" }, d: "M399.76,16.699c10.12,37.84,8.67,78.13-4.34,115.28h-0.01L284.48,21.049v-0.01 C321.63,8.029,361.92,6.579,399.76,16.699z" }),
15
- React.createElement("path", { style: { fill: "#F2D59F" }, d: "M90.21,207.929l87.14-101.42h0.01l33.71-39.24c21.43-21.43,46.6-36.84,73.41-46.23v0.01 l110.93,110.93h0.01c-9.39,26.81-24.8,51.98-46.23,73.41l-39.24,33.71l-101.43,87.14l-29.57-29.57l-29.58-29.58l-29.58-29.58 L90.21,207.929z M296.11,193.399c20.18-20.17,20.18-52.89,0-73.06c-20.17-20.18-52.89-20.18-73.06,0 c-20.18,20.17-20.18,52.89,0,73.06C243.22,213.579,275.94,213.579,296.11,193.399z" }),
16
- React.createElement("path", { style: { fill: "#F2D59F" }, d: "M309.95,239.099c1.74,45.6-14.8,91.78-49.61,126.59c-10.69,10.68-22.44,19.65-34.93,26.89 l-16.89-66.34L309.95,239.099z" }),
17
- React.createElement("path", { style: { fill: "#8ECAC1" }, d: "M296.11,120.339c20.18,20.17,20.18,52.89,0,73.06c-20.17,20.18-52.89,20.18-73.06,0 c-20.18-20.17-20.18-52.89,0-73.06C243.22,100.159,275.94,100.159,296.11,120.339z" }),
18
- React.createElement("path", { style: { fill: "#E6B263" }, d: "M208.52,326.239l-39.94,14.71c-10.98,4.05-23.31,1.34-31.58-6.94l-6.85-6.85l48.8-30.49 L208.52,326.239z" }),
19
- React.createElement("polygon", { style: { fill: "#E6B263" }, points: "178.95,296.669 130.15,327.159 130.14,327.159 109.72,306.739 149.37,267.089" }),
20
- React.createElement("path", { style: { fill: "#F2D59F" }, d: "M177.35,106.509l-87.14,101.42l-66.33-16.88c7.24-12.49,16.21-24.24,26.89-34.93 C85.58,121.309,131.74,104.769,177.35,106.509z" }),
21
- React.createElement("polygon", { style: { fill: "#E6B263" }, points: "149.37,267.089 109.72,306.739 89.3,286.309 119.79,237.509" }),
22
- React.createElement("path", { style: { fill: "#E6B263" }, d: "M119.79,237.509l-30.49,48.8l-6.86-6.85c-8.27-8.28-10.98-20.6-6.94-31.58l14.71-39.95 L119.79,237.509z" })));
23
- const RocketCursor = ({ size = 50, threshold = 10, isVisible = true, flameHideTimeout = 300, }) => {
24
- const [position, setPosition] = useState({ x: -size, y: -size });
25
- const [angle, setAngle] = useState(0);
26
- const [isMoving, setIsMoving] = useState(false);
27
- const [visible, setVisible] = useState(isVisible);
28
- const lastSignificantPosition = useRef({ x: 0, y: 0 });
29
- const lastMoveTimestamp = useRef(Date.now());
30
- const timeoutRef = useRef(null);
31
- const handleMouseMove = useCallback((e) => {
32
- const target = e.target;
33
- if (target.closest(".no-rocket-cursor")) {
34
- setVisible(false);
35
- return;
36
- }
37
- setVisible(true);
38
- const currentPosition = { x: e.clientX, y: e.clientY };
39
- setPosition(currentPosition);
40
- const dx = currentPosition.x - lastSignificantPosition.current.x;
41
- const dy = currentPosition.y - lastSignificantPosition.current.y;
42
- const distance = Math.sqrt(dx * dx + dy * dy);
43
- if (distance > threshold) {
44
- const newAngle = Math.atan2(dy, dx) * (180 / Math.PI) + 45;
45
- setAngle(newAngle);
46
- lastSignificantPosition.current = currentPosition;
47
- }
48
- setIsMoving(true);
49
- lastMoveTimestamp.current = Date.now();
50
- if (timeoutRef.current) {
51
- clearTimeout(timeoutRef.current);
52
- }
53
- timeoutRef.current = setTimeout(() => {
54
- setIsMoving(false);
55
- }, flameHideTimeout);
56
- }, [threshold, flameHideTimeout]);
57
- const handleMouseOut = (e) => {
58
- if (!e.relatedTarget ||
59
- e.relatedTarget.nodeName === "HTML") {
60
- setVisible(false);
61
- }
62
- };
63
- const handleVisibilityChange = () => {
64
- if (document.visibilityState === "visible") {
65
- setVisible(true);
66
- }
67
- };
68
- useEffect(() => {
69
- const initialX = window.innerWidth / 2;
70
- const initialY = window.innerHeight / 2;
71
- setPosition({ x: initialX, y: initialY });
72
- lastSignificantPosition.current = { x: initialX, y: initialY };
73
- window.addEventListener("mousemove", handleMouseMove);
74
- document.addEventListener("mouseout", handleMouseOut);
75
- document.addEventListener("visibilitychange", handleVisibilityChange);
76
- return () => {
77
- window.removeEventListener("mousemove", handleMouseMove);
78
- document.removeEventListener("mouseout", handleMouseOut);
79
- document.removeEventListener("visibilitychange", handleVisibilityChange);
80
- if (timeoutRef.current) {
81
- clearTimeout(timeoutRef.current);
82
- }
83
- };
84
- }, [handleMouseMove]);
85
- const wrapperStyle = {
86
- position: "fixed",
87
- left: position.x,
88
- top: position.y,
89
- transform: `translate(-50%, -50%) rotate(${angle}deg)`,
90
- pointerEvents: "none",
91
- zIndex: 9999,
92
- width: `${size}px`,
93
- height: `${size * 1.5}px`,
94
- transition: 'left 0.05s linear, top 0.05s linear',
95
- };
96
- const svgStyle = {
97
- width: "100%",
98
- height: "100%",
99
- display: 'block',
100
- };
101
- if (!isVisible || !visible) {
102
- return null;
103
- }
104
- return (React.createElement("div", { style: wrapperStyle },
105
- React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 416.449 516.449", style: svgStyle },
106
- isMoving && React.createElement(FlameSvg, null),
107
- React.createElement(RocketSvg, null))));
108
- };
109
- export default RocketCursor;
1
+ import React, { useEffect, useRef, useId, useMemo, useCallback, startTransition } from "react";
2
+ // SVG components for flame and rocket visuals
3
+ const FlameSvg = ({ gradientId }) => (React.createElement("g", { transform: "translate(1.2245, 350.449) rotate(45) scale(0.5, -0.5)" },
4
+ React.createElement("defs", null,
5
+ React.createElement("linearGradient", { id: gradientId, gradientUnits: "userSpaceOnUse", x1: "94.141", y1: "255", x2: "94.141", y2: "0.188" },
6
+ React.createElement("stop", { offset: "0", stopColor: "#ff4c0d" }),
7
+ React.createElement("stop", { offset: "1", stopColor: "#fc9502" }))),
8
+ React.createElement("path", { d: "M187.899,164.809 C185.803,214.868 144.574,254.812 94.000,254.812 C42.085,254.812 -0.000,211.312 -0.000,160.812 C-0.000,154.062 -0.121,140.572 10.000,117.812 C16.057,104.191 19.856,95.634 22.000,87.812 C23.178,83.513 25.469,76.683 32.000,87.812 C35.851,94.374 36.000,103.812 36.000,103.812 C36.000,103.812 50.328,92.817 60.000,71.812 C74.179,41.019 62.866,22.612 59.000,9.812 C57.662,5.384 56.822,-2.574 66.000,0.812 C75.352,4.263 100.076,21.570 113.000,39.812 C131.445,65.847 138.000,90.812 138.000,90.812 C138.000,90.812 143.906,83.482 146.000,75.812 C148.365,67.151 148.400,58.573 155.999,67.813 C163.226,76.600 173.959,93.113 180.000,108.812 C190.969,137.321 187.899,164.809 187.899,164.809 Z", fill: `url(#${gradientId})`, fillRule: "evenodd" }),
9
+ React.createElement("path", { d: "M94.000,254.812 C58.101,254.812 29.000,225.711 29.000,189.812 C29.000,168.151 37.729,155.000 55.896,137.166 C67.528,125.747 78.415,111.722 83.042,102.172 C83.953,100.292 86.026,90.495 94.019,101.966 C98.212,107.982 104.785,118.681 109.000,127.812 C116.266,143.555 118.000,158.812 118.000,158.812 C118.000,158.812 125.121,154.616 130.000,143.812 C131.573,140.330 134.753,127.148 143.643,140.328 C150.166,150.000 159.127,167.390 159.000,189.812 C159.000,225.711 129.898,254.812 94.000,254.812 Z", fill: "#fc9502", fillRule: "evenodd" }),
10
+ React.createElement("path", { d: "M95.000,183.812 C104.250,183.812 104.250,200.941 116.000,223.812 C123.824,239.041 112.121,254.812 95.000,254.812 C77.879,254.812 69.000,240.933 69.000,223.812 C69.000,206.692 85.750,183.812 95.000,183.812 Z", fill: "#fce202", fillRule: "evenodd" })));
11
+ const RocketSvg = () => (React.createElement("g", { transform: "translate(0, 0)" },
12
+ React.createElement("path", { style: { fill: "#FF7124" }, d: "M399.76,16.699c10.12,37.84,8.67,78.13-4.34,115.28h-0.01L284.48,21.049v-0.01 C321.63,8.029,361.92,6.579,399.76,16.699z" }),
13
+ React.createElement("path", { style: { fill: "#F2D59F" }, d: "M90.21,207.929l87.14-101.42h0.01l33.71-39.24c21.43-21.43,46.6-36.84,73.41-46.23v0.01 l110.93,110.93h0.01c-9.39,26.81-24.8,51.98-46.23,73.41l-39.24,33.71l-101.43,87.14l-29.57-29.57l-29.58-29.58l-29.58-29.58 L90.21,207.929z M296.11,193.399c20.18-20.17,20.18-52.89,0-73.06c-20.17-20.18-52.89-20.18-73.06,0 c-20.18,20.17-20.18,52.89,0,73.06C243.22,213.579,275.94,213.579,296.11,193.399z" }),
14
+ React.createElement("path", { style: { fill: "#F2D59F" }, d: "M309.95,239.099c1.74,45.6-14.8,91.78-49.61,126.59c-10.69,10.68-22.44,19.65-34.93,26.89 l-16.89-66.34L309.95,239.099z" }),
15
+ React.createElement("path", { style: { fill: "#8ECAC1" }, d: "M296.11,120.339c20.18,20.17,20.18,52.89,0,73.06c-20.17,20.18-52.89,20.18-73.06,0 c-20.18-20.17-20.18-52.89,0-73.06C243.22,100.159,275.94,100.159,296.11,120.339z" }),
16
+ React.createElement("path", { style: { fill: "#E6B263" }, d: "M208.52,326.239l-39.94,14.71c-10.98,4.05-23.31,1.34-31.58-6.94l-6.85-6.85l48.8-30.49 L208.52,326.239z" }),
17
+ React.createElement("polygon", { style: { fill: "#E6B263" }, points: "178.95,296.669 130.15,327.159 130.14,327.159 109.72,306.739 149.37,267.089" }),
18
+ React.createElement("path", { style: { fill: "#F2D59F" }, d: "M177.35,106.509l-87.14,101.42l-66.33-16.88c7.24-12.49,16.21-24.24,26.89-34.93 C85.58,121.309,131.74,104.769,177.35,106.509z" }),
19
+ React.createElement("polygon", { style: { fill: "#E6B263" }, points: "149.37,267.089 109.72,306.739 89.3,286.309 119.79,237.509" }),
20
+ React.createElement("path", { style: { fill: "#E6B263" }, d: "M119.79,237.509l-30.49,48.8l-6.86-6.85c-8.27-8.28-10.98-20.6-6.94-31.58l14.71-39.95 L119.79,237.509z" })));
21
+ const RocketCursor = ({ size = 50, threshold = 10, flameHideTimeout = 300, isVisible = true, hideCursor = false, offsetX = -15, // Compensation for SVG geometry
22
+ offsetY = 0, }) => {
23
+ const gradientId = useId();
24
+ const wrapperRef = useRef(null);
25
+ const flameRef = useRef(null);
26
+ const target = useRef({
27
+ x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0,
28
+ y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0
29
+ });
30
+ const current = useRef({ x: target.current.x, y: target.current.y });
31
+ const angleRef = useRef(0);
32
+ const lastMoveTs = useRef(Date.now());
33
+ const lastSignificantPosition = useRef({ x: target.current.x, y: target.current.y });
34
+ const rafRef = useRef(null);
35
+ const visibleRef = useRef(isVisible);
36
+ useEffect(() => {
37
+ visibleRef.current = isVisible;
38
+ if (wrapperRef.current)
39
+ wrapperRef.current.style.display = isVisible ? "block" : "none";
40
+ // Apply cursor hiding to body if requested
41
+ if (hideCursor) {
42
+ document.body.style.cursor = 'none';
43
+ }
44
+ else {
45
+ document.body.style.cursor = '';
46
+ }
47
+ return () => {
48
+ // Cleanup: restore cursor when component unmounts
49
+ document.body.style.cursor = '';
50
+ };
51
+ }, [isVisible, hideCursor]);
52
+ useEffect(() => {
53
+ const onMouseMove = useCallback((e) => {
54
+ // Toggle visibility if hovering excluded elements
55
+ const t = e.target;
56
+ const exclude = t && t.closest && t.closest(".no-rocket-cursor");
57
+ const show = !exclude && visibleRef.current;
58
+ if (wrapperRef.current)
59
+ wrapperRef.current.style.display = show ? "block" : "none";
60
+ if (!show)
61
+ return;
62
+ // Set rocket position on cursor with optional offset
63
+ target.current.x = e.clientX + offsetX;
64
+ target.current.y = e.clientY + offsetY;
65
+ lastMoveTs.current = Date.now();
66
+ // Update angle based on movement direction from previous position
67
+ const moveDx = target.current.x - lastSignificantPosition.current.x;
68
+ const moveDy = target.current.y - lastSignificantPosition.current.y;
69
+ const moveDist = Math.hypot(moveDx, moveDy);
70
+ if (moveDist > threshold) {
71
+ startTransition(() => {
72
+ angleRef.current = Math.atan2(moveDy, moveDx) * (180 / Math.PI) + 45;
73
+ lastSignificantPosition.current = { x: target.current.x, y: target.current.y };
74
+ });
75
+ }
76
+ }, [offsetX, offsetY, threshold]);
77
+ const onMouseOut = useCallback((e) => {
78
+ const rel = e.relatedTarget;
79
+ if (!rel || rel.nodeName === "HTML") {
80
+ if (wrapperRef.current)
81
+ wrapperRef.current.style.display = "none";
82
+ }
83
+ }, []);
84
+ window.addEventListener("mousemove", onMouseMove, { passive: true });
85
+ document.addEventListener("mouseout", onMouseOut);
86
+ const step = () => {
87
+ // Move rocket directly to cursor position (no easing)
88
+ current.current.x = target.current.x;
89
+ current.current.y = target.current.y;
90
+ // Show flame based on recent mouse movement
91
+ const showFlame = Date.now() - lastMoveTs.current < flameHideTimeout;
92
+ const el = wrapperRef.current;
93
+ if (el) {
94
+ el.style.transform = `translate3d(${current.current.x}px, ${current.current.y}px, 0)`;
95
+ // Rotation is now in SVG element
96
+ const svg = el.querySelector('svg');
97
+ if (svg) {
98
+ svg.style.transform = `translate(-50%, -50%) rotate(${angleRef.current}deg)`;
99
+ }
100
+ }
101
+ if (flameRef.current) {
102
+ flameRef.current.style.opacity = showFlame ? "1" : "0";
103
+ }
104
+ rafRef.current = requestAnimationFrame(step);
105
+ };
106
+ rafRef.current = requestAnimationFrame(step);
107
+ return () => {
108
+ window.removeEventListener("mousemove", onMouseMove);
109
+ document.removeEventListener("mouseout", onMouseOut);
110
+ if (rafRef.current)
111
+ cancelAnimationFrame(rafRef.current);
112
+ };
113
+ }, [flameHideTimeout, threshold, offsetX, offsetY]);
114
+ const wrapperStyle = useMemo(() => ({
115
+ position: "fixed",
116
+ left: 0,
117
+ top: 0,
118
+ pointerEvents: "none",
119
+ zIndex: 9999,
120
+ width: `${size}px`,
121
+ height: `${size * 1.5}px`,
122
+ willChange: "transform",
123
+ }), [size]);
124
+ const svgStyle = useMemo(() => ({
125
+ width: "100%",
126
+ height: "100%",
127
+ display: "block",
128
+ }), []);
129
+ return (React.createElement("div", { ref: wrapperRef, style: wrapperStyle },
130
+ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 416.449 516.449", style: svgStyle },
131
+ React.createElement("g", { ref: flameRef },
132
+ React.createElement(FlameSvg, { gradientId: gradientId })),
133
+ React.createElement(RocketSvg, null))));
134
+ };
135
+ export default RocketCursor;
package/package.json CHANGED
@@ -1,42 +1,45 @@
1
- {
2
- "name": "rocket-cursor-component",
3
- "version": "1.1.1",
4
- "description": "A customizable React component that replaces the cursor with an animated rocket, featuring rotation and flame effects.",
5
- "main": "dist/rocket.Cursor.js",
6
- "types": "dist/rocket.Cursor.d.ts",
7
- "scripts": {
8
- "build": "tsc",
9
- "prepublishOnly": "npm run build"
10
- },
11
- "files": [
12
- "dist",
13
- "LICENSE",
14
- "README.md"
15
- ],
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/No898/RocketCursor.git"
19
- },
20
- "keywords": [
21
- "react",
22
- "cursor",
23
- "rocket",
24
- "component",
25
- "typescript",
26
- "animation"
27
- ],
28
- "author": "Tomas Dinh",
29
- "license": "MIT",
30
- "peerDependencies": {
31
- "react": ">=16.8.0",
32
- "react-dom": ">=16.8.0"
33
- },
34
- "devDependencies": {
35
- "@types/node": "^22.15.3",
36
- "@types/react": "^18.3.20",
37
- "@types/react-dom": "^18.3.0",
38
- "react": ">=16.8.0",
39
- "react-dom": ">=16.8.0",
40
- "typescript": "^4.0.0"
41
- }
42
- }
1
+ {
2
+ "name": "rocket-cursor-component",
3
+ "version": "2.0.0",
4
+ "description": "A customizable React component that replaces the cursor with an animated rocket, featuring rotation and flame effects.",
5
+ "main": "dist/rocket.Cursor.js",
6
+ "types": "dist/rocket.Cursor.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/No898/RocketCursor.git"
19
+ },
20
+ "keywords": [
21
+ "react",
22
+ "cursor",
23
+ "rocket",
24
+ "component",
25
+ "typescript",
26
+ "animation"
27
+ ],
28
+ "author": "Tomas Dinh",
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "react": ">=19.0.0",
32
+ "react-dom": ">=19.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.15.3",
36
+ "@types/react": "^19.0.0",
37
+ "@types/react-dom": "^19.0.0",
38
+ "react": "^19.0.0",
39
+ "react-dom": "^19.0.0",
40
+ "typescript": "^5.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=16.0.0"
44
+ }
45
+ }