{"slug":"dock","name":"Dock","description":"macOS-style dock with grouped actions and a gliding active pill.","category":"motion","source_url":"https://beui.dev/r/dock/raw","detail_url":"https://beui.dev/r/dock","raw_url":"https://beui.dev/r/dock/raw","page_url":"https://beui.dev/components/motion/dock","dependencies":["clsx","lucide-react","motion","react","tailwind-merge"],"internal":["@/components/app/icons","@/components/motion/dock","@/lib/ease","@/lib/utils"],"files":[{"path":"components/motion/dock.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { createContext, useContext, useId, useMemo, type ReactNode } from \"react\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype DockContextValue = {\n  size: number;\n  pillLayoutId: string;\n};\n\nconst DockContext = createContext<DockContextValue | null>(null);\n\nexport interface DockProps {\n  children: ReactNode;\n  className?: string;\n  /** Size of each item in px. */\n  size?: number;\n}\n\nexport function Dock({ children, size = 44, className }: DockProps) {\n  const pillLayoutId = useId();\n  const ctx = useMemo<DockContextValue>(\n    () => ({ size, pillLayoutId }),\n    [size, pillLayoutId],\n  );\n\n  return (\n    <DockContext.Provider value={ctx}>\n      <div\n        className={cn(\n          \"inline-flex h-auto items-end gap-1.5 rounded-2xl border border-border bg-card/80 px-2 py-1 shadow-2xl backdrop-blur-xl\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </DockContext.Provider>\n  );\n}\n\nexport interface DockItemProps {\n  children: ReactNode;\n  className?: string;\n  /** When set, the item renders as a <button>. Omit when children carry their own link or button. */\n  onClick?: () => void;\n  active?: boolean;\n  \"aria-label\"?: string;\n}\n\nexport function DockItem({\n  children,\n  className,\n  onClick,\n  active,\n  ...rest\n}: DockItemProps) {\n  const dock = useContext(DockContext);\n  const reduce = useReducedMotion();\n  const size = dock?.size ?? 44;\n  const pillLayoutId = dock?.pillLayoutId ?? \"dock-pill\";\n\n  const pill = active ? (\n    <motion.span\n      layoutId={pillLayoutId}\n      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n      className=\"absolute inset-0.5 -z-10 rounded-xl bg-primary/5\"\n    />\n  ) : null;\n  const sharedStyle = { width: size, height: size };\n  const sharedClass = cn(\n    \"relative flex shrink-0 items-center justify-center rounded-full text-foreground\",\n    className,\n  );\n\n  if (onClick) {\n    return (\n      <button\n        type=\"button\"\n        onClick={onClick}\n        aria-label={rest[\"aria-label\"]}\n        aria-pressed={active}\n        style={sharedStyle}\n        className={cn(\n          sharedClass,\n          \"cursor-pointer border-0 bg-transparent p-0 outline-none\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        )}\n      >\n        {pill}\n        {children}\n      </button>\n    );\n  }\n\n  // Children carry their own link or button (and its accessible name).\n  return (\n    <div style={sharedStyle} className={sharedClass}>\n      {pill}\n      {children}\n    </div>\n  );\n}\n\nexport function DockSeparator({ className }: { className?: string }) {\n  return (\n    <span\n      aria-hidden\n      className={cn(\"mx-1 h-6 w-px self-center bg-border\", className)}\n    />\n  );\n}\n"},{"path":"lib/ease.ts","type":"util","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/previews/motion/dock.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { Calendar, Home, Mail, Music, Settings, Sparkles } from \"lucide-react\";\nimport { GithubIcon } from \"@/components/app/icons\";\nimport { Dock, DockItem, DockSeparator } from \"@/components/motion/dock\";\n\nexport function DockPreview() {\n  const [active, setActive] = useState(\"home\");\n  const items = [\n    { id: \"home\", icon: Home, label: \"Home\" },\n    { id: \"mail\", icon: Mail, label: \"Mail\" },\n    { id: \"calendar\", icon: Calendar, label: \"Calendar\" },\n    { id: \"music\", icon: Music, label: \"Music\" },\n    { id: \"discover\", icon: Sparkles, label: \"Discover\" },\n  ];\n\n  return (\n    <div className=\"flex w-full justify-center\">\n      <Dock>\n        {items.map(({ id, icon: Icon, label }) => (\n          <DockItem\n            key={id}\n            aria-label={label}\n            active={active === id}\n            onClick={() => setActive(id)}\n          >\n            <Icon className=\"h-5 w-5\" />\n          </DockItem>\n        ))}\n        <DockSeparator />\n        <DockItem\n          aria-label=\"Settings\"\n          active={active === \"settings\"}\n          onClick={() => setActive(\"settings\")}\n        >\n          <Settings className=\"h-5 w-5\" />\n        </DockItem>\n        <DockItem aria-label=\"GitHub\">\n          <GithubIcon className=\"h-5 w-5\" />\n        </DockItem>\n      </Dock>\n    </div>\n  );\n}\n"},{"path":"components/app/icons.tsx","type":"util","content":"import type { SVGProps } from \"react\";\n\nexport function GithubIcon(props: SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      {...props}\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 .5C5.65.5.5 5.65.5 12.02c0 5.1 3.29 9.43 7.86 10.96.58.1.79-.25.79-.56v-2.01c-3.2.7-3.87-1.54-3.87-1.54-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.69.08-.69 1.15.08 1.76 1.18 1.76 1.18 1.02 1.76 2.68 1.25 3.34.96.1-.74.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.04 0 0 .96-.31 3.15 1.18.91-.25 1.89-.38 2.87-.39.97 0 1.96.13 2.87.39 2.19-1.49 3.15-1.18 3.15-1.18.62 1.58.23 2.75.11 3.04.74.81 1.18 1.84 1.18 3.1 0 4.42-2.7 5.4-5.27 5.68.42.36.78 1.07.78 2.16v3.2c0 .31.21.67.8.56 4.57-1.53 7.85-5.86 7.85-10.96C23.5 5.65 18.35.5 12 .5Z\"\n      />\n    </svg>\n  );\n}\n"}]}