# Dock Magnify — React Menu Component

> A dock whose icons swell by their distance to the cursor, lift their labels, bounce on launch and respond to keyboard focus the same way.

- Category: [Docks & Menus](https://pixelpapa.com/components/docks.md)
- Tags: dock, magnify, hover, spring, macos
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/docks/dock-magnify

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/dock-magnify.json"
```

The file is written to `components/pixelpapa/dock-magnify.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Calendar, Camera, Compass, FolderOpen, Mail, MessageCircle, Music, Settings, Trash2, type LucideIcon } from "lucide-react";
import { animate, motion, useMotionValue, useSpring, useTransform, type MotionValue } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";

export type DockItem = { label: string; icon: LucideIcon; gradient: string; running?: boolean; separatorBefore?: boolean };

type MagnifyDockProps = {
  items: DockItem[];
  /** Resting icon size in px. */
  size?: number;
  /** Icon size directly under the cursor. */
  magnifiedSize?: number;
  /** How far (px) the magnification reaches on each side. */
  reach?: number;
  /** Sweep across the dock once on mount so it never sits flat. */
  intro?: boolean;
  className?: string;
};

function DockIcon({ item, pointer, size, magnifiedSize, reach }: { item: DockItem; pointer: MotionValue<number>; size: number; magnifiedSize: number; reach: number }) {
  const ref = useRef<HTMLButtonElement>(null);
  const [hovered, setHovered] = useState(false);
  const [running, setRunning] = useState(item.running ?? false);
  const lift = useMotionValue(0);
  const distance = useTransform(pointer, (x) => {
    const r = ref.current?.getBoundingClientRect();
    return r ? x - (r.left + r.width / 2) : Infinity;
  });
  const target = useTransform(distance, [-reach, 0, reach], [size, magnifiedSize, size]);
  const width = useSpring(target, { mass: 0.1, stiffness: 190, damping: 14 });

  function centre() {
    const r = ref.current?.getBoundingClientRect();
    if (r) pointer.set(r.left + r.width / 2);
  }

  return (
    <div className={cn("relative flex flex-col items-center", item.separatorBefore && "ml-3 before:absolute before:top-1 before:bottom-1 before:-left-3 before:w-px before:bg-border")}>
      <motion.span
        aria-hidden
        animate={{ opacity: hovered ? 1 : 0, y: hovered ? 0 : 6, scale: hovered ? 1 : 0.9 }}
        transition={{ type: "spring", stiffness: 500, damping: 30 }}
        className="pointer-events-none absolute bottom-full mb-2.5 rounded-lg bg-foreground px-2.5 py-1 text-xs font-medium whitespace-nowrap text-background shadow-lg"
      >
        {item.label}
      </motion.span>
      <motion.button
        ref={ref}
        aria-label={item.label}
        style={{ width, height: width, y: lift }}
        onPointerEnter={() => setHovered(true)}
        onPointerLeave={() => setHovered(false)}
        onFocus={() => {
          setHovered(true);
          centre();
        }}
        onBlur={() => {
          setHovered(false);
          pointer.set(Infinity);
        }}
        onClick={() => {
          setRunning(true);
          animate(lift, [0, -22, 0, -9, 0], { duration: 0.7, times: [0, 0.3, 0.6, 0.8, 1], ease: "easeOut" });
        }}
        className={cn("flex items-center justify-center rounded-[26%] bg-linear-to-b text-white shadow-md shadow-black/15 outline-offset-4 [&_svg]:size-1/2", item.gradient)}
      >
        <item.icon strokeWidth={1.8} />
      </motion.button>
      <span className={cn("mt-1 size-1 rounded-full bg-foreground/60 transition-opacity", !running && "opacity-0")} />
    </div>
  );
}

export function MagnifyDock({ items, size = 46, magnifiedSize = 82, reach = 130, intro = true, className }: MagnifyDockProps) {
  const ref = useRef<HTMLDivElement>(null);
  const pointer = useMotionValue(Infinity);

  // Rest an imaginary cursor over the third icon so the dock never sits flat; a real pointer takes over.
  useEffect(() => {
    const r = ref.current?.getBoundingClientRect();
    if (!intro || !r) return;
    const rest = r.left + 12 + size * 2.5 + 24;
    // Icon widths are springs, so jumping the pointer still reads as a smooth swell.
    pointer.set(rest);
  }, [intro, pointer, size]);

  return (
    <div
      ref={ref}
      role="toolbar"
      aria-label="Dock"
      onPointerMove={(e) => pointer.set(e.clientX)}
      onPointerLeave={() => pointer.set(Infinity)}
      style={{ height: size + 24 }}
      className={cn("flex w-fit items-end gap-3 rounded-[1.4rem] border border-border bg-card/75 px-3 pb-1.5 shadow-xl shadow-black/10 backdrop-blur-xl", className)}
    >
      {items.map((item) => (
        <DockIcon key={item.label} item={item} pointer={pointer} size={size} magnifiedSize={magnifiedSize} reach={reach} />
      ))}
    </div>
  );
}

const apps: DockItem[] = [
  { label: "Files", icon: FolderOpen, gradient: "from-sky-400 to-blue-600", running: true },
  { label: "Browser", icon: Compass, gradient: "from-cyan-400 to-teal-600" },
  { label: "Mail", icon: Mail, gradient: "from-indigo-400 to-indigo-600", running: true },
  { label: "Messages", icon: MessageCircle, gradient: "from-emerald-400 to-green-600" },
  { label: "Calendar", icon: Calendar, gradient: "from-rose-400 to-red-600" },
  { label: "Music", icon: Music, gradient: "from-fuchsia-400 to-pink-600", running: true },
  { label: "Camera", icon: Camera, gradient: "from-amber-400 to-orange-600" },
  { label: "Settings", icon: Settings, gradient: "from-slate-400 to-slate-600" },
  { label: "Trash", icon: Trash2, gradient: "from-zinc-400 to-zinc-600", separatorBefore: true },
];

export default function DockMagnifyDemo() {
  return (
    <div className="relative flex h-80 w-full max-w-[700px] items-end justify-center overflow-hidden rounded-3xl border border-border bg-muted/40 pb-5">
      <div
        aria-hidden
        className="absolute inset-0 opacity-70 [background:radial-gradient(60%_70%_at_20%_10%,color-mix(in_oklch,var(--chart-2)_35%,transparent),transparent),radial-gradient(55%_70%_at_85%_20%,color-mix(in_oklch,var(--chart-3)_35%,transparent),transparent),radial-gradient(60%_60%_at_50%_100%,color-mix(in_oklch,var(--chart-1)_30%,transparent),transparent)]"
      />
      <p className="absolute top-8 text-sm text-muted-foreground">Move across the dock, or Tab through it.</p>
      <MagnifyDock items={apps} className="relative" />
    </div>
  );
}
```

## AI prompt

```text
Add the "Dock Magnify" component from PixelPapa UI to this project.

What it is: A dock whose icons swell by their distance to the cursor, lift their labels, bounce on launch and respond to keyboard focus the same way.
Category: Docks & Menus. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/dock-magnify.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/dock-magnify.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/dock-magnify" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More docks & menus

- [Dock Bottom Nav Mobile](https://pixelpapa.com/components/docks/dock-bottom-nav-mobile.md): A mobile tab bar where a blob slides under the active tab, squashing as it travels, while the icon hops up into it and its label fades in below.
- [Dock Expandable FAB](https://pixelpapa.com/components/docks/dock-expandable-fab.md): A floating action button that turns its plus into a cross and fires a speed-dial of labelled actions upward with staggered springs.
- [Dock Floating Toolbar](https://pixelpapa.com/components/docks/dock-floating-toolbar.md) (Pro): A text-format toolbar that pops over a selection: toggles spring into their active state and the whole bar morphs into a link field and back.
- [Menu Bar App](https://pixelpapa.com/components/docks/menu-bar-app.md) (Pro): A desktop-style File / Edit / View menubar: click once, then glide between menus as the highlight follows, with check items, shortcuts and full arrow-key navigation.
- [Menu Context Right Click](https://pixelpapa.com/components/docks/menu-context-right-click.md) (Pro): A custom context menu that opens at the pointer, flips to stay inside its area, scales from the corner nearest the click and is fully keyboard driven.
- [Menu Dropdown Nested](https://pixelpapa.com/components/docks/menu-dropdown-nested.md) (Pro): A dropdown with submenus to any depth: they open on hover intent or ArrowRight, scale out from their parent row and close level by level with ArrowLeft and Escape.
