Velocity UI
Loading…
Menu

Magnetic Navigation Dock

A macOS-style dock with magnetic magnification and spring physics for a playful navigation experience.

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-dock

Source 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: latest
  • lucide-react: latest
  • clsx: latest
  • tailwind-merge: latest