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
dock-icon.tsx
"use client"
import { useRef, useState } from "react"
import {
motion,
useMotionValue,
useSpring,
useTransform,
AnimatePresence,
type MotionValue,
} from "framer-motion"
import { cn } from "@/lib/utils"
import type { DockItem } from "./dock-items"
type DockIconProps = {
mouseX: MotionValue<number>
item: DockItem
isActive: boolean
onClick: () => void
}
export function DockIcon({ mouseX, item, isActive, onClick }: DockIconProps) {
const ref = useRef<HTMLDivElement>(null)
const Icon = item.icon
const [hovered, setHovered] = useState(false)
const distance = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }
return val - bounds.x - bounds.width / 2
})
const widthSync = useTransform(distance, [-150, 0, 150], [38, 72, 38])
const width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 })
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}
>
<motion.div
className={cn(
"absolute inset-0 rounded-2xl bg-white dark:bg-zinc-100 border border-slate-200/80 dark:border-zinc-300 shadow-lg flex items-center justify-center overflow-hidden transition-all duration-200",
isActive && "ring-2 ring-white/60 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 } : {}}
>
<div className="absolute inset-0 bg-gradient-to-b from-white/40 to-transparent opacity-60" />
<Icon className="relative z-10 w-[42%] h-[42%]" />
</motion.div>
<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"
>
{item.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>
<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>
)
}
dock-items.ts
dock-items.ts
import type { ComponentType } from "react"
import {
ReactIcon,
NextjsIcon,
TailwindCssIcon,
ClaudeIcon,
GeminiIcon,
CopilotIcon,
ChatgptIcon,
FramerIcon,
} from "@/components/icons/tech-svgs"
import { Settings } from "lucide-react"
export type DockItem = {
id: string
label: string
icon: ComponentType<{ className?: string }>
dividerAfter?: boolean
}
export const MAIN_DOCK_ITEMS: DockItem[] = [
{ id: "react", label: "React", icon: ReactIcon },
{ id: "nextjs", label: "Next.js", icon: NextjsIcon },
{ id: "tailwind", label: "Tailwind", icon: TailwindCssIcon },
{ id: "claude", label: "Claude", icon: ClaudeIcon },
{ id: "gemini", label: "Gemini", icon: GeminiIcon },
{ id: "copilot", label: "Copilot", icon: CopilotIcon },
{ id: "chatgpt", label: "ChatGPT", icon: ChatgptIcon, dividerAfter: true },
]
export const UTILITY_DOCK_ITEMS: DockItem[] = [
{ id: "framer", label: "Framer", icon: FramerIcon },
{ id: "settings", label: "Settings", icon: Settings },
]
Source Code
magnetic-nav-dock.tsx
"use client"
import { useState } from "react"
import { motion, useMotionValue } from "framer-motion"
import { cn } from "@/lib/utils"
import { DockIcon } from "./dock-icon"
import { MAIN_DOCK_ITEMS, UTILITY_DOCK_ITEMS } from "./dock-items"
export type MagneticNavDockProps = {
className?: string
defaultActive?: string
onItemClick?: (id: string) => void
}
export default function MagneticNavDock({
className,
defaultActive = "react",
onItemClick,
}: MagneticNavDockProps) {
const mouseX = useMotionValue(Infinity)
const [activeApp, setActiveApp] = useState(defaultActive)
const handleClick = (id: string) => {
setActiveApp(id)
onItemClick?.(id)
}
return (
<div
className={cn(
"relative flex min-h-[500px] w-full items-center justify-center overflow-hidden bg-neutral-950 font-sans",
className
)}
>
<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>
<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"
>
{MAIN_DOCK_ITEMS.map((item) => (
<div key={item.id} className="contents">
<DockIcon
mouseX={mouseX}
item={item}
isActive={activeApp === item.id}
onClick={() => handleClick(item.id)}
/>
{item.dividerAfter && <div className="h-10 w-px bg-white/10 mx-1 self-center" />}
</div>
))}
{UTILITY_DOCK_ITEMS.map((item) => (
<DockIcon
key={item.id}
mouseX={mouseX}
item={item}
isActive={activeApp === item.id}
onClick={() => handleClick(item.id)}
/>
))}
</motion.div>
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| className | string | - | Optional class name for the dock container. |
| defaultActive | string | - | ID of the dock item shown as active on first render. |
| onItemClick | (id: string) => void | - | Callback fired when a dock icon is clicked. |
Context Worth Keeping In Orbit
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.