{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "magnetic-dock", "title": "Magnetic Dock", "description": "A dock that responds to cursor movement with a magnetic effect.", "dependencies": [ "motion" ], "registryDependencies": [ "amitgajare2/ariseui/utils" ], "files": [ { "path": "components/ui/magnetic-dock.tsx", "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport {\r\n motion,\r\n useMotionValue,\r\n useReducedMotion,\r\n useSpring,\r\n useTransform,\r\n type MotionValue,\r\n} from \"motion/react\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ntype DockSpring = {\r\n mass?: number;\r\n stiffness?: number;\r\n damping?: number;\r\n restDelta?: number;\r\n restSpeed?: number;\r\n};\r\n\r\ntype MagneticDockContextValue = {\r\n cursorX: MotionValue;\r\n baseSize: number;\r\n magnification: number;\r\n distance: number;\r\n lift: number;\r\n spring: DockSpring;\r\n reducedMotion: boolean;\r\n pointerInsideRef: React.MutableRefObject;\r\n};\r\n\r\nconst MagneticDockContext = React.createContext(\r\n null,\r\n);\r\n\r\nfunction useMagneticDockContext() {\r\n const context = React.useContext(MagneticDockContext);\r\n\r\n if (!context) {\r\n throw new Error(\"MagneticDockItem must be used inside MagneticDock.\");\r\n }\r\n\r\n return context;\r\n}\r\n\r\nexport type MagneticDockProps = Omit<\r\n React.ComponentPropsWithoutRef<\"nav\">,\r\n \"children\"\r\n> & {\r\n children: React.ReactNode;\r\n baseSize?: number;\r\n magnification?: number;\r\n distance?: number;\r\n lift?: number;\r\n gap?: number;\r\n spring?: DockSpring;\r\n};\r\n\r\nconst MagneticDock = React.forwardRef(\r\n (\r\n {\r\n children,\r\n baseSize = 50,\r\n magnification = 78,\r\n distance = 150,\r\n lift = 18,\r\n gap = 10,\r\n spring = { mass: 0.12, stiffness: 220, damping: 18 },\r\n className,\r\n style,\r\n onPointerMove,\r\n onPointerLeave,\r\n \"aria-label\": ariaLabel = \"Application dock\",\r\n ...props\r\n },\r\n forwardedRef,\r\n ) => {\r\n const cursorX = useMotionValue(Number.POSITIVE_INFINITY);\r\n const prefersReducedMotion = useReducedMotion();\r\n const pointerInsideRef = React.useRef(false);\r\n\r\n const safeBaseSize = Math.max(24, baseSize);\r\n const safeMagnification = Math.max(safeBaseSize, magnification);\r\n const safeDistance = Math.max(1, distance);\r\n const safeGap = Math.max(0, gap);\r\n\r\n const context = React.useMemo(\r\n () => ({\r\n cursorX,\r\n baseSize: safeBaseSize,\r\n magnification: safeMagnification,\r\n distance: safeDistance,\r\n lift: Math.max(0, lift),\r\n spring,\r\n reducedMotion: Boolean(prefersReducedMotion),\r\n pointerInsideRef,\r\n }),\r\n [\r\n cursorX,\r\n lift,\r\n prefersReducedMotion,\r\n safeBaseSize,\r\n safeDistance,\r\n safeMagnification,\r\n spring,\r\n ],\r\n );\r\n\r\n return (\r\n \r\n {\r\n pointerInsideRef.current = true;\r\n\r\n if (event.pointerType !== \"touch\") {\r\n cursorX.set(event.clientX);\r\n }\r\n\r\n onPointerMove?.(event);\r\n }}\r\n onPointerLeave={(event) => {\r\n pointerInsideRef.current = false;\r\n cursorX.set(Number.POSITIVE_INFINITY);\r\n onPointerLeave?.(event);\r\n }}\r\n {...props}\r\n >\r\n {children}\r\n \r\n \r\n );\r\n },\r\n);\r\n\r\nMagneticDock.displayName = \"MagneticDock\";\r\n\r\nexport type MagneticDockItemProps = Omit<\r\n React.ComponentPropsWithoutRef<\"button\">,\r\n \"children\"\r\n> & {\r\n children: React.ReactNode;\r\n label: string;\r\n href?: string;\r\n active?: boolean;\r\n external?: boolean;\r\n tooltipSide?: \"top\" | \"bottom\";\r\n};\r\n\r\nconst MagneticDockItem = React.forwardRef<\r\n HTMLButtonElement | HTMLAnchorElement,\r\n MagneticDockItemProps\r\n>(\r\n (\r\n {\r\n children,\r\n label,\r\n href,\r\n active = false,\r\n external = false,\r\n tooltipSide = \"top\",\r\n className,\r\n disabled,\r\n onFocus,\r\n onBlur,\r\n onClick,\r\n type = \"button\",\r\n ...props\r\n },\r\n forwardedRef,\r\n ) => {\r\n const {\r\n cursorX,\r\n baseSize,\r\n magnification,\r\n distance,\r\n lift,\r\n spring,\r\n reducedMotion,\r\n pointerInsideRef,\r\n } = useMagneticDockContext();\r\n\r\n const itemRef = React.useRef(null);\r\n const tooltipId = React.useId();\r\n\r\n const targetSize = useTransform(() => {\r\n if (reducedMotion) return baseSize;\r\n\r\n const item = itemRef.current;\r\n const pointer = cursorX.get();\r\n\r\n if (!item || !Number.isFinite(pointer)) return baseSize;\r\n\r\n const bounds = item.getBoundingClientRect();\r\n const center = bounds.left + bounds.width / 2;\r\n const delta = Math.abs(pointer - center);\r\n\r\n if (delta >= distance) return baseSize;\r\n\r\n const proximity = 1 - delta / distance;\r\n const eased = (1 - Math.cos(proximity * Math.PI)) / 2;\r\n\r\n return baseSize + (magnification - baseSize) * eased;\r\n });\r\n\r\n const size = useSpring(targetSize, spring);\r\n const y = useTransform(() => {\r\n if (reducedMotion || magnification === baseSize) return 0;\r\n\r\n const progress = Math.min(\r\n 1,\r\n Math.max(0, (size.get() - baseSize) / (magnification - baseSize)),\r\n );\r\n\r\n return -lift * progress;\r\n });\r\n\r\n const setControlRef = React.useCallback(\r\n (node: HTMLButtonElement | HTMLAnchorElement | null) => {\r\n if (typeof forwardedRef === \"function\") {\r\n forwardedRef(node);\r\n } else if (forwardedRef) {\r\n forwardedRef.current = node;\r\n }\r\n },\r\n [forwardedRef],\r\n );\r\n\r\n const focusItem = React.useCallback(() => {\r\n const bounds = itemRef.current?.getBoundingClientRect();\r\n if (bounds) cursorX.set(bounds.left + bounds.width / 2);\r\n }, [cursorX]);\r\n\r\n const blurItem = React.useCallback(() => {\r\n if (!pointerInsideRef.current) {\r\n cursorX.set(Number.POSITIVE_INFINITY);\r\n }\r\n }, [cursorX, pointerInsideRef]);\r\n\r\n const sharedProps = {\r\n ref: setControlRef,\r\n \"aria-label\": label,\r\n \"aria-current\": active ? (\"page\" as const) : undefined,\r\n className: cn(\r\n \"group/control relative flex h-full w-full items-center justify-center overflow-hidden rounded-[28%] border border-white/10 bg-gradient-to-b from-white/20 to-white/5 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_8px_20px_-10px_rgba(0,0,0,0.9)] outline-none transition-[filter,background-color] hover:brightness-110 focus-visible:ring-2 focus-visible:ring-white/80 focus-visible:ring-offset-2 focus-visible:ring-offset-black/70 disabled:pointer-events-none disabled:opacity-45\",\r\n disabled && \"pointer-events-none opacity-45\",\r\n className,\r\n ),\r\n onFocus: (event: React.FocusEvent) => {\r\n focusItem();\r\n onFocus?.(event as React.FocusEvent);\r\n },\r\n onBlur: (event: React.FocusEvent) => {\r\n blurItem();\r\n onBlur?.(event as React.FocusEvent);\r\n },\r\n onClick: (event: React.MouseEvent) => {\r\n onClick?.(event as React.MouseEvent);\r\n },\r\n children: (\r\n <>\r\n \r\n {children}\r\n \r\n \r\n ),\r\n };\r\n\r\n return (\r\n \r\n \r\n {label}\r\n \r\n\r\n {href ? (\r\n )}\r\n {...sharedProps}\r\n href={disabled ? undefined : href}\r\n aria-disabled={disabled || undefined}\r\n tabIndex={disabled ? -1 : props.tabIndex}\r\n target={external ? \"_blank\" : undefined}\r\n rel={external ? \"noreferrer\" : undefined}\r\n />\r\n ) : (\r\n \r\n )}\r\n\r\n {active ? (\r\n \r\n ) : null}\r\n \r\n );\r\n },\r\n);\r\n\r\nMagneticDockItem.displayName = \"MagneticDockItem\";\r\n\r\nexport { MagneticDock, MagneticDockItem };\r\n", "type": "registry:ui" } ], "type": "registry:ui" }