{"slug":"dock","name":"Dock","description":"macOS-style dock with cursor-proximity magnification on each item.","category":"motion","source_url":"https://beui.saura3h.xyz/r/dock/raw","detail_url":"https://beui.saura3h.xyz/r/dock","raw_url":"https://beui.saura3h.xyz/r/dock/raw","page_url":"https://beui.saura3h.xyz/components/motion/dock","dependencies":["lucide-react","motion","react"],"internal":["@/components/app/icons","@/components/motion/dock","@/lib/utils"],"files":[{"path":"components/motion/dock.tsx","type":"component","content":"\"use client\";\n\nimport {\n  motion,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type MotionValue,\n  type SpringOptions,\n} from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  isValidElement,\n  useId,\n  useRef,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype DockContext = {\n  mouseX: MotionValue<number>;\n  size: number;\n  magnification: number;\n  distance: number;\n  spring: SpringOptions;\n  pillLayoutId: string;\n};\n\nconst DEFAULT_SPRING: SpringOptions = {\n  stiffness: 320,\n  damping: 22,\n  mass: 0.4,\n};\nconst PILL_SPRING: SpringOptions = { stiffness: 360, damping: 32, mass: 0.6 };\n\nexport interface DockProps {\n  children: ReactNode;\n  className?: string;\n  /** Base size of each item in px. */\n  size?: number;\n  /** Max size an item grows to when cursor is directly over it. */\n  magnification?: number;\n  /** Distance (px) over which magnification interpolates. */\n  distance?: number;\n  /** Spring options for the magnification transition. */\n  spring?: SpringOptions;\n}\n\nexport function Dock({\n  children,\n  size = 36,\n  magnification = 56,\n  distance = 110,\n  spring = DEFAULT_SPRING,\n  className,\n}: DockProps) {\n  const mouseX = useMotionValue(Infinity);\n  const pillLayoutId = useId();\n  const ctx: DockContext = {\n    mouseX,\n    size,\n    magnification,\n    distance,\n    spring,\n    pillLayoutId,\n  };\n\n  return (\n    <motion.div\n      onMouseMove={(e) => mouseX.set(e.clientX)}\n      onMouseLeave={() => mouseX.set(Infinity)}\n      className={cn(\n        \"inline-flex h-auto items-end gap-1.5 rounded-2xl px-2 py-1 glass-strong\",\n        className,\n      )}\n    >\n      {Children.map(children, (child) => {\n        if (!isValidElement(child)) return child;\n        const props = (child.props ?? {}) as { __dock?: DockContext };\n        if (\"__dock\" in props) {\n          return cloneElement(child as ReactElement<{ __dock?: DockContext }>, {\n            __dock: ctx,\n          });\n        }\n        return child;\n      })}\n    </motion.div>\n  );\n}\n\nexport interface DockItemProps {\n  children: ReactNode;\n  className?: string;\n  onClick?: () => void;\n  active?: boolean;\n  \"aria-label\"?: string;\n  __dock?: DockContext;\n}\n\nexport function DockItem({\n  children,\n  className,\n  onClick,\n  active,\n  __dock,\n  ...rest\n}: DockItemProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fallback = useMotionValue(Infinity);\n  const mouseX = __dock?.mouseX ?? fallback;\n  const size = __dock?.size ?? 44;\n  const magnification = __dock?.magnification ?? 44;\n  const distance = __dock?.distance ?? 120;\n  const spring = __dock?.spring ?? DEFAULT_SPRING;\n  const pillLayoutId = __dock?.pillLayoutId ?? \"dock-pill\";\n\n  const distanceCalc = useTransform(mouseX, (val) => {\n    const rect = ref.current?.getBoundingClientRect() ?? { x: 0, width: size };\n    return val - rect.x - rect.width / 2;\n  });\n\n  const widthRaw = useTransform(\n    distanceCalc,\n    [-distance, 0, distance],\n    [size, magnification, size],\n  );\n  const width = useSpring(widthRaw, spring);\n\n  return (\n    <motion.div\n      ref={ref}\n      onClick={onClick}\n      aria-label={rest[\"aria-label\"]}\n      style={{ width, height: width }}\n      className={cn(\n        \"relative flex shrink-0 items-center justify-center rounded-full text-(--color-fg)\",\n        className,\n      )}\n    >\n      {active ? (\n        <motion.span\n          layoutId={pillLayoutId}\n          transition={PILL_SPRING}\n          className=\"absolute inset-0.5 -z-10 rounded-xl bg-(--color-fg)/5\"\n        />\n      ) : null}\n      {children}\n    </motion.div>\n  );\n}\n\nexport function DockSeparator({ className }: { className?: string }) {\n  return (\n    <span\n      className={cn(\"mx-1 h-6 w-px self-center bg-(--color-border)\", className)}\n    />\n  );\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":"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"}]}