{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dock","title":"Magnifying Dock","description":"A dock whose items swell as the pointer nears, on springs. Distance drives the scale, so the row breathes rather than one icon popping.","dependencies":["framer-motion@^13.1.1","gsap@^3.15.0","@gsap/react@^2.1.2"],"files":[{"path":"src/components/Dock/index.js","content":"export { default } from './Dock.jsx'\n","type":"registry:component","target":"components/dock/index.js"},{"path":"src/components/Dock/Dock.jsx","content":"'use client'\n\nimport { useState, useRef, useCallback } from 'react'\nimport { motion, useMotionValue, useTransform, useSpring } from 'framer-motion'\nimport gsap from 'gsap'\nimport { useGSAP } from '@gsap/react'\nimport { HomeIcon, BellIcon, SearchIcon, SettingsIcon, UserIcon } from './Icons.jsx'\nimport './Dock.css'\n\ngsap.registerPlugin(useGSAP)\n\n/* The default row, used when no `items` are passed — a dock with nothing in it\n   is not a useful default, and these five are what the demos show. Pass your\n   own to replace them; the shape is { id, label, Icon }. */\nconst DOCK_ITEMS = [\n  { id: 'home', label: 'Home', Icon: HomeIcon },\n  { id: 'bell', label: 'Notifications', Icon: BellIcon },\n  { id: 'search', label: 'Search', Icon: SearchIcon },\n  { id: 'settings', label: 'Settings', Icon: SettingsIcon },\n  { id: 'user', label: 'Profile', Icon: UserIcon },\n]\n\nconst DISTANCE_RANGE = 130\nconst BASE_SIZE = 36       // Baseline item width/height (36px x 36px)\nconst MAGNIFIED_SIZE = 54  // Magnified item width/height (54px x 54px)\nconst MAX_Y = -14          // Upward elevation offset\n\n/* Standalone palette. These are the component's own defaults — deliberately\n   not tied to any host theme, so an installed copy looks right on day one.\n   Pass the props to match your own design; the dock writes them as custom\n   properties and the stylesheet reads them from there. */\nconst SURFACE = '#08101e'      // dock bar and item fill\nconst BORDER = '#142544'       // bar and tooltip hairline\nconst ITEM_BORDER = '#0c1f43'  // item hairline, at rest\nconst ACCENT = '#639aff'       // active item and backlight glow\nconst ICON = '#ffffff'         // glyphs and tooltip text\n\n/* =========================================================\n   DockItem Component (100% Opacity Tooltip + Slide-In & Fade Out)\n   ========================================================= */\n\nfunction DockItem({ item, index, mouseX, activeIndex, onClick, baseSize, magnifiedSize }) {\n  const { Icon, label, id } = item\n  const itemRef = useRef(null)\n  const isActive = activeIndex === index\n\n  // Calculate distance from mouse clientX to item center\n  const distance = useTransform(mouseX, (val) => {\n    const bounds = itemRef.current?.getBoundingClientRect()\n    if (!bounds || val === Infinity) return Infinity\n    return val - (bounds.x + bounds.width / 2)\n  })\n\n  // Dynamic button size (1:1 square scaling from 36px to 54px)\n  const sizeSync = useTransform(\n    distance,\n    [-DISTANCE_RANGE, 0, DISTANCE_RANGE],\n    [baseSize, magnifiedSize, baseSize]\n  )\n\n  // Dynamic upward elevation\n  const ySync = useTransform(\n    distance,\n    [-DISTANCE_RANGE, 0, DISTANCE_RANGE],\n    [0, MAX_Y, 0]\n  )\n\n  // Tooltip opacity: 100% (1.0) while hovering over/near the button, 0% when moving out\n  const tooltipOpacitySync = useTransform(distance, (d) => {\n    return Math.abs(d) <= 24 ? 1 : 0\n  })\n\n  // Tooltip slide offset: 0px when hovered (at 100% opacity), +10px when moving out (slides down & fades out)\n  const tooltipYOffsetSync = useTransform(distance, (d) => {\n    return Math.abs(d) <= 24 ? 0 : 10\n  })\n\n  // Responsive spring physics\n  const size = useSpring(sizeSync, { mass: 0.1, stiffness: 200, damping: 15 })\n  const y = useSpring(ySync, { mass: 0.1, stiffness: 200, damping: 15 })\n  const tooltipOpacity = useSpring(tooltipOpacitySync, { mass: 0.1, stiffness: 300, damping: 20 })\n  const tooltipYOffset = useSpring(tooltipYOffsetSync, { mass: 0.1, stiffness: 280, damping: 20 })\n\n  // Combine button elevation + tooltip slide offset\n  const tooltipY = useTransform([y, tooltipYOffset], ([latestY, latestOffset]) => latestY + latestOffset)\n\n  // Active micro-ripple effect via GSAP on click\n  useGSAP(() => {\n    if (!isActive || !itemRef.current) return\n    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return\n    gsap.fromTo(\n      itemRef.current,\n      { boxShadow: '0 0 0 0 rgba(34, 34, 34, 0.4)' },\n      {\n        boxShadow: '0 0 0 8px rgba(34, 34, 34, 0)',\n        duration: 0.6,\n        ease: 'power2.out',\n      }\n    )\n  }, [isActive])\n\n  return (\n    <div className=\"dock-item-container\">\n      {/* Tooltip Label - stays at 100% opacity on hover, slides in from bottom, completely fades out on leave */}\n      <motion.div\n        className=\"dock-tooltip\"\n        style={{\n          opacity: tooltipOpacity,\n          x: '-50%',\n          y: tooltipY,\n        }}\n      >\n        {label}\n      </motion.div>\n\n      {/* Button container scaling & elevating */}\n      <motion.button\n        ref={itemRef}\n        className={`dock-item${isActive ? ' dock-item--active' : ''}`}\n        style={{\n          width: size,\n          height: size,\n          y: y,\n        }}\n        onClick={() => onClick(index)}\n        /* Keyboard focus stands in for the pointer. Magnification and the\n           tooltip are both driven by `mouseX`, so pinning it to this item's\n           own centre gives a keyboard user the same swell and the same label\n           a mouse user gets, through the existing machinery rather than a\n           second code path. Pointer-induced focus is skipped, since the\n           pointer is already driving mouseX. */\n        onFocus={(e) => {\n          if (!e.target.matches(':focus-visible')) return\n          const r = itemRef.current?.getBoundingClientRect()\n          if (r) mouseX.set(r.x + r.width / 2)\n        }}\n        onBlur={() => mouseX.set(Infinity)}\n        whileTap={{ scale: 0.92 }}\n        aria-label={label}\n        aria-pressed={isActive}\n        id={`dock-item-${id}`}\n      >\n        <Icon size={24} color=\"currentColor\" />\n      </motion.button>\n    </div>\n  )\n}\n\n/* =========================================================\n   Main Dock Container\n   ========================================================= */\n\nexport default function Dock({\n  /* Content was a module constant, so changing the icons meant editing the\n     copied source, and `activeIndex` was internal state with no way out — the\n     host could neither set what was selected nor learn that it had changed. */\n  items = DOCK_ITEMS,\n  activeId,                 // controlled selection, by id\n  defaultActiveId = null,   // uncontrolled starting selection\n  onSelect,                 // (id, item, index) — fires on every activation\n\n  baseSize = BASE_SIZE,\n  magnifiedSize = MAGNIFIED_SIZE,\n\n  // --- paint ---\n  surface = SURFACE,\n  border = BORDER,\n  itemBorder = ITEM_BORDER,\n  accent = ACCENT,\n  icon = ICON,\n\n  'aria-label': ariaLabel = 'Dock',\n\n  /* The only component in the library that forwarded none of these. Without\n     them the dock is position: fixed with no way to place it anywhere else. */\n  className = '',\n  style,\n  ...rest\n}) {\n  const mouseX = useMotionValue(Infinity)\n  const dockRef = useRef(null)\n\n  /* Controlled when `activeId` is given, uncontrolled otherwise — the usual\n     React contract, so the host can drive it or ignore it. */\n  const [ownId, setOwnId] = useState(defaultActiveId)\n  const controlled = activeId !== undefined\n  const currentId = controlled ? activeId : ownId\n  const activeIndex = items.findIndex((i) => i.id === currentId)\n\n  const handleClick = useCallback(\n    (idx) => {\n      const item = items[idx]\n      if (!item) return\n      const next = item.id === currentId ? null : item.id\n      if (!controlled) setOwnId(next)\n      onSelect?.(next, next === null ? null : item, next === null ? -1 : idx)\n    },\n    [items, currentId, controlled, onSelect]\n  )\n\n  // Initial Entrance Animation using GSAP\n  useGSAP(() => {\n    if (!dockRef.current) return\n    // The dock is chrome, not content: with reduced motion it is simply there.\n    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return\n    const items = dockRef.current.querySelectorAll('.dock-item-container')\n    gsap.from(items, {\n      y: 50,\n      opacity: 0,\n      stagger: 0.06,\n      duration: 0.6,\n      ease: 'back.out(1.5)',\n      delay: 0.1,\n    })\n\n    gsap.from(dockRef.current, {\n      y: 30,\n      opacity: 0,\n      duration: 0.5,\n      ease: 'power3.out',\n    })\n  }, { scope: dockRef })\n\n  return (\n    <div\n      className={`dock-container ${className}`.trim()}\n      ref={dockRef}\n      style={{\n        '--dock-surface': surface,\n        '--dock-border': border,\n        '--dock-item-border': itemBorder,\n        '--dock-accent': accent,\n        '--dock-icon': icon,\n        ...style,\n      }}\n      {...rest}\n    >\n      {/* Ambient Backlight Glow */}\n      <div className=\"dock-glow\" />\n\n      <motion.div\n        className=\"dock\"\n        /* `group`, not `toolbar`: toolbar carries an arrow-key contract this\n           does not implement, and claiming a role you do not honour is worse\n           than claiming none. */\n        role=\"group\"\n        aria-label={ariaLabel}\n        onMouseMove={(e) => mouseX.set(e.clientX)}\n        onMouseLeave={() => mouseX.set(Infinity)}\n      >\n        {items.map((item, idx) => (\n          <DockItem\n            key={item.id}\n            item={item}\n            index={idx}\n            mouseX={mouseX}\n            activeIndex={activeIndex}\n            onClick={handleClick}\n            baseSize={baseSize}\n            magnifiedSize={magnifiedSize}\n          />\n        ))}\n      </motion.div>\n    </div>\n  )\n}\n","type":"registry:component","target":"components/dock/Dock.jsx"},{"path":"src/components/Dock/Icons.jsx","content":"export const HomeIcon = ({ size = 24, color = '#ffffff' }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M3 10.25V20C3 20.5523 3.44772 21 4 21H9V15C9 14.4477 9.44772 14 10 14H14C14.5523 14 15 14.4477 15 15V21H20C20.5523 21 21 20.5523 21 20V10.25\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n    <path\n      d=\"M1 12L11.3292 2.33876C11.7077 1.98577 12.2923 1.98577 12.6708 2.33876L23 12\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n)\n\nexport const BellIcon = ({ size = 24, color = '#ffffff' }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M18 8A6 6 0 1 0 6 8c0 7-3 9-3 9h18s-3-2-3-9Z\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n    <path\n      d=\"M13.73 21a2 2 0 0 1-3.46 0\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n)\n\nexport const SearchIcon = ({ size = 24, color = '#ffffff' }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <circle\n      cx=\"11\"\n      cy=\"11\"\n      r=\"7\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n    />\n    <path\n      d=\"M21 21L16.5 16.5\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n    />\n  </svg>\n)\n\nexport const SettingsIcon = ({ size = 24, color = '#ffffff' }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n    <circle\n      cx=\"12\"\n      cy=\"12\"\n      r=\"3\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n    />\n  </svg>\n)\n\nexport const UserIcon = ({ size = 24, color = '#ffffff' }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <circle\n      cx=\"12\"\n      cy=\"8\"\n      r=\"4.5\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n    />\n    <path\n      d=\"M20 21a8 8 0 1 0-16 0\"\n      stroke={color}\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n    />\n  </svg>\n)\n","type":"registry:component","target":"components/dock/Icons.jsx"},{"path":"src/components/Dock/Dock.css","content":"/* ==========================================================\n   Dock Container & Ambient Backlight Glow\n\n   Colours are read from four custom properties, each with the\n   standalone default baked in as a fallback. The component\n   writes them from its props, so the CSS works on its own if\n   you drop it in without the JSX — and a host that wants the\n   dock to follow its own theme sets the props rather than\n   editing this file.\n   ========================================================== */\n\n.dock-container {\n  position: fixed;\n  bottom: 50px;\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 100;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.dock-glow {\n  position: absolute;\n  bottom: -15px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 320px;\n  height: 50px;\n  background: radial-gradient(\n    ellipse at center,\n    color-mix(in srgb, var(--dock-accent, #639aff) 14%, transparent) 0%,\n    transparent 75%\n  );\n  pointer-events: none;\n  filter: blur(12px);\n}\n\n/* ==========================================================\n   Dock Bar (Fixed 60px Height, Horizontal Dock Stretch)\n   ========================================================== */\n\n.dock {\n  display: inline-flex;\n  padding: 10px;\n  align-items: center;\n  gap: 15px;\n  height: 60px;\n  min-height: 60px;\n  max-height: 60px; /* Lock navbar dock height to 60px */\n  background-color: var(--dock-surface, #08101e);\n  border: 2px solid var(--dock-border, #142544);\n  border-radius: 20px;\n  position: relative;\n  overflow: visible; /* Elevated square buttons pop out top */\n  box-shadow:\n    0 8px 30px rgba(0, 0, 0, 0.18),\n    0 2px 6px rgba(0, 0, 0, 0.08);\n  box-sizing: border-box;\n}\n\n/* ==========================================================\n   Dock Item Container\n   ========================================================== */\n\n.dock-item-container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n/* ==========================================================\n   Dock Item Button (Strict 1:1 Aspect Ratio Square Buttons)\n   ========================================================== */\n\n.dock-item {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0;\n  aspect-ratio: 1 / 1; /* Guaranteed 1:1 square button */\n  background: var(--dock-surface, #08101e);\n  border: 2px solid var(--dock-item-border, #0c1f43);\n  border-radius: 14px;\n  cursor: pointer;\n  outline: none;\n  position: relative;\n  /* The ring is drawn only for keyboard focus, but it must exist: without it\n     tabbing through the dock gave no visible indicator at all, and the label\n     naming each icon-only button never appeared. */\n\n  transition: border-color 0.15s ease;\n  will-change: width, height, transform, box-shadow;\n  box-sizing: border-box;\n}\n\n.dock-item:focus-visible {\n  outline: 2px solid var(--dock-accent, #639aff);\n  outline-offset: 3px;\n}\n\n.dock-item--active {\n  border-color: var(--dock-accent, #639aff);\n}\n\n.dock-item svg {\n  width: 24px;\n  height: 24px;\n  display: block;\n  flex-shrink: 0;\n  stroke: var(--dock-icon, #ffffff);\n  color: var(--dock-icon, #ffffff);\n}\n\n/* ==========================================================\n   Figma Tooltip Label (Design 297:102)\n   ========================================================== */\n\n.dock-tooltip {\n  position: absolute;\n  bottom: calc(100% + 8px);\n  left: 50%;\n  white-space: nowrap;\n  padding: 3px 8px;\n  background: var(--dock-surface, #08101e);\n  border: 1px solid var(--dock-border, #142544);\n  border-radius: 6px;\n  font-family: 'Product Sans', 'Inter', -apple-system, sans-serif;\n  font-size: 11px;\n  line-height: 1.2;\n  font-weight: 500;\n  color: var(--dock-icon, #ffffff);\n  pointer-events: none;\n  z-index: 30;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);\n}\n","type":"registry:file","target":"components/dock/Dock.css"},{"path":"src/components/Dock/Dock.d.ts","content":"import type { ComponentPropsWithoutRef, ComponentType, ReactElement } from 'react'\n\nexport interface DockItem {\n  id: string\n  label: string\n  /** Rendered inside the button, given `size` and `color`. */\n  Icon: ComponentType<{ size?: number; color?: string }>\n}\n\nexport interface DockProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onSelect'> {\n  /** Defaults to the five-icon row the demos show. */\n  items?: DockItem[]\n  /** Controlled selection. Pass `null` for none. */\n  activeId?: string | null\n  /** Starting selection when uncontrolled. */\n  defaultActiveId?: string | null\n  /** Fires on every activation, including deselection, where `id` is null. */\n  onSelect?: (id: string | null, item: DockItem | null, index: number) => void\n\n  /** Item size at rest, in px. */\n  baseSize?: number\n  /** Item size directly under the pointer, in px. */\n  magnifiedSize?: number\n\n  /** Fill of the dock bar, of each item, and of the label that appears above one. */\n  surface?: string\n  /** Outline of the dock bar and of those labels. */\n  border?: string\n  /** Outline of an item at rest. */\n  itemBorder?: string\n  /** Outline of the selected item, the keyboard focus ring, and the soft glow under the bar. */\n  accent?: string\n  /** Colour of the icons and of the label text. */\n  icon?: string\n}\n\ndeclare const Dock: (props: DockProps) => ReactElement\nexport default Dock\n","type":"registry:lib","target":"components/dock/Dock.d.ts"}],"type":"registry:component"}