{ "name": "magnetic-dock", "type": "registry:ui", "dependencies": [ "framer-motion" ], "registryDependencies": [], "description": "A macOS-style magnetic dock with smooth scaling animations, spring physics, and premium micro-interactions. Supports both light and dark modes.", "files": [ { "path": "components/ui/magnetic-dock.tsx", "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useMotionValue, useSpring, useTransform, AnimatePresence, type MotionValue } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\ninterface MagneticDockProps {\n /** Array of dock items */\n items: DockItemData[]\n /** Size of icons in pixels */\n iconSize?: number\n /** Maximum scale on hover */\n maxScale?: number\n /** Distance of magnetic effect in pixels */\n magneticDistance?: number\n /** Show labels on hover */\n showLabels?: boolean\n /** Dock position */\n position?: \"bottom\" | \"top\" | \"left\" | \"right\"\n /** Background style */\n variant?: \"glass\" | \"solid\" | \"transparent\"\n /** Custom class name */\n className?: string\n}\n\ninterface DockItemData {\n /** Unique identifier */\n id: string\n /** Display label */\n label: string\n /** Icon component or image URL */\n icon: React.ReactNode\n /** Click handler */\n onClick?: () => void\n /** Whether item is active */\n isActive?: boolean\n /** Badge count */\n badge?: number\n}\n\ninterface DockItemProps {\n item: DockItemData\n mouseX: MotionValue\n iconSize: number\n maxScale: number\n magneticDistance: number\n showLabels: boolean\n isVertical: boolean\n}\n\nfunction DockItem({\n item,\n mouseX,\n iconSize,\n maxScale,\n magneticDistance,\n showLabels,\n isVertical,\n}: DockItemProps) {\n const ref = React.useRef(null)\n const [isHovered, setIsHovered] = React.useState(false)\n\n // Calculate distance from mouse to center of item\n const distance = useTransform(mouseX, (val: number) => {\n if (!ref.current) return magneticDistance + 1\n const rect = ref.current.getBoundingClientRect()\n const center = isVertical\n ? rect.top + rect.height / 2\n : rect.left + rect.width / 2\n return val - center\n })\n\n // Scale based on distance - closer = larger\n const scale = useTransform(distance, [-magneticDistance, 0, magneticDistance], [1, maxScale, 1])\n\n // Apply spring physics for smooth animation\n const springConfig = { damping: 20, stiffness: 300, mass: 0.5 }\n const smoothScale = useSpring(scale, springConfig)\n\n // Calculate the size based on scale\n const size = useTransform(smoothScale, (s) => s * iconSize)\n\n // Floating effect\n const y = useTransform(smoothScale, (s) => (s - 1) * -10)\n const smoothY = useSpring(y, springConfig)\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\n \"relative flex items-center justify-center\",\n \"rounded-2xl transition-colors duration-200\",\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 dark:focus-visible:ring-white/50\",\n item.isActive && \"bg-neutral-200/50 dark:bg-white/10\"\n )}\n style={{\n width: size,\n height: size,\n y: isVertical ? 0 : smoothY,\n x: isVertical ? smoothY : 0,\n }}\n whileTap={{ scale: 0.9 }}\n >\n {/* Icon Container */}\n \n {/* Icon */}\n
\n {item.icon}\n
\n\n {/* Shine effect */}\n \n \n\n {/* Badge */}\n \n {item.badge !== undefined && item.badge > 0 && (\n \n {item.badge > 99 ? \"99+\" : item.badge}\n \n )}\n \n\n {/* Active Indicator */}\n \n {item.isActive && (\n \n )}\n \n\n {/* Tooltip */}\n \n {showLabels && isHovered && (\n \n {item.label}\n {/* Tooltip arrow */}\n \n \n )}\n \n\n {/* Hover glow */}\n \n \n )\n}\n\nfunction MagneticDock({\n items,\n iconSize = 56,\n maxScale = 1.5,\n magneticDistance = 150,\n showLabels = true,\n position = \"bottom\",\n variant = \"glass\",\n className,\n}: MagneticDockProps) {\n const mousePosition = useMotionValue(Infinity)\n const isVertical = position === \"left\" || position === \"right\"\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (isVertical) {\n mousePosition.set(e.clientY)\n } else {\n mousePosition.set(e.clientX)\n }\n },\n [mousePosition, isVertical]\n )\n\n const handleMouseLeave = () => {\n mousePosition.set(Infinity)\n }\n\n const variantStyles = {\n glass: cn(\n \"bg-white/80 dark:bg-neutral-900/80\",\n \"backdrop-blur-xl backdrop-saturate-150\",\n \"border border-neutral-200 dark:border-neutral-700\"\n ),\n solid: cn(\n \"bg-neutral-100 dark:bg-neutral-900\",\n \"border border-neutral-300 dark:border-neutral-700\"\n ),\n transparent: \"bg-transparent border-0\",\n }\n\n const positionStyles = {\n bottom: \"flex-row\",\n top: \"flex-row\",\n left: \"flex-col\",\n right: \"flex-col\",\n }\n\n return (\n \n {items.map((item) => (\n \n ))}\n \n )\n}\n\n// Preset icons for common use cases\nfunction DockIconHome({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconSearch({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconFolder({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\nfunction DockIconMail({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconMusic({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\nfunction DockIconSettings({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconTrash({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nexport {\n MagneticDock,\n DockIconHome,\n DockIconSearch,\n DockIconFolder,\n DockIconMail,\n DockIconMusic,\n DockIconSettings,\n DockIconTrash,\n type MagneticDockProps,\n type DockItemData,\n}\n", "type": "registry:ui" } ] }