# Magnify Dock

> macOS-style dock whose icons magnify as the pointer approaches.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/magnify-dock
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/magnify-dock.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/magnify-dock-demo.tsx"
"use client"

import { Calendar, Camera, Folder, House, Mail, MessageSquare, Music, Settings } from "lucide-react"

import { MagnifyDock } from "@/components/magnify-dock"

export default function MagnifyDockDemo() {
  return (
    <MagnifyDock
      items={[
        { label: "Home", icon: House, active: true },
        { label: "Files", icon: Folder },
        { label: "Mail", icon: Mail, active: true },
        { label: "Messages", icon: MessageSquare },
        { label: "Calendar", icon: Calendar },
        { label: "Music", icon: Music },
        { label: "Photos", icon: Camera },
        { label: "Settings", icon: Settings },
      ]}
    />
  )
}
```

## Source

```tsx title="components/magnify-dock.tsx"
"use client"

import * as React from "react"
import type { LucideIcon } from "lucide-react"

import { cn } from "@/lib/utils"

export type DockItem = {
  label: string
  icon: LucideIcon
  href?: string
  onClick?: () => void
  active?: boolean
}

export interface MagnifyDockProps extends React.ComponentProps<"nav"> {
  items: DockItem[]
  /** Resting icon size in px */
  baseSize?: number
  /** Icon size directly under the pointer, in px */
  maxSize?: number
  /** Distance in px over which magnification fades out */
  range?: number
}

export function MagnifyDock({
  items,
  baseSize = 44,
  maxSize = 72,
  range = 140,
  className,
  ...props
}: MagnifyDockProps) {
  const [sizes, setSizes] = React.useState<number[] | null>(null)
  const refs = React.useRef<(HTMLElement | null)[]>([])

  // Measure in the event handler so render only reads state.
  const magnify = (pointerX: number) => {
    setSizes(
      items.map((_, i) => {
        const el = refs.current[i]
        if (!el) return baseSize
        const rect = el.getBoundingClientRect()
        const distance = Math.abs(pointerX - (rect.left + rect.width / 2))
        const t = Math.max(0, 1 - distance / range)
        return baseSize + (maxSize - baseSize) * (1 - Math.cos((t * Math.PI) / 2))
      })
    )
  }

  return (
    <nav
      onMouseMove={(e) => magnify(e.clientX)}
      onMouseLeave={() => setSizes(null)}
      className={cn(
        "flex max-w-full items-end gap-2 rounded-2xl border bg-background/70 px-3 pb-2.5 shadow-lg backdrop-blur-md",
        className
      )}
      style={{ height: baseSize + 22 }}
      {...props}
    >
      {items.map((item, i) => {
        const size = sizes?.[i] ?? baseSize
        const Comp = item.href ? "a" : "button"
        return (
          <Comp
            key={item.label}
            ref={(el: HTMLElement | null) => {
              refs.current[i] = el
            }}
            href={item.href}
            onClick={item.onClick}
            aria-label={item.label}
            className="group relative flex min-w-0 shrink items-center justify-center rounded-xl border bg-card text-card-foreground transition-[width,height] duration-150 ease-out outline-none focus-visible:ring-2 focus-visible:ring-ring"
            style={{ width: size, height: size }}
          >
            <item.icon style={{ width: size * 0.45, height: size * 0.45 }} />
            <span className="pointer-events-none absolute -top-9 rounded-md border bg-popover px-2 py-1 text-xs whitespace-nowrap text-popover-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
              {item.label}
            </span>
            {item.active && <span className="absolute -bottom-2 size-1 rounded-full bg-foreground/60" />}
          </Comp>
        )
      })}
    </nav>
  )
}
```
