{
  "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<number>\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<HTMLButtonElement>(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        <motion.button\n            ref={ref}\n            onClick={item.onClick}\n            onMouseEnter={() => 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            <motion.div\n                className={cn(\n                    \"relative w-full h-full rounded-2xl overflow-hidden\",\n                    \"bg-gradient-to-b from-neutral-100 to-neutral-50\",\n                    \"dark:from-neutral-800 dark:to-neutral-900\",\n                    \"backdrop-blur-sm\",\n                    \"border border-neutral-300 dark:border-neutral-700\",\n                    \"shadow-lg shadow-black/10 dark:shadow-black/30\",\n                    \"flex items-center justify-center\",\n                    \"transition-all duration-200\"\n                )}\n                style={{\n                    boxShadow: isHovered\n                        ? \"0 8px 32px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.5)\"\n                        : \"0 4px 12px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.3)\",\n                }}\n            >\n                {/* Icon */}\n                <div className=\"w-[60%] h-[60%] flex items-center justify-center text-neutral-700 dark:text-white\">\n                    {item.icon}\n                </div>\n\n                {/* Shine effect */}\n                <motion.div\n                    className=\"absolute inset-0 pointer-events-none\"\n                    style={{\n                        background:\n                            \"linear-gradient(135deg, rgba(255,255,255,0.6) 0%, transparent 50%, transparent 100%)\",\n                        opacity: isHovered ? 0.9 : 0.5,\n                    }}\n                />\n            </motion.div>\n\n            {/* Badge */}\n            <AnimatePresence>\n                {item.badge !== undefined && item.badge > 0 && (\n                    <motion.div\n                        initial={{ scale: 0, opacity: 0 }}\n                        animate={{ scale: 1, opacity: 1 }}\n                        exit={{ scale: 0, opacity: 0 }}\n                        className={cn(\n                            \"absolute -top-1 -right-1\",\n                            \"min-w-[20px] h-5 px-1.5\",\n                            \"rounded-full\",\n                            \"bg-red-500\",\n                            \"text-white text-xs font-semibold\",\n                            \"flex items-center justify-center\",\n                            \"border-2 border-white dark:border-neutral-950\",\n                            \"shadow-lg\"\n                        )}\n                    >\n                        {item.badge > 99 ? \"99+\" : item.badge}\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            {/* Active Indicator */}\n            <AnimatePresence>\n                {item.isActive && (\n                    <motion.div\n                        initial={{ scale: 0, opacity: 0 }}\n                        animate={{ scale: 1, opacity: 1 }}\n                        exit={{ scale: 0, opacity: 0 }}\n                        className={cn(\n                            \"absolute -bottom-2\",\n                            \"w-1.5 h-1.5 rounded-full\",\n                            \"bg-neutral-600 dark:bg-white/80\"\n                        )}\n                    />\n                )}\n            </AnimatePresence>\n\n            {/* Tooltip */}\n            <AnimatePresence>\n                {showLabels && isHovered && (\n                    <motion.div\n                        initial={{ opacity: 0, y: 8, scale: 0.9 }}\n                        animate={{ opacity: 1, y: 0, scale: 1 }}\n                        exit={{ opacity: 0, y: 8, scale: 0.9 }}\n                        transition={{ duration: 0.15, ease: \"easeOut\" }}\n                        className={cn(\n                            \"absolute -top-10 left-1/2 -translate-x-1/2\",\n                            \"px-3 py-1.5 rounded-lg\",\n                            \"bg-white dark:bg-neutral-900/95\",\n                            \"backdrop-blur-sm\",\n                            \"text-neutral-800 dark:text-white text-sm font-medium whitespace-nowrap\",\n                            \"border border-neutral-200 dark:border-white/10\",\n                            \"shadow-xl shadow-black/10 dark:shadow-black/20\",\n                            \"pointer-events-none z-50\"\n                        )}\n                    >\n                        {item.label}\n                        {/* Tooltip arrow */}\n                        <div\n                            className={cn(\n                                \"absolute left-1/2 -translate-x-1/2 -bottom-1\",\n                                \"w-2 h-2 rotate-45\",\n                                \"bg-white dark:bg-neutral-900/95\",\n                                \"border-r border-b border-neutral-200 dark:border-white/10\"\n                            )}\n                        />\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            {/* Hover glow */}\n            <motion.div\n                className=\"absolute inset-0 rounded-2xl pointer-events-none\"\n                animate={{\n                    boxShadow: isHovered\n                        ? \"0 0 30px rgba(255,255,255,0.15)\"\n                        : \"0 0 0px rgba(255,255,255,0)\",\n                }}\n                transition={{ duration: 0.3 }}\n            />\n        </motion.button>\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        <motion.div\n            onMouseMove={handleMouseMove}\n            onMouseLeave={handleMouseLeave}\n            className={cn(\n                \"inline-flex items-end gap-2 p-3 rounded-3xl\",\n                variantStyles[variant],\n                positionStyles[position],\n                \"shadow-xl shadow-black/10 dark:shadow-black/30\",\n                className\n            )}\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 0.5, ease: [0.25, 0.46, 0.45, 0.94] }}\n        >\n            {items.map((item) => (\n                <DockItem\n                    key={item.id}\n                    item={item}\n                    mouseX={mousePosition}\n                    iconSize={iconSize}\n                    maxScale={maxScale}\n                    magneticDistance={magneticDistance}\n                    showLabels={showLabels}\n                    isVertical={isVertical}\n                />\n            ))}\n        </motion.div>\n    )\n}\n\n// Preset icons for common use cases\nfunction DockIconHome({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />\n            <polyline points=\"9 22 9 12 15 12 15 22\" />\n        </svg>\n    )\n}\n\nfunction DockIconSearch({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <circle cx=\"11\" cy=\"11\" r=\"8\" />\n            <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n        </svg>\n    )\n}\n\nfunction DockIconFolder({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\n        </svg>\n    )\n}\n\nfunction DockIconMail({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <path d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\" />\n            <polyline points=\"22,6 12,13 2,6\" />\n        </svg>\n    )\n}\n\nfunction DockIconMusic({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <path d=\"M9 18V5l12-2v13\" />\n            <circle cx=\"6\" cy=\"18\" r=\"3\" />\n            <circle cx=\"18\" cy=\"16\" r=\"3\" />\n        </svg>\n    )\n}\n\nfunction DockIconSettings({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <circle cx=\"12\" cy=\"12\" r=\"3\" />\n            <path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z\" />\n        </svg>\n    )\n}\n\nfunction DockIconTrash({ className }: { className?: string }) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\"w-full h-full\", className)}\n        >\n            <polyline points=\"3 6 5 6 21 6\" />\n            <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n        </svg>\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"
    }
  ]
}