Components
Loading preview...
A premium macOS-style floating dock featuring smooth fluid magnification and physics-based scaling animations on hover. Includes elegant tooltip micro-labels and custom icon container formatting. The perfect minimalist navigation shell for SaaS platforms, tools, and AI desktop wr
npx shadcn@latest add https://21st.dev/r/arunjdass/mac-os-fluid-floating-dockimport React, { useRef, useState, useEffect } from 'react';
interface DockItemProps {
icon: React.ReactNode;
label: string;
mouseX: number | null;
index: number;
}
const DockItem = ({ icon, label, mouseX, index }: DockItemProps) => {
const ref = useRef<HTMLButtonElement>(null);
const [scale, setScale] = useState(1);
const targetScale = useRef(1);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
let animationFrameId: number;
const animate = () => {
setScale((prev) => {
const next = prev + (targetScale.current - prev) * 0.2;
if (Math.abs(targetScale.current - next) < 0.001) return targetScale.current;
return next;
});
animationFrameId = requestAnimationFrame(animate);
};
animate();
return () => cancelAnimationFrame(animationFrameId);
}, []);
useEffect(() => {
if (!ref.current || mouseX === null) {
targetScale.current = 1;
return;
}
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const distance = Math.abs(mouseX - centerX);
const maxDistance = 150;
if (distance < maxDistance) {
const newScale = 1 + 0.6 * Math.cos((distance / maxDistance) * (Math.PI / 2));
targetScale.current = newScale;
} else {
targetScale.current = 1;
}
}, [mouseX]);
return (
<div className="relative flex flex-col items-center group">
<div
className={`absolute -top-14 px-4 py-2 bg-black/90 backdrop-blur-xl text-white text-xs font-medium rounded-lg whitespace-nowrap border border-white/10 shadow-[0_4px_20px_rgba(0,0,0,0.5)] transition-all duration-300 pointer-events-none ${isHovered ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-2 scale-95'}`}
>
{label}
</div>
<button
ref={ref}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
width: `${56 * scale}px`,
height: `${56 * scale}px`,
}}
className="flex items-center justify-center rounded-2xl bg-gradient-to-b from-white/10 to-white/5 border border-white/10 backdrop-blur-xl shadow-[0_8px_30px_rgba(0,0,0,0.3)] transition-colors duration-300 hover:bg-white/10 hover:border-white/20 cursor-pointer overflow-hidden"
>
<div style={{ transform: `scale(${scale * 0.8})` }} className="text-white/80 group-hover:text-white transition-colors duration-300">
{icon}
</div>
</button>
</div>
);
};
export default function FloatingDockPreview() {
const [mouseX, setMouseX] = useState<number | null>(null);
const icons = [
{ label: "Home", icon: <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> },
{ label: "Search", icon: <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> },
{ label: "Agents", icon: <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg> },
{ label: "Memory", icon: <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M3 15h6"/><path d="M3 18h6"/><path d="M3 21h6"/></svg> },
{ label: "Settings", icon: <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><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"/></svg> },
];
return (
<div className="flex flex-col items-center justify-start w-full min-h-[700px] bg-background p-4 md:p-8 pt-32">
<div className="text-center mb-16 pt-10">
<h2 className="text-4xl font-semibold tracking-tight text-white mb-4">Floating Dock</h2>
<p className="text-white/60 max-w-md mx-auto text-lg">Hover over the icons below to see the fluid macOS-style scaling animation.</p>
</div>
<div className="relative">
<div
className="absolute inset-0 -top-8 -bottom-8 -left-8 -right-8"
onMouseMove={(e) => setMouseX(e.clientX)}
onMouseLeave={() => setMouseX(null)}
/>
<div
className="relative flex items-end gap-3 rounded-3xl bg-black/20 p-3 border border-white/5 backdrop-blur-xl shadow-2xl"
onMouseMove={(e) => setMouseX(e.clientX)}
onMouseLeave={() => setMouseX(null)}
>
{icons.map((item, idx) => (
<DockItem
key={idx}
index={idx}
label={item.label}
icon={item.icon}
mouseX={mouseX}
/>
))}
</div>
</div>
</div>
);
}