Velocity UI
Loading…
Menu

Capsule Dock

A premium macOS‑style Dock with a glass‑morphic backdrop that adapts to light/dark themes. Icons respond to mouse proximity with magnetic attraction, scaling smoothly from 1.0→1.4 and lifting ~18px with subtle rotation.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add velocity-capsule-dock

Source Code

velocity-capsule-dock.tsx
// VelocityCapsuleDock.tsx
'use client'
import React from 'react'
import { motion, useMotionValue, useMotionTemplate, useTransform } from 'framer-motion'
import { Home, Zap, Shield, BarChart, Cpu, Terminal } from 'lucide-react'
import { useMagneticPhysics } from './use-magnetic-physics'

const NAV_ITEMS = [
  { label: 'Internal', icon: Home },
  { label: 'Power', icon: Zap },
  { label: 'Secure', icon: Shield },
  { label: 'Metrics', icon: BarChart },
  { label: 'Compute', icon: Cpu },
  { label: 'Console', icon: Terminal },
]

export const VelocityCapsuleDock = () => {
  const mouseX = useMotionValue(Infinity)

  return (
    <nav className="relative w-full h-[64px] flex items-center justify-center">
      <motion.div
        onMouseMove={(e) => mouseX.set(e.pageX)}
        onMouseLeave={() => mouseX.set(Infinity)}
        className="relative flex items-center h-full gap-1 px-3 rounded-full border border-border bg-card/70 backdrop-blur-2xl shadow-[0_20px_40px_rgba(0,0,0,0.15)] overflow-hidden ring-1 ring-border/50"
      >
        {NAV_ITEMS.map((item, idx) => (
          <DockItem key={idx} mouseX={mouseX} item={item} />
        ))}
      </motion.div>
    </nav>
  )
}

const DockItem = ({ mouseX, item }: { mouseX: any; item: any }) => {
  const { ref, width, scale, y, proximity } = useMagneticPhysics(mouseX)
  const Icon = item.icon

  // Fluent "Bloom" - Stays inside the container
  const bloom = useMotionTemplate`radial-gradient(35px circle at 50% 50%, rgba(255,255,255,${useMotionTemplate`${proximity.get() * 0.15}`}), transparent)`

  return (
    <motion.div
      ref={ref}
      style={{ width }}
      className="relative flex items-center justify-center h-full group"
    >
      {/* Tooltip - Positioned above the dock bounds */}
      <motion.span
        style={{
          opacity: proximity,
          y: -42,
          scale: useMotionTemplate`${0.9 + proximity.get() * 0.1}`,
        }}
        className="absolute px-2.5 py-1 rounded-md bg-neutral-900/80 text-white text-[10px] font-medium backdrop-blur-xl border border-white/10 pointer-events-none whitespace-nowrap z-[110]"
      >
        {item.label}
      </motion.span>

      {/* Internal Fluent Light Stage */}
      <motion.div className="absolute inset-0 pointer-events-none" style={{ background: bloom }} />

      {/* The Icon Container */}
      <motion.div
        style={{
          scale,
          y,
          opacity: useTransform(proximity, [0, 1], [0.75, 1]),
        }}
        className="relative flex items-center justify-center aspect-square w-10 h-10 z-10"
      >
        <Icon
          size={22}
          className="text-foreground/70 group-hover:text-foreground transition-colors duration-200"
          strokeWidth={1.5}
          style={{ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.2))' }}
        />
      </motion.div>

      {/* Active Indicator Dot */}
      <motion.div
        className="absolute bottom-1.5 w-1 h-[2px] bg-white/40 rounded-full"
        style={{
          opacity: useMotionTemplate`${proximity.get() * 0.8}`,
          scaleX: useMotionTemplate`${0.6 + proximity.get() * 0.4}`,
        }}
      />
    </motion.div>
  )
}

export default VelocityCapsuleDock

Source Code

use-magnetic-physics.tsx
"use client";
import { MotionValue, useSpring, useTransform } from "framer-motion";
import { useMemo, useRef } from "react";

const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));

export function useMagneticPhysics(mouseX: MotionValue<number>, radius: number = 200) {
  const ref = useRef<HTMLDivElement>(null);

  const distance = useTransform(mouseX, (val: number) => {
    const bounds = ref.current?.getBoundingClientRect() ?? { left: 0, width: 0 };
    return val - (bounds.left + bounds.width / 2);
  });

  const proximity = useTransform(distance, (d) => {
    if (!isFinite(d)) return 0;
    const p = 1 - Math.abs(d) / radius;
    // Cubic ease-in for a "magnetic snap" feel
    return Math.pow(clamp(p, 0, 1), 3); 
  });

  const widthT = useTransform(proximity, [0, 1], [44, 72]);
  const scaleT = useTransform(proximity, [0, 1], [1, 1.45]);
  const liftT = useTransform(proximity, [0, 1], [0, -10]);

  // Spring configured for "High-End Damping" - no wobble, just smooth travel
  const springCfg = { mass: 0.1, stiffness: 260, damping: 24 };
  
  const width = useSpring(widthT, springCfg);
  const scale = useSpring(scaleT, springCfg);
  const y = useSpring(liftT, springCfg);

  return { ref, width, scale, y, proximity };
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest

Props

Component property reference.

NameTypeDefaultDescription
itemsArray<{ label: string; icon: LucideIcon }>NAV_ITEMSIcons and labels to render in the dock. Each item includes a label and a Lucide icon component.
radiusnumber140Proximity radius for magnetic attraction. Larger values create a wider area of influence.
classNamestringundefinedAdditional classes for the dock container to customize spacing or shadows.
onSelect(index: number, item: { label: string; icon: LucideIcon }) => voidundefinedCallback fired when an icon is clicked. Receives the item index and item payload.
fixedbooleantrueIf true, positions the dock fixed at the bottom center. Set false to mount inline for previews.
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.