Fortura/apps/www/public/r/cursor.json
2025-08-20 04:12:49 -06:00

18 lines
7.4 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "cursor",
"type": "registry:ui",
"title": "Cursor",
"description": "An animated cursor component that allows you to customize both the cursor and cursor follow elements with smooth animations.",
"dependencies": [
"motion"
],
"files": [
{
"path": "registry/components/cursor/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport {\n motion,\n useMotionValue,\n useSpring,\n AnimatePresence,\n type HTMLMotionProps,\n type SpringOptions,\n} from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\ntype CursorContextType = {\n cursorPos: { x: number; y: number };\n isActive: boolean;\n containerRef: React.RefObject<HTMLDivElement | null>;\n cursorRef: React.RefObject<HTMLDivElement | null>;\n};\n\nconst CursorContext = React.createContext<CursorContextType | undefined>(\n undefined,\n);\n\nconst useCursor = (): CursorContextType => {\n const context = React.useContext(CursorContext);\n if (!context) {\n throw new Error('useCursor must be used within a CursorProvider');\n }\n return context;\n};\n\ntype CursorProviderProps = React.ComponentProps<'div'> & {\n children: React.ReactNode;\n};\n\nfunction CursorProvider({ ref, children, ...props }: CursorProviderProps) {\n const [cursorPos, setCursorPos] = React.useState({ x: 0, y: 0 });\n const [isActive, setIsActive] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n const cursorRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n React.useEffect(() => {\n if (!containerRef.current) return;\n\n const parent = containerRef.current.parentElement;\n if (!parent) return;\n\n if (getComputedStyle(parent).position === 'static') {\n parent.style.position = 'relative';\n }\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = parent.getBoundingClientRect();\n setCursorPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });\n setIsActive(true);\n };\n const handleMouseLeave = () => setIsActive(false);\n\n parent.addEventListener('mousemove', handleMouseMove);\n parent.addEventListener('mouseleave', handleMouseLeave);\n\n return () => {\n parent.removeEventListener('mousemove', handleMouseMove);\n parent.removeEventListener('mouseleave', handleMouseLeave);\n };\n }, []);\n\n return (\n <CursorContext.Provider\n value={{ cursorPos, isActive, containerRef, cursorRef }}\n >\n <div ref={containerRef} data-slot=\"cursor-provider\" {...props}>\n {children}\n </div>\n </CursorContext.Provider>\n );\n}\n\ntype CursorProps = HTMLMotionProps<'div'> & {\n children: React.ReactNode;\n};\n\nfunction Cursor({ ref, children, className, style, ...props }: CursorProps) {\n const { cursorPos, isActive, containerRef, cursorRef } = useCursor();\n React.useImperativeHandle(ref, () => cursorRef.current as HTMLDivElement);\n\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n\n React.useEffect(() => {\n const parentElement = containerRef.current?.parentElement;\n\n if (parentElement && isActive) parentElement.style.cursor = 'none';\n\n return () => {\n if (parentElement) parentElement.style.cursor = 'default';\n };\n }, [containerRef, cursorPos, isActive]);\n\n React.useEffect(() => {\n x.set(cursorPos.x);\n y.set(cursorPos.y);\n }, [cursorPos, x, y]);\n\n return (\n <AnimatePresence>\n {isActive && (\n <motion.div\n ref={cursorRef}\n data-slot=\"cursor\"\n className={cn(\n 'transform-[translate(-50%,-50%)] pointer-events-none z-[9999] absolute',\n className,\n )}\n style={{ top: y, left: x, ...style }}\n initial={{ scale: 0, opacity: 0 }}\n animate={{ scale: 1, opacity: 1 }}\n exit={{ scale: 0, opacity: 0 }}\n {...props}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>\n );\n}\n\ntype Align =\n | 'top'\n | 'top-left'\n | 'top-right'\n | 'bottom'\n | 'bottom-left'\n | 'bottom-right'\n | 'left'\n | 'right'\n | 'center';\n\ntype CursorFollowProps = HTMLMotionProps<'div'> & {\n sideOffset?: number;\n align?: Align;\n transition?: SpringOptions;\n children: React.ReactNode;\n};\n\nfunction CursorFollow({\n ref,\n sideOffset = 15,\n align = 'bottom-right',\n children,\n className,\n style,\n transition = { stiffness: 500, damping: 50, bounce: 0 },\n ...props\n}: CursorFollowProps) {\n const { cursorPos, isActive, cursorRef } = useCursor();\n const cursorFollowRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(\n ref,\n () => cursorFollowRef.current as HTMLDivElement,\n );\n\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n\n const springX = useSpring(x, transition);\n const springY = useSpring(y, transition);\n\n const calculateOffset = React.useCallback(() => {\n const rect = cursorFollowRef.current?.getBoundingClientRect();\n const width = rect?.width ?? 0;\n const height = rect?.height ?? 0;\n\n let newOffset;\n\n switch (align) {\n case 'center':\n newOffset = { x: width / 2, y: height / 2 };\n break;\n case 'top':\n newOffset = { x: width / 2, y: height + sideOffset };\n break;\n case 'top-left':\n newOffset = { x: width + sideOffset, y: height + sideOffset };\n break;\n case 'top-right':\n newOffset = { x: -sideOffset, y: height + sideOffset };\n break;\n case 'bottom':\n newOffset = { x: width / 2, y: -sideOffset };\n break;\n case 'bottom-left':\n newOffset = { x: width + sideOffset, y: -sideOffset };\n break;\n case 'bottom-right':\n newOffset = { x: -sideOffset, y: -sideOffset };\n break;\n case 'left':\n newOffset = { x: width + sideOffset, y: height / 2 };\n break;\n case 'right':\n newOffset = { x: -sideOffset, y: height / 2 };\n break;\n default:\n newOffset = { x: 0, y: 0 };\n }\n\n return newOffset;\n }, [align, sideOffset]);\n\n React.useEffect(() => {\n const offset = calculateOffset();\n const cursorRect = cursorRef.current?.getBoundingClientRect();\n const cursorWidth = cursorRect?.width ?? 20;\n const cursorHeight = cursorRect?.height ?? 20;\n\n x.set(cursorPos.x - offset.x + cursorWidth / 2);\n y.set(cursorPos.y - offset.y + cursorHeight / 2);\n }, [calculateOffset, cursorPos, cursorRef, x, y]);\n\n return (\n <AnimatePresence>\n {isActive && (\n <motion.div\n ref={cursorFollowRef}\n data-slot=\"cursor-follow\"\n className={cn(\n 'transform-[translate(-50%,-50%)] pointer-events-none z-[9998] absolute',\n className,\n )}\n style={{ top: springY, left: springX, ...style }}\n initial={{ scale: 0, opacity: 0 }}\n animate={{ scale: 1, opacity: 1 }}\n exit={{ scale: 0, opacity: 0 }}\n {...props}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>\n );\n}\n\nexport {\n CursorProvider,\n Cursor,\n CursorFollow,\n useCursor,\n type CursorContextType,\n type CursorProviderProps,\n type CursorProps,\n type CursorFollowProps,\n};\n",
"type": "registry:ui",
"target": "components/animate-ui/components/cursor.tsx"
}
]
}