Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
terminal
npx -y vui-registry-cli-v1@latest add magnetic-nav-dockSource Code
magnetic-nav-dock.tsx
"use client"
import React, { useRef, useState } from 'react'
import { motion, useMotionValue, useSpring, useTransform, AnimatePresence, MotionValue } from 'framer-motion'
import {
AppWindow,
MessageSquare,
Music,
Settings,
Github,
Terminal,
FolderGit2,
CalendarDays,
Sparkles
} from 'lucide-react'
import { cn } from '@/lib/utils'
export default function MagneticNavDock() {
let mouseX = useMotionValue(Infinity)
const [activeApp, setActiveApp] = useState("Terminal")
return (
<div className="relative flex min-h-[500px] w-full items-end justify-center overflow-hidden bg-neutral-950 font-sans pb-20">
{/* Wallpaper / Background */}
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1550684848-fac1c5b4e853?q=80&w=2070&auto=format&fit=crop')] bg-cover bg-center opacity-40 mix-blend-overlay" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-950 via-neutral-950/50 to-neutral-950/20" />
</div>
{/* Dock Container */}
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className="mx-auto flex h-16 items-end gap-3 rounded-2xl bg-white/5 px-4 pb-3 shadow-2xl backdrop-blur-2xl border border-white/10 ring-1 ring-white/5 relative z-10"
>
{[
{ icon: Terminal, label: "Terminal", color: "from-slate-800 to-slate-900" },
{ icon: MessageSquare, label: "Messages", color: "from-green-400 to-emerald-600" },
{ icon: Music, label: "Music", color: "from-pink-500 to-rose-600" },
{ icon: CalendarDays, label: "Calendar", color: "from-red-500 to-red-600" },
{ icon: FolderGit2, label: "Projects", color: "from-blue-400 to-blue-600" },
{ icon: Sparkles, label: "AI", color: "from-amber-300 to-orange-500" },
{ icon: AppWindow, label: "Browser", color: "from-cyan-400 to-blue-500" },
].map((item) => (
<DockIcon
key={item.label}
mouseX={mouseX}
{...item}
isActive={activeApp === item.label}
onClick={() => setActiveApp(item.label)}
/>
))}
{/* Divider */}
<div className="h-10 w-px bg-white/10 mx-1 self-center" />
{[
{ icon: Settings, label: "Settings", color: "from-neutral-600 to-neutral-700" },
{ icon: Github, label: "GitHub", color: "from-neutral-800 to-black" },
].map((item) => (
<DockIcon
key={item.label}
mouseX={mouseX}
{...item}
isActive={activeApp === item.label}
onClick={() => setActiveApp(item.label)}
/>
))}
</motion.div>
</div>
)
}
function DockIcon({
mouseX,
icon: Icon,
label,
color,
isActive,
onClick
}: {
mouseX: MotionValue,
icon: any,
label: string,
color: string,
isActive: boolean,
onClick: () => void
}) {
let ref = useRef<HTMLDivElement>(null)
let distance = useTransform(mouseX, (val) => {
let bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }
return val - bounds.x - bounds.width / 2
})
let widthSync = useTransform(distance, [-150, 0, 150], [40, 85, 40])
let width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 })
const [hovered, setHovered] = useState(false)
return (
<div className="flex flex-col items-center gap-1">
<motion.div
ref={ref}
style={{ width, height: width }}
className="aspect-square cursor-pointer relative group flex items-center justify-center"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={onClick}
>
{/* Icon Background (Squircle) */}
<motion.div
className={cn(
"absolute inset-0 rounded-2xl bg-gradient-to-br shadow-lg flex items-center justify-center transition-all duration-200",
color,
isActive ? "ring-2 ring-white/50 ring-offset-2 ring-offset-black/50" : ""
)}
whileTap={{ scale: 0.85 }}
animate={isActive ? { y: [0, -10, 0] } : {}}
transition={isActive ? { duration: 0.5, ease: "easeInOut", repeat: 0 } : {}}
>
{/* Glossy Overlay */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-b from-white/30 to-transparent opacity-50" />
{/* Icon */}
<Icon className="text-white relative z-10 w-1/2 h-1/2 drop-shadow-md" />
</motion.div>
{/* Tooltip */}
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }}
animate={{ opacity: 1, y: -50, x: "-50%" }}
exit={{ opacity: 0, y: 2, x: "-50%" }}
transition={{ duration: 0.2 }}
className="absolute left-1/2 -top-2 px-3 py-1 bg-neutral-900/90 text-neutral-200 text-xs font-medium rounded-md pointer-events-none whitespace-nowrap border border-white/10 shadow-xl backdrop-blur-md z-50"
>
{label}
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-neutral-900/90 rotate-45 border-r border-b border-white/10" />
</motion.div>
)}
</AnimatePresence>
</motion.div>
{/* Active Dot */}
<div className={cn(
"w-1 h-1 rounded-full bg-white/50 transition-all duration-300",
isActive ? "opacity-100 scale-100" : "opacity-0 scale-0"
)} />
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest

