{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dock",
  "title": "Dock",
  "description": "macOS-style dock with pointer-distance magnification, spring tooltips, active dots, badges and click bounce.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://trytweenly.vercel.app/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/new-york/dock/dock.tsx",
      "content": "\"use client\"\n\nimport { createContext, useContext, useState } from \"react\"\nimport {\n  AnimatePresence,\n  motion,\n  useAnimate,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotion } from \"@/registry/new-york/hooks/use-reduced-motion\"\n\nexport type DockOrientation = \"horizontal\" | \"vertical\"\nexport type DockVariant = \"glass\" | \"solid\" | \"minimal\"\nexport type DockPosition = \"static\" | \"fixed-bottom\" | \"fixed-left\"\n\nexport interface DockSpring {\n  /** Default: 450 */\n  stiffness?: number\n  /** Default: 34 */\n  damping?: number\n  /** Default: 0.25 */\n  mass?: number\n}\n\nexport interface DockProps {\n  children: React.ReactNode\n  /** Lay the items out in a row or a column. Default: \"horizontal\" */\n  orientation?: DockOrientation\n  /** Resting item size in px. Default: 44 */\n  size?: number\n  /** Item size in px right under the pointer. Default: 72 */\n  magnification?: number\n  /** How far from the pointer, in px, items still grow. Default: 140 */\n  distance?: number\n  /** Spring used for the magnification. Default: { stiffness: 450, damping: 34, mass: 0.25 } */\n  spring?: DockSpring\n  /** Surface style. Default: \"glass\" */\n  variant?: DockVariant\n  /** Where the dock sits. Default: \"static\" */\n  position?: DockPosition\n  /** Bounce items when clicked. Default: true */\n  bounce?: boolean\n  /** Accessible label for the dock. Default: \"Dock\" */\n  label?: string\n  className?: string\n}\n\nexport interface DockItemProps {\n  /** Tooltip text and accessible name. */\n  label: string\n  /** Render the item as a link. */\n  href?: string\n  /** Click handler. */\n  onClick?: (event: React.MouseEvent<HTMLElement>) => void\n  /** Show the active indicator dot. Default: false */\n  active?: boolean\n  /** Count shown in a bubble on the corner. Hidden when 0 or undefined. */\n  badge?: number\n  /** The icon. SVG children are sized to fit automatically. */\n  children: React.ReactNode\n  className?: string\n}\n\ninterface DockContextValue {\n  pointer: MotionValue<number>\n  horizontal: boolean\n  size: number\n  magnification: number\n  distance: number\n  spring: Required<DockSpring>\n  bounce: boolean\n  reduced: boolean\n}\n\nconst DockContext = createContext<DockContextValue | null>(null)\n\nfunction useDock() {\n  const ctx = useContext(DockContext)\n  if (!ctx) throw new Error(\"DockItem and DockSeparator must be used inside <Dock>\")\n  return ctx\n}\n\nconst ENTER = [0.22, 1, 0.36, 1] as const\nconst PAD = 8\n\nconst surface: Record<DockVariant, string> = {\n  glass:\n    \"border border-border bg-background/60 shadow-[0_8px_32px_-8px_rgba(0,0,0,0.35)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/45\",\n  solid: \"border border-border bg-card shadow-[0_8px_28px_-10px_rgba(0,0,0,0.4)]\",\n  minimal: \"bg-transparent\",\n}\n\nconst placement: Record<DockPosition, string> = {\n  static: \"relative\",\n  \"fixed-bottom\": \"fixed bottom-4 left-1/2 z-50 -translate-x-1/2\",\n  \"fixed-left\": \"fixed left-4 top-1/2 z-50 -translate-y-1/2\",\n}\n\nexport function Dock({\n  children,\n  orientation = \"horizontal\",\n  size = 44,\n  magnification = 72,\n  distance = 140,\n  spring,\n  variant = \"glass\",\n  position = \"static\",\n  bounce = true,\n  label = \"Dock\",\n  className,\n}: DockProps) {\n  const reduced = useReducedMotion()\n  const mouseX = useMotionValue(Infinity)\n  const mouseY = useMotionValue(Infinity)\n  const horizontal = orientation === \"horizontal\"\n\n  const value: DockContextValue = {\n    pointer: horizontal ? mouseX : mouseY,\n    horizontal,\n    size,\n    magnification: Math.max(size, magnification),\n    distance,\n    spring: { stiffness: 450, damping: 34, mass: 0.25, ...spring },\n    bounce,\n    reduced,\n  }\n\n  const rest = size + PAD * 2\n\n  return (\n    <DockContext.Provider value={value}>\n      <nav\n        aria-label={label}\n        onPointerMove={(e) => {\n          if (e.pointerType === \"touch\") return\n          mouseX.set(e.clientX)\n          mouseY.set(e.clientY)\n        }}\n        onPointerLeave={() => {\n          mouseX.set(Infinity)\n          mouseY.set(Infinity)\n        }}\n        className={cn(\n          \"flex w-max gap-2 rounded-[22px]\",\n          horizontal ? \"flex-row items-end\" : \"flex-col items-start\",\n          surface[variant],\n          placement[position],\n          className\n        )}\n        style={horizontal ? { height: rest, padding: `0 ${PAD}px ${PAD}px` } : { width: rest, padding: `${PAD}px 0 ${PAD}px ${PAD}px` }}\n      >\n        {children}\n      </nav>\n    </DockContext.Provider>\n  )\n}\n\nexport function DockItem({ label, href, onClick, active = false, badge, children, className }: DockItemProps) {\n  const { pointer, horizontal, size, magnification, distance, spring, bounce, reduced } = useDock()\n  const [scope, animate] = useAnimate<HTMLDivElement>()\n  const [hovered, setHovered] = useState(false)\n  const [focused, setFocused] = useState(false)\n  const open = hovered || focused\n\n  const offset = useTransform(pointer, (p) => {\n    const el = scope.current\n    if (!el || !Number.isFinite(p)) return Infinity\n    const r = el.getBoundingClientRect()\n    return p - (horizontal ? r.left + r.width / 2 : r.top + r.height / 2)\n  })\n  const target = useTransform(offset, [-distance, 0, distance], reduced ? [size, size, size] : [size, magnification, size])\n  const itemSize = useSpring(target, spring)\n  const iconSize = useTransform(itemSize, (v) => v * 0.48)\n\n  const handleClick = (e: React.MouseEvent<HTMLElement>) => {\n    onClick?.(e)\n    if (bounce && !reduced && scope.current) {\n      const hop = horizontal ? { y: [0, -18, 0, -6, 0] } : { x: [0, 18, 0, 6, 0] }\n      animate(scope.current, hop, { duration: 0.7, ease: \"easeOut\" })\n    }\n  }\n\n  const shared = {\n    \"aria-label\": label,\n    onClick: handleClick,\n    onFocus: () => setFocused(true),\n    onBlur: () => setFocused(false),\n    className: cn(\n      \"relative flex size-full items-center justify-center rounded-[28%] border border-border bg-foreground/[0.06] text-foreground outline-none transition-colors hover:bg-foreground/[0.1] focus-visible:ring-2 focus-visible:ring-[#ff4d12] focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n      className\n    ),\n  }\n\n  const inner = (\n    <motion.span aria-hidden=\"true\" className=\"flex items-center justify-center [&_svg]:size-full\" style={{ width: iconSize, height: iconSize }}>\n      {children}\n    </motion.span>\n  )\n\n  return (\n    <motion.div\n      ref={scope}\n      className=\"relative shrink-0\"\n      style={{ width: itemSize, height: itemSize }}\n      onPointerEnter={() => setHovered(true)}\n      onPointerLeave={() => setHovered(false)}\n    >\n      {href ? (\n        <a href={href} aria-current={active ? \"page\" : undefined} {...shared}>\n          {inner}\n        </a>\n      ) : (\n        <button type=\"button\" aria-pressed={active || undefined} {...shared}>\n          {inner}\n        </button>\n      )}\n\n      {badge ? (\n        <motion.span\n          key={badge}\n          initial={reduced ? false : { scale: 0.4 }}\n          animate={{ scale: 1 }}\n          transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n          className=\"pointer-events-none absolute -right-1 -top-1 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-[#ff4d12] px-1 text-[10px] font-semibold leading-none text-white shadow ring-2 ring-background\"\n        >\n          {badge > 99 ? \"99+\" : badge}\n        </motion.span>\n      ) : null}\n\n      <AnimatePresence>\n        {active && (\n          <motion.span\n            aria-hidden=\"true\"\n            initial={{ opacity: 0, scale: 0 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0 }}\n            transition={{ duration: reduced ? 0 : 0.25, ease: ENTER }}\n            className={cn(\n              \"pointer-events-none absolute size-1 rounded-full bg-foreground/70\",\n              horizontal ? \"-bottom-[6px] left-1/2 -ml-0.5\" : \"-left-[6px] top-1/2 -mt-0.5\"\n            )}\n          />\n        )}\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {open && (\n          <motion.span\n            role=\"tooltip\"\n            initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.85, ...(horizontal ? { y: 6 } : { x: -6 }) }}\n            animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}\n            exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.9, ...(horizontal ? { y: 4 } : { x: -4 }) }}\n            transition={reduced ? { duration: 0.12 } : { type: \"spring\", stiffness: 500, damping: 32 }}\n            className={cn(\n              \"pointer-events-none absolute z-10 whitespace-nowrap rounded-md border border-border bg-popover px-2 py-1 text-xs font-medium text-popover-foreground shadow-md\",\n              horizontal ? \"bottom-full left-1/2 mb-2.5 -translate-x-1/2 origin-bottom\" : \"left-full top-1/2 ml-3 -translate-y-1/2 origin-left\"\n            )}\n          >\n            {label}\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  )\n}\n\nexport function DockSeparator({ className }: { className?: string }) {\n  const { horizontal } = useDock()\n  return (\n    <div\n      role=\"separator\"\n      aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n      className={cn(\"shrink-0 bg-border\", horizontal ? \"mx-1 w-px self-stretch mt-3\" : \"my-1 h-px self-stretch mr-3\", className)}\n    />\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/dock",
  "type": "registry:component"
}