Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 863fe8b

Browse files
committedMar 19, 2025
feat(ui): add smooth cursor component
1 parent 34455ca commit 863fe8b

File tree

7 files changed

+451
-0
lines changed

7 files changed

+451
-0
lines changed
 

‎config/docs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ export const docsConfig: DocsConfig = {
231231
items: [],
232232
label: "New",
233233
},
234+
{
235+
title: "Smooth Cursor",
236+
href: `/docs/components/smooth-cursor`,
237+
items: [],
238+
label: "New",
239+
},
234240
],
235241
},
236242
{
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
title: Smooth Cursor
3+
date: 2024-03-19
4+
description: A customizable, physics-based smooth cursor animation component for React applications.
5+
author: magicui
6+
published: true
7+
---
8+
9+
<ComponentPreview name="smooth-cursor-demo" />
10+
11+
## Overview
12+
13+
The `SmoothCursor` component provides a physics-based cursor animation that brings a polished and interactive feel to your application. Features include spring animations, velocity tracking, and rotation effects to create engaging cursor experiences.
14+
15+
## Features
16+
17+
- 🎯 Smooth physics-based cursor animations
18+
- 🔄 Rotation effects based on movement direction
19+
- ⚡ Performance optimized with RAF
20+
- 🎨 Fully customizable cursor design
21+
- 📦 Lightweight and easy to implement
22+
23+
## Installation
24+
25+
<Tabs defaultValue="cli">
26+
27+
<TabsList>
28+
<TabsTrigger value="cli">CLI</TabsTrigger>
29+
<TabsTrigger value="manual">Manual</TabsTrigger>
30+
</TabsList>
31+
<TabsContent value="cli">
32+
33+
```bash
34+
npx shadcn@latest add "https://magicui.design/r/smooth-cursor"
35+
```
36+
37+
</TabsContent>
38+
39+
<TabsContent value="manual">
40+
41+
<Steps>
42+
43+
<Step>Copy and paste the following code into your project.</Step>
44+
45+
<ComponentSource name="smooth-cursor" />
46+
47+
<Step>Add the component to your page or layout.</Step>
48+
49+
```tsx
50+
import { SmoothCursor } from "@/components/ui/smooth-cursor";
51+
52+
export default function Page() {
53+
return (
54+
<>
55+
<SmoothCursor />
56+
{/* Your page content */}
57+
</>
58+
);
59+
}
60+
```
61+
62+
</Steps>
63+
64+
</TabsContent>
65+
66+
</Tabs>
67+
68+
## Next.js Integration
69+
70+
### App Router (Next.js 13+)
71+
72+
```tsx
73+
"use client";
74+
75+
// app/layout.tsx
76+
import { SmoothCursor } from "@/components/ui/smooth-cursor";
77+
78+
export default function RootLayout({
79+
children,
80+
}: {
81+
children: React.ReactNode;
82+
}) {
83+
return (
84+
<html lang="en">
85+
<body>
86+
<SmoothCursor />
87+
{children}
88+
</body>
89+
</html>
90+
);
91+
}
92+
```
93+
94+
### Pages Router
95+
96+
```tsx
97+
// pages/_app.tsx
98+
import type { AppProps } from "next/app";
99+
import { SmoothCursor } from "@/components/ui/smooth-cursor";
100+
101+
export default function App({ Component, pageProps }: AppProps) {
102+
return (
103+
<>
104+
<SmoothCursor />
105+
<Component {...pageProps} />
106+
</>
107+
);
108+
}
109+
```
110+
111+
## Props
112+
113+
| Prop | Type | Default | Description |
114+
| -------------- | -------------- | ---------------------- | ------------------------------------------------------ |
115+
| `cursor` | `JSX.Element` | `<DefaultCursorSVG />` | Custom cursor component to replace the default cursor |
116+
| `springConfig` | `SpringConfig` | See below | Configuration object for the spring animation behavior |
117+
118+
### SpringConfig Type
119+
120+
```typescript
121+
interface SpringConfig {
122+
damping: number; // Controls how quickly the animation settles
123+
stiffness: number; // Controls the spring stiffness
124+
mass: number; // Controls the virtual mass of the animated object
125+
restDelta: number; // Controls the threshold at which animation is considered complete
126+
}
127+
```
128+
129+
### Default Spring Configuration
130+
131+
```typescript
132+
const defaultSpringConfig = {
133+
damping: 45,
134+
stiffness: 400,
135+
mass: 1,
136+
restDelta: 0.001,
137+
};
138+
```
139+
140+
## Performance Considerations
141+
142+
The component is optimized for performance by:
143+
144+
- Using `requestAnimationFrame` for smooth animations
145+
- Implementing throttling for mouse movement events
146+
- Using hardware-accelerated transforms
147+
- Optimizing re-renders with React's lifecycle methods
148+
149+
## Browser Support
150+
151+
Compatible with all modern browsers that support:
152+
153+
- `requestAnimationFrame`
154+
- CSS transforms
155+
- Pointer events
156+
157+
## Accessibility
158+
159+
When using this component, consider that:
160+
161+
- Users navigating via keyboard will not see the custom cursor
162+
- You may want to provide alternative visual cues for interactive elements
163+
- Some users may have motion sensitivity, so consider providing a way to disable the animation
164+
165+
## Credits
166+
167+
- Credit to [@Code_Parth](https://twitter.com/Code_Parth) for the original concept and implementation

‎registry.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,20 @@
158158
}
159159
]
160160
},
161+
{
162+
"name": "smooth-cursor",
163+
"type": "registry:ui",
164+
"description": "A customizable, physics-based smooth cursor animation component with spring animations and rotation effects",
165+
"dependencies": [
166+
"framer-motion"
167+
],
168+
"files": [
169+
{
170+
"path": "registry/magicui/smooth-cursor.tsx",
171+
"type": "registry:ui"
172+
}
173+
]
174+
},
161175
{
162176
"name": "neon-gradient-card",
163177
"type": "registry:ui",
@@ -1503,6 +1517,21 @@
15031517
}
15041518
]
15051519
},
1520+
{
1521+
"name": "smooth-cursor-demo",
1522+
"type": "registry:example",
1523+
"description": "Basic smooth cursor example",
1524+
"dependencies": [
1525+
"smooth-cursor"
1526+
],
1527+
"files": [
1528+
{
1529+
"path": "registry/example/smooth-cursor-demo.tsx",
1530+
"type": "registry:example",
1531+
"target": "components/smooth-cursor-demo.tsx"
1532+
}
1533+
]
1534+
},
15061535
{
15071536
"name": "neon-gradient-card-demo",
15081537
"type": "registry:example",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { SmoothCursor } from "@/registry/magicui/smooth-cursor";
5+
6+
export default function SmoothCursorDemo() {
7+
return (
8+
<div className="z-10 rounded-lg p-4">
9+
<h2 className="pb-4 font-bold">
10+
Note: The smooth cursor is shown on the page.
11+
</h2>
12+
<SmoothCursor />
13+
</div>
14+
);
15+
}

‎registry/magicui/smooth-cursor.tsx

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"use client";
2+
3+
import { motion, useSpring } from "motion/react";
4+
import React, { FC, useState, useEffect, useRef, JSX } from "react";
5+
6+
interface Position {
7+
x: number;
8+
y: number;
9+
}
10+
11+
export interface SmoothCursorProps {
12+
cursor?: JSX.Element;
13+
springConfig?: {
14+
damping: number;
15+
stiffness: number;
16+
mass: number;
17+
restDelta: number;
18+
};
19+
}
20+
21+
const DefaultCursorSVG: FC = () => {
22+
return (
23+
<svg
24+
xmlns="http://www.w3.org/2000/svg"
25+
width={50}
26+
height={54}
27+
viewBox="0 0 50 54"
28+
fill="none"
29+
style={{ scale: 0.5 }}
30+
>
31+
<g filter="url(#filter0_d_91_7928)">
32+
<path
33+
d="M42.6817 41.1495L27.5103 6.79925C26.7269 5.02557 24.2082 5.02558 23.3927 6.79925L7.59814 41.1495C6.75833 42.9759 8.52712 44.8902 10.4125 44.1954L24.3757 39.0496C24.8829 38.8627 25.4385 38.8627 25.9422 39.0496L39.8121 44.1954C41.6849 44.8902 43.4884 42.9759 42.6817 41.1495Z"
34+
fill="black"
35+
/>
36+
<path
37+
d="M43.7146 40.6933L28.5431 6.34306C27.3556 3.65428 23.5772 3.69516 22.3668 6.32755L6.57226 40.6778C5.3134 43.4156 7.97238 46.298 10.803 45.2549L24.7662 40.109C25.0221 40.0147 25.2999 40.0156 25.5494 40.1082L39.4193 45.254C42.2261 46.2953 44.9254 43.4347 43.7146 40.6933Z"
38+
stroke="white"
39+
strokeWidth={2.25825}
40+
/>
41+
</g>
42+
<defs>
43+
<filter
44+
id="filter0_d_91_7928"
45+
x={0.602397}
46+
y={0.952444}
47+
width={49.0584}
48+
height={52.428}
49+
filterUnits="userSpaceOnUse"
50+
colorInterpolationFilters="sRGB"
51+
>
52+
<feFlood floodOpacity={0} result="BackgroundImageFix" />
53+
<feColorMatrix
54+
in="SourceAlpha"
55+
type="matrix"
56+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
57+
result="hardAlpha"
58+
/>
59+
<feOffset dy={2.25825} />
60+
<feGaussianBlur stdDeviation={2.25825} />
61+
<feComposite in2="hardAlpha" operator="out" />
62+
<feColorMatrix
63+
type="matrix"
64+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"
65+
/>
66+
<feBlend
67+
mode="normal"
68+
in2="BackgroundImageFix"
69+
result="effect1_dropShadow_91_7928"
70+
/>
71+
<feBlend
72+
mode="normal"
73+
in="SourceGraphic"
74+
in2="effect1_dropShadow_91_7928"
75+
result="shape"
76+
/>
77+
</filter>
78+
</defs>
79+
</svg>
80+
);
81+
};
82+
83+
export function SmoothCursor({
84+
cursor = <DefaultCursorSVG />,
85+
springConfig = {
86+
damping: 45,
87+
stiffness: 400,
88+
mass: 1,
89+
restDelta: 0.001,
90+
},
91+
}: SmoothCursorProps) {
92+
const [isMoving, setIsMoving] = useState(false);
93+
const lastMousePos = useRef<Position>({ x: 0, y: 0 });
94+
const velocity = useRef<Position>({ x: 0, y: 0 });
95+
const lastUpdateTime = useRef(Date.now());
96+
const previousAngle = useRef(0);
97+
const accumulatedRotation = useRef(0);
98+
99+
const cursorX = useSpring(0, springConfig);
100+
const cursorY = useSpring(0, springConfig);
101+
const rotation = useSpring(0, {
102+
...springConfig,
103+
damping: 60,
104+
stiffness: 300,
105+
});
106+
const scale = useSpring(1, {
107+
...springConfig,
108+
stiffness: 500,
109+
damping: 35,
110+
});
111+
112+
useEffect(() => {
113+
const updateVelocity = (currentPos: Position) => {
114+
const currentTime = Date.now();
115+
const deltaTime = currentTime - lastUpdateTime.current;
116+
117+
if (deltaTime > 0) {
118+
velocity.current = {
119+
x: (currentPos.x - lastMousePos.current.x) / deltaTime,
120+
y: (currentPos.y - lastMousePos.current.y) / deltaTime,
121+
};
122+
}
123+
124+
lastUpdateTime.current = currentTime;
125+
lastMousePos.current = currentPos;
126+
};
127+
128+
const smoothMouseMove = (e: MouseEvent) => {
129+
const currentPos = { x: e.clientX, y: e.clientY };
130+
updateVelocity(currentPos);
131+
132+
const speed = Math.sqrt(
133+
Math.pow(velocity.current.x, 2) + Math.pow(velocity.current.y, 2),
134+
);
135+
136+
cursorX.set(currentPos.x);
137+
cursorY.set(currentPos.y);
138+
139+
if (speed > 0.1) {
140+
const currentAngle =
141+
Math.atan2(velocity.current.y, velocity.current.x) * (180 / Math.PI) +
142+
90;
143+
144+
let angleDiff = currentAngle - previousAngle.current;
145+
if (angleDiff > 180) angleDiff -= 360;
146+
if (angleDiff < -180) angleDiff += 360;
147+
accumulatedRotation.current += angleDiff;
148+
rotation.set(accumulatedRotation.current);
149+
previousAngle.current = currentAngle;
150+
151+
scale.set(0.95);
152+
setIsMoving(true);
153+
154+
const timeout = setTimeout(() => {
155+
scale.set(1);
156+
setIsMoving(false);
157+
}, 150);
158+
159+
return () => clearTimeout(timeout);
160+
}
161+
};
162+
163+
let rafId: number;
164+
const throttledMouseMove = (e: MouseEvent) => {
165+
if (rafId) return;
166+
167+
rafId = requestAnimationFrame(() => {
168+
smoothMouseMove(e);
169+
rafId = 0;
170+
});
171+
};
172+
173+
document.body.style.cursor = "none";
174+
window.addEventListener("mousemove", throttledMouseMove);
175+
176+
return () => {
177+
window.removeEventListener("mousemove", throttledMouseMove);
178+
document.body.style.cursor = "auto";
179+
if (rafId) cancelAnimationFrame(rafId);
180+
};
181+
}, [cursorX, cursorY, rotation, scale]);
182+
183+
return (
184+
<motion.div
185+
style={{
186+
position: "fixed",
187+
left: cursorX,
188+
top: cursorY,
189+
translateX: "-50%",
190+
translateY: "-50%",
191+
rotate: rotation,
192+
scale: scale,
193+
zIndex: 100,
194+
pointerEvents: "none",
195+
willChange: "transform",
196+
}}
197+
initial={{ scale: 0 }}
198+
animate={{ scale: 1 }}
199+
transition={{
200+
type: "spring",
201+
stiffness: 400,
202+
damping: 30,
203+
}}
204+
>
205+
{cursor}
206+
</motion.div>
207+
);
208+
}

‎registry/registry-examples.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,19 @@ export const examples: Registry["items"] = [
185185
},
186186
],
187187
},
188+
{
189+
name: "smooth-cursor-demo",
190+
description: "Basic smooth cursor example",
191+
type: "registry:example",
192+
files: [
193+
{
194+
path: "registry/example/smooth-cursor-demo.tsx",
195+
type: "registry:example",
196+
target: "components/smooth-cursor-demo.tsx",
197+
},
198+
],
199+
dependencies: ["smooth-cursor"],
200+
},
188201
{
189202
name: "neon-gradient-card-demo",
190203
type: "registry:example",

‎registry/registry-ui.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ export const ui: Registry["items"] = [
127127
},
128128
],
129129
},
130+
{
131+
name: "smooth-cursor",
132+
description:
133+
"A customizable, physics-based smooth cursor animation component with spring animations and rotation effects",
134+
type: "registry:ui",
135+
files: [
136+
{
137+
path: "registry/magicui/smooth-cursor.tsx",
138+
type: "registry:ui",
139+
},
140+
],
141+
dependencies: ["framer-motion"],
142+
},
130143
{
131144
name: "neon-gradient-card",
132145
type: "registry:ui",

0 commit comments

Comments
 (0)
Please sign in to comment.