{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dock",
  "title": "Dock",
  "description": "Animated dock with Gaussian magnification, spring physics, separators, and tooltip carets.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/components/unlumen/dock/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type SpringOptions,\n  AnimatePresence,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface DockItem {\n  icon: React.ReactNode;\n  label: string;\n  /** if provided, item renders as an `<a>` */\n  href?: string;\n  onClick?: () => void;\n  /** renders a visual separator after this item */\n  separator?: boolean;\n}\n\nexport interface DockProps {\n  items: DockItem[];\n  /** @default 1.8 */\n  magnification?: number;\n  /** cursor radius (px) within which neighbors are magnified — @default 120 */\n  distance?: number;\n  /** @default 40 */\n  iconSize?: number;\n  /** @default 4 */\n  gap?: number;\n  /** @default 16 */\n  borderRadius?: number;\n  /** show labels permanently instead of on hover — @default false */\n  alwaysShowLabels?: boolean;\n  springOptions?: SpringOptions;\n  className?: string;\n}\n\nconst DEFAULT_SPRING: SpringOptions = {\n  stiffness: 400,\n  damping: 25,\n  mass: 0.4,\n};\n\nfunction DockSeparator() {\n  return (\n    <div className=\"mx-1 flex items-center self-stretch\">\n      <div className=\"h-6 w-px bg-foreground/10\" />\n    </div>\n  );\n}\n\nfunction DockIcon({\n  item,\n  mouseX,\n  magnification,\n  distance,\n  iconSize,\n  borderRadius,\n  alwaysShowLabels,\n  springOptions,\n  onHover,\n  iconRef: externalIconRef,\n}: {\n  item: DockItem;\n  mouseX: ReturnType<typeof useMotionValue<number>>;\n  magnification: number;\n  distance: number;\n  iconSize: number;\n  borderRadius: number;\n  alwaysShowLabels: boolean;\n  springOptions: SpringOptions;\n  onHover: (ref: React.RefObject<HTMLDivElement | null> | null) => void;\n  iconRef: React.RefObject<HTMLDivElement | null>;\n}) {\n  const wrapperRef = React.useRef<HTMLDivElement>(null);\n\n  const distanceFromMouse = useTransform(mouseX, (val) => {\n    const el = wrapperRef.current;\n    if (!el) return distance * 100;\n    const rect = el.getBoundingClientRect();\n    return Math.abs(val - (rect.left + rect.width / 2));\n  });\n\n  const gaussian = (d: number) =>\n    (magnification - 1) * Math.exp(-(d * d) / (2 * distance * distance)) + 1;\n\n  const widthRaw = useTransform(\n    distanceFromMouse,\n    (d) => iconSize * gaussian(d),\n  );\n  const heightRaw = useTransform(\n    distanceFromMouse,\n    (d) => iconSize * gaussian(d),\n  );\n\n  const width = useSpring(widthRaw, springOptions);\n  const height = useSpring(heightRaw, springOptions);\n\n  const Tag = item.href ? \"a\" : \"button\";\n\n  return (\n    // fixed height in-flow; width animates to push neighbors\n    <motion.div\n      ref={wrapperRef}\n      className=\"relative flex items-end justify-center\"\n      style={{ width, height: iconSize }}\n    >\n      {/* absolute, anchored bottom so icon grows upward */}\n      <motion.div\n        ref={externalIconRef}\n        style={{ width, height, bottom: 0 }}\n        className=\"absolute\"\n      >\n        <Tag\n          href={item.href}\n          onClick={item.onClick}\n          onMouseEnter={() => onHover(externalIconRef)}\n          onMouseLeave={() => onHover(null)}\n          aria-label={item.label}\n          style={{ borderRadius }}\n          className={cn(\n            \"flex h-full w-full items-center justify-center\",\n            \"text-foreground/70 transition-colors duration-150\",\n            \"hover:bg-foreground/[0.06] hover:text-foreground\",\n            \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/20\",\n            \"[&_svg]:size-[55%]\",\n          )}\n        >\n          {item.icon}\n        </Tag>\n      </motion.div>\n\n      {alwaysShowLabels && (\n        <span className=\"mt-0.5 text-[10px] font-medium tracking-tight text-foreground/40 whitespace-nowrap pointer-events-none select-none leading-none\">\n          {item.label}\n        </span>\n      )}\n    </motion.div>\n  );\n}\n\nexport function Dock({\n  items,\n  magnification = 1.8,\n  distance = 120,\n  iconSize = 40,\n  gap = 4,\n  borderRadius = 16,\n  alwaysShowLabels = false,\n  springOptions = DEFAULT_SPRING,\n  className,\n}: DockProps) {\n  const mouseX = useMotionValue(Infinity);\n  const dockRef = React.useRef<HTMLDivElement>(null);\n\n  const iconRefs = React.useRef<React.RefObject<HTMLDivElement | null>[]>(\n    items.map(() => React.createRef<HTMLDivElement>()),\n  );\n\n  const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null);\n  const [tooltipX, setTooltipX] = React.useState(0);\n  const [tooltipBottomOffset, setTooltipBottomOffset] = React.useState(0);\n\n  React.useEffect(() => {\n    if (hoveredIndex === null) return;\n\n    let raf: number;\n    const update = () => {\n      const iconEl = iconRefs.current[hoveredIndex]?.current;\n      const dockEl = dockRef.current;\n      if (iconEl && dockEl) {\n        const iconRect = iconEl.getBoundingClientRect();\n        const dockRect = dockEl.getBoundingClientRect();\n        setTooltipX(iconRect.left - dockRect.left + iconRect.width / 2);\n        setTooltipBottomOffset(dockRect.bottom - iconRect.top);\n      }\n      raf = requestAnimationFrame(update);\n    };\n    raf = requestAnimationFrame(update);\n    return () => cancelAnimationFrame(raf);\n  }, [hoveredIndex]);\n\n  const handleHover = React.useCallback(\n    (ref: React.RefObject<HTMLDivElement | null> | null) => {\n      if (ref === null) {\n        setHoveredIndex(null);\n        return;\n      }\n      const idx = iconRefs.current.findIndex((r) => r === ref);\n      setHoveredIndex(idx >= 0 ? idx : null);\n    },\n    [],\n  );\n\n  return (\n    <motion.div\n      ref={dockRef}\n      className={cn(\n        \"relative flex items-end overflow-visible border border-foreground/[0.08] bg-background/80 px-2 py-2 shadow-none hover:shadow-[0_0_0_1px_rgba(0,0,0,0.02),0_2px_8px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.06)] transition-shadow duration-200 backdrop-blur-xl\",\n        className,\n      )}\n      style={{ gap, borderRadius }}\n      onMouseMove={(e) => mouseX.set(e.clientX)}\n      onMouseLeave={() => mouseX.set(Infinity)}\n    >\n      {items.map((item, i) => (\n        <React.Fragment key={i}>\n          <DockIcon\n            item={item}\n            mouseX={mouseX}\n            magnification={magnification}\n            distance={distance}\n            iconSize={iconSize}\n            borderRadius={borderRadius}\n            alwaysShowLabels={alwaysShowLabels}\n            springOptions={springOptions}\n            onHover={handleHover}\n            iconRef={iconRefs.current[i]}\n          />\n          {item.separator && <DockSeparator />}\n        </React.Fragment>\n      ))}\n\n      {!alwaysShowLabels && (\n        <AnimatePresence>\n          {hoveredIndex !== null && (\n            <motion.div\n              key=\"dock-tooltip\"\n              layoutId=\"dock-tooltip\"\n              className=\"pointer-events-none absolute flex flex-col items-center z-50\"\n              style={{\n                left: tooltipX,\n                bottom: tooltipBottomOffset + 8,\n                x: \"-50%\",\n              }}\n              initial={{ opacity: 0, y: 6, scale: 0.94 }}\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              exit={{ opacity: 0, y: 6, scale: 0.94 }}\n              transition={{ duration: 0.13, ease: \"easeOut\" }}\n            >\n              <span className=\"rounded-md border border-foreground/10 bg-background px-2 py-1 text-sm font-medium text-foreground shadow-sm whitespace-nowrap\">\n                {items[hoveredIndex].label}\n              </span>\n              <svg\n                width=\"8\"\n                height=\"4\"\n                viewBox=\"0 0 8 4\"\n                className=\"-mt-px text-background\"\n                aria-hidden\n              >\n                <path d=\"M0 0L4 4L8 0\" fill=\"currentColor\" />\n              </svg>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      )}\n    </motion.div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/dock.tsx"
    }
  ],
  "type": "registry:ui"
}