Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions apps/www/content/docs/components/smooth-cursor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ npx shadcn@latest add @magicui/smooth-cursor

<TabsContent value="manual">

```bash
npx add smooth-cursor framer-motion
```

<Steps>

<Step>Copy and paste the following code into your project.</Step>
Expand Down Expand Up @@ -102,10 +106,10 @@ select {

## Props

| Prop | Type | Default | Description |
| -------------- | ----------------- | ---------------------- | ------------------------------------------------------ |
| `cursor` | `React.ReactNode` | `<DefaultCursorSVG />` | Custom cursor component to replace the default cursor |
| `springConfig` | `SpringConfig` | See below | Configuration object for the spring animation behavior |
| Prop | Type | Default | Description |
| -------------- | -------------- | ---------------------- | ------------------------------------------------------ |
| `cursor` | `JSX.Element`. | `<DefaultCursorSVG />` | Custom cursor component to replace the default cursor |
| `springConfig` | `SpringConfig` | See below | Configuration object for the spring animation behavior |

### SpringConfig Type

Expand Down
1 change: 1 addition & 0 deletions apps/www/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const nextConfig = {
"images.unsplash.com",
"img.youtube.com",
"pbs.twimg.com",
"cdn.outrank.so",
],
},
async redirects() {
Expand Down
1 change: 1 addition & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"react-tweet": "^3.2.2",
"rough-notation": "^0.5.1",
"schema-dts": "^1.1.5",
"smooth-cursor": "^0.1.2",
"sonner": "^1.7.4",
"svg-dotted-map": "^2.0.1",
"tailwind-merge": "^3.3.1",
Expand Down
200 changes: 5 additions & 195 deletions apps/www/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13225,16 +13225,11 @@ Description: A customizable, physics-based smooth cursor animation component wit
--- file: magicui/smooth-cursor.tsx ---
"use client"

import { FC, useEffect, useRef, useState } from "react"
import { motion, useSpring } from "motion/react"

interface Position {
x: number
y: number
}
import { JSX } from "react"
import { SmoothCursor as SmoothCursorComponent } from "smooth-cursor"

export interface SmoothCursorProps {
cursor?: React.ReactNode
cursor?: JSX.Element
springConfig?: {
damping: number
stiffness: number
Expand All @@ -13243,193 +13238,8 @@ export interface SmoothCursorProps {
}
}

const DefaultCursorSVG: FC = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={50}
height={54}
viewBox="0 0 50 54"
fill="none"
style={{ scale: 0.5 }}
>
<g filter="url(#filter0_d_91_7928)">
<path
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"
fill="black"
/>
<path
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"
stroke="white"
strokeWidth={2.25825}
/>
</g>
<defs>
<filter
id="filter0_d_91_7928"
x={0.602397}
y={0.952444}
width={49.0584}
height={52.428}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy={2.25825} />
<feGaussianBlur stdDeviation={2.25825} />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_91_7928"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_91_7928"
result="shape"
/>
</filter>
</defs>
</svg>
)
}

export function SmoothCursor({
cursor = <DefaultCursorSVG />,
springConfig = {
damping: 45,
stiffness: 400,
mass: 1,
restDelta: 0.001,
},
}: SmoothCursorProps) {
const [isMoving, setIsMoving] = useState(false)
const lastMousePos = useRef<Position>({ x: 0, y: 0 })
const velocity = useRef<Position>({ x: 0, y: 0 })
const lastUpdateTime = useRef(Date.now())
const previousAngle = useRef(0)
const accumulatedRotation = useRef(0)

const cursorX = useSpring(0, springConfig)
const cursorY = useSpring(0, springConfig)
const rotation = useSpring(0, {
...springConfig,
damping: 60,
stiffness: 300,
})
const scale = useSpring(1, {
...springConfig,
stiffness: 500,
damping: 35,
})

useEffect(() => {
const updateVelocity = (currentPos: Position) => {
const currentTime = Date.now()
const deltaTime = currentTime - lastUpdateTime.current

if (deltaTime > 0) {
velocity.current = {
x: (currentPos.x - lastMousePos.current.x) / deltaTime,
y: (currentPos.y - lastMousePos.current.y) / deltaTime,
}
}

lastUpdateTime.current = currentTime
lastMousePos.current = currentPos
}

const smoothMouseMove = (e: MouseEvent) => {
const currentPos = { x: e.clientX, y: e.clientY }
updateVelocity(currentPos)

const speed = Math.sqrt(
Math.pow(velocity.current.x, 2) + Math.pow(velocity.current.y, 2)
)

cursorX.set(currentPos.x)
cursorY.set(currentPos.y)

if (speed > 0.1) {
const currentAngle =
Math.atan2(velocity.current.y, velocity.current.x) * (180 / Math.PI) +
90

let angleDiff = currentAngle - previousAngle.current
if (angleDiff > 180) angleDiff -= 360
if (angleDiff < -180) angleDiff += 360
accumulatedRotation.current += angleDiff
rotation.set(accumulatedRotation.current)
previousAngle.current = currentAngle

scale.set(0.95)
setIsMoving(true)

const timeout = setTimeout(() => {
scale.set(1)
setIsMoving(false)
}, 150)

return () => clearTimeout(timeout)
}
}

let rafId: number
const throttledMouseMove = (e: MouseEvent) => {
if (rafId) return

rafId = requestAnimationFrame(() => {
smoothMouseMove(e)
rafId = 0
})
}

document.body.style.cursor = "none"
window.addEventListener("mousemove", throttledMouseMove)

return () => {
window.removeEventListener("mousemove", throttledMouseMove)
document.body.style.cursor = "auto"
if (rafId) cancelAnimationFrame(rafId)
}
}, [cursorX, cursorY, rotation, scale])

return (
<motion.div
style={{
position: "fixed",
left: cursorX,
top: cursorY,
translateX: "-50%",
translateY: "-50%",
rotate: rotation,
scale: scale,
zIndex: 100,
pointerEvents: "none",
willChange: "transform",
}}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 400,
damping: 30,
}}
>
{cursor}
</motion.div>
)
export function SmoothCursor({ cursor, springConfig }: SmoothCursorProps) {
return <SmoothCursorComponent cursor={cursor} springConfig={springConfig} />
}


Expand Down
3 changes: 2 additions & 1 deletion apps/www/public/r/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@
"type": "registry:ui",
"description": "A customizable, physics-based smooth cursor animation component with spring animations and rotation effects",
"dependencies": [
"framer-motion"
"framer-motion",
"smooth-cursor"
],
"files": [
{
Expand Down
5 changes: 3 additions & 2 deletions apps/www/public/r/smooth-cursor.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
"type": "registry:ui",
"description": "A customizable, physics-based smooth cursor animation component with spring animations and rotation effects",
"dependencies": [
"framer-motion"
"framer-motion",
"smooth-cursor"
],
"files": [
{
"path": "registry/magicui/smooth-cursor.tsx",
"content": "\"use client\"\n\nimport { FC, useEffect, useRef, useState } from \"react\"\nimport { motion, useSpring } from \"motion/react\"\n\ninterface Position {\n x: number\n y: number\n}\n\nexport interface SmoothCursorProps {\n cursor?: React.ReactNode\n springConfig?: {\n damping: number\n stiffness: number\n mass: number\n restDelta: number\n }\n}\n\nconst DefaultCursorSVG: FC = () => {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={50}\n height={54}\n viewBox=\"0 0 50 54\"\n fill=\"none\"\n style={{ scale: 0.5 }}\n >\n <g filter=\"url(#filter0_d_91_7928)\">\n <path\n 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\"\n fill=\"black\"\n />\n <path\n 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\"\n stroke=\"white\"\n strokeWidth={2.25825}\n />\n </g>\n <defs>\n <filter\n id=\"filter0_d_91_7928\"\n x={0.602397}\n y={0.952444}\n width={49.0584}\n height={52.428}\n filterUnits=\"userSpaceOnUse\"\n colorInterpolationFilters=\"sRGB\"\n >\n <feFlood floodOpacity={0} result=\"BackgroundImageFix\" />\n <feColorMatrix\n in=\"SourceAlpha\"\n type=\"matrix\"\n values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\"\n result=\"hardAlpha\"\n />\n <feOffset dy={2.25825} />\n <feGaussianBlur stdDeviation={2.25825} />\n <feComposite in2=\"hardAlpha\" operator=\"out\" />\n <feColorMatrix\n type=\"matrix\"\n values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0\"\n />\n <feBlend\n mode=\"normal\"\n in2=\"BackgroundImageFix\"\n result=\"effect1_dropShadow_91_7928\"\n />\n <feBlend\n mode=\"normal\"\n in=\"SourceGraphic\"\n in2=\"effect1_dropShadow_91_7928\"\n result=\"shape\"\n />\n </filter>\n </defs>\n </svg>\n )\n}\n\nexport function SmoothCursor({\n cursor = <DefaultCursorSVG />,\n springConfig = {\n damping: 45,\n stiffness: 400,\n mass: 1,\n restDelta: 0.001,\n },\n}: SmoothCursorProps) {\n const [isMoving, setIsMoving] = useState(false)\n const lastMousePos = useRef<Position>({ x: 0, y: 0 })\n const velocity = useRef<Position>({ x: 0, y: 0 })\n const lastUpdateTime = useRef(Date.now())\n const previousAngle = useRef(0)\n const accumulatedRotation = useRef(0)\n\n const cursorX = useSpring(0, springConfig)\n const cursorY = useSpring(0, springConfig)\n const rotation = useSpring(0, {\n ...springConfig,\n damping: 60,\n stiffness: 300,\n })\n const scale = useSpring(1, {\n ...springConfig,\n stiffness: 500,\n damping: 35,\n })\n\n useEffect(() => {\n const updateVelocity = (currentPos: Position) => {\n const currentTime = Date.now()\n const deltaTime = currentTime - lastUpdateTime.current\n\n if (deltaTime > 0) {\n velocity.current = {\n x: (currentPos.x - lastMousePos.current.x) / deltaTime,\n y: (currentPos.y - lastMousePos.current.y) / deltaTime,\n }\n }\n\n lastUpdateTime.current = currentTime\n lastMousePos.current = currentPos\n }\n\n const smoothMouseMove = (e: MouseEvent) => {\n const currentPos = { x: e.clientX, y: e.clientY }\n updateVelocity(currentPos)\n\n const speed = Math.sqrt(\n Math.pow(velocity.current.x, 2) + Math.pow(velocity.current.y, 2)\n )\n\n cursorX.set(currentPos.x)\n cursorY.set(currentPos.y)\n\n if (speed > 0.1) {\n const currentAngle =\n Math.atan2(velocity.current.y, velocity.current.x) * (180 / Math.PI) +\n 90\n\n let angleDiff = currentAngle - previousAngle.current\n if (angleDiff > 180) angleDiff -= 360\n if (angleDiff < -180) angleDiff += 360\n accumulatedRotation.current += angleDiff\n rotation.set(accumulatedRotation.current)\n previousAngle.current = currentAngle\n\n scale.set(0.95)\n setIsMoving(true)\n\n const timeout = setTimeout(() => {\n scale.set(1)\n setIsMoving(false)\n }, 150)\n\n return () => clearTimeout(timeout)\n }\n }\n\n let rafId: number\n const throttledMouseMove = (e: MouseEvent) => {\n if (rafId) return\n\n rafId = requestAnimationFrame(() => {\n smoothMouseMove(e)\n rafId = 0\n })\n }\n\n document.body.style.cursor = \"none\"\n window.addEventListener(\"mousemove\", throttledMouseMove)\n\n return () => {\n window.removeEventListener(\"mousemove\", throttledMouseMove)\n document.body.style.cursor = \"auto\"\n if (rafId) cancelAnimationFrame(rafId)\n }\n }, [cursorX, cursorY, rotation, scale])\n\n return (\n <motion.div\n style={{\n position: \"fixed\",\n left: cursorX,\n top: cursorY,\n translateX: \"-50%\",\n translateY: \"-50%\",\n rotate: rotation,\n scale: scale,\n zIndex: 100,\n pointerEvents: \"none\",\n willChange: \"transform\",\n }}\n initial={{ scale: 0 }}\n animate={{ scale: 1 }}\n transition={{\n type: \"spring\",\n stiffness: 400,\n damping: 30,\n }}\n >\n {cursor}\n </motion.div>\n )\n}\n",
"content": "\"use client\"\n\nimport { JSX } from \"react\"\nimport { SmoothCursor as SmoothCursorComponent } from \"smooth-cursor\"\n\nexport interface SmoothCursorProps {\n cursor?: JSX.Element\n springConfig?: {\n damping: number\n stiffness: number\n mass: number\n restDelta: number\n }\n}\n\nexport function SmoothCursor({ cursor, springConfig }: SmoothCursorProps) {\n return <SmoothCursorComponent cursor={cursor} springConfig={springConfig} />\n}\n",
"type": "registry:ui"
}
]
Expand Down
3 changes: 2 additions & 1 deletion apps/www/public/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@
"type": "registry:ui",
"description": "A customizable, physics-based smooth cursor animation component with spring animations and rotation effects",
"dependencies": [
"framer-motion"
"framer-motion",
"smooth-cursor"
],
"files": [
{
Expand Down
3 changes: 2 additions & 1 deletion apps/www/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@
"type": "registry:ui",
"description": "A customizable, physics-based smooth cursor animation component with spring animations and rotation effects",
"dependencies": [
"framer-motion"
"framer-motion",
"smooth-cursor"
],
"files": [
{
Expand Down
Loading