{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-dock",
  "title": "Dock",
  "description": "A macOS-style dock of buttons or links that magnify near the pointer, with labels and separators.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "components/core/dock.tsx",
      "content": "'use client';\n\nimport {\n  motion,\n  MotionValue,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type SpringOptions,\n  AnimatePresence,\n  MotionConfig,\n} from 'motion/react';\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { cn } from '@/lib/utils';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\n\nconst DOCK_HEIGHT = 128;\nconst DEFAULT_MAGNIFICATION = 80;\nconst DEFAULT_DISTANCE = 150;\nconst DEFAULT_PANEL_HEIGHT = 64;\nconst DEFAULT_ITEM_SIZE = 40;\n\nexport type DockProps = {\n  children: React.ReactNode;\n  className?: string;\n  distance?: number;\n  panelHeight?: number;\n  magnification?: number;\n  itemSize?: number;\n  disableMagnification?: boolean;\n  spring?: SpringOptions;\n};\n\nexport type DockItemProps = {\n  className?: string;\n  children: React.ReactNode;\n  href?: string;\n  onClick?: () => void;\n};\n\nexport type DockSeparatorProps = {\n  className?: string;\n};\n\ntype DockChildProps = {\n  width?: MotionValue<number>;\n  isHovered?: MotionValue<number>;\n};\n\nexport type DockLabelProps = {\n  className?: string;\n  children: React.ReactNode;\n};\n\nexport type DockIconProps = {\n  className?: string;\n  children: React.ReactNode;\n};\n\nexport type DocContextType = {\n  mouseX: MotionValue;\n  spring: SpringOptions;\n  magnification: number;\n  distance: number;\n  itemSize?: number;\n};\n\nexport type DockProviderProps = {\n  children: React.ReactNode;\n  value: DocContextType;\n};\n\nconst DockContext = createContext<DocContextType | undefined>(undefined);\n\nfunction ReducedMotionConfig({ children }: { children: React.ReactNode }) {\n  const prefersReducedMotion = usePrefersReducedMotion();\n  if (!prefersReducedMotion) {\n    return children;\n  }\n  return <MotionConfig reducedMotion='always'>{children}</MotionConfig>;\n}\n\nfunction followerOf(\n  prefersReducedMotion: boolean | null,\n  target: MotionValue<number>,\n  spring: MotionValue<number>\n) {\n  if (prefersReducedMotion) {\n    return target;\n  }\n  return spring;\n}\n\nfunction peakWidthOf(\n  disableMagnification: boolean,\n  magnification: number,\n  itemSize: number\n) {\n  if (disableMagnification) {\n    return itemSize;\n  }\n  return magnification;\n}\n\nfunction isActivationKey(key: string) {\n  return key === 'Enter' || key === ' ';\n}\n\nfunction DockProvider({ children, value }: DockProviderProps) {\n  return <DockContext.Provider value={value}>{children}</DockContext.Provider>;\n}\n\nfunction useDock() {\n  const context = useContext(DockContext);\n  if (!context) {\n    throw new Error('useDock must be used within an DockProvider');\n  }\n  return context;\n}\n\nfunction Dock({\n  children,\n  className,\n  spring = { mass: 0.1, stiffness: 150, damping: 12 },\n  magnification = DEFAULT_MAGNIFICATION,\n  distance = DEFAULT_DISTANCE,\n  panelHeight = DEFAULT_PANEL_HEIGHT,\n  itemSize = DEFAULT_ITEM_SIZE,\n  disableMagnification = false,\n}: DockProps) {\n  const mouseX = useMotionValue(Infinity);\n  const isHovered = useMotionValue(0);\n  const prefersReducedMotion = usePrefersReducedMotion();\n  const peakWidth = peakWidthOf(disableMagnification, magnification, itemSize);\n\n  const maxHeight = useMemo(() => {\n    return Math.max(DOCK_HEIGHT, magnification + magnification / 2 + 4);\n  }, [magnification]);\n\n  const heightRow = useTransform(isHovered, [0, 1], [panelHeight, maxHeight]);\n  const heightSpring = useSpring(heightRow, spring);\n  const height = followerOf(prefersReducedMotion, heightRow, heightSpring);\n\n  return (\n    <ReducedMotionConfig>\n      <motion.div\n        style={{\n          height: height,\n          scrollbarWidth: 'none',\n        }}\n        className='mx-2 flex max-w-full items-end overflow-x-auto'\n      >\n        <motion.div\n          onMouseMove={({ clientX }) => {\n            isHovered.set(1);\n            mouseX.set(clientX);\n          }}\n          onMouseLeave={() => {\n            isHovered.set(0);\n            mouseX.set(Infinity);\n          }}\n          className={cn(\n            'bg-card hairline-ring ring-border mx-auto flex w-fit gap-4 rounded-2xl px-4 shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n            className\n          )}\n          style={{ height: panelHeight }}\n          role='toolbar'\n          aria-label='Application dock'\n        >\n          <DockProvider\n            value={{\n              mouseX,\n              spring,\n              distance,\n              magnification: peakWidth,\n              itemSize,\n            }}\n          >\n            {children}\n          </DockProvider>\n        </motion.div>\n      </motion.div>\n    </ReducedMotionConfig>\n  );\n}\n\nfunction DockItem({ children, className, href, onClick }: DockItemProps) {\n  const ref = useRef<HTMLElement>(null);\n\n  const {\n    distance,\n    magnification,\n    mouseX,\n    spring,\n    itemSize = DEFAULT_ITEM_SIZE,\n  } = useDock();\n  const prefersReducedMotion = usePrefersReducedMotion();\n\n  const isHovered = useMotionValue(0);\n\n  const mouseDistance = useTransform(mouseX, (val) => {\n    const domRect = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };\n    return val - domRect.x - domRect.width / 2;\n  });\n\n  const widthTransform = useTransform(\n    mouseDistance,\n    [-distance, 0, distance],\n    [itemSize, magnification, itemSize]\n  );\n\n  const widthSpring = useSpring(widthTransform, spring);\n  const width = followerOf(prefersReducedMotion, widthTransform, widthSpring);\n\n  const handleKeyDown = (event: React.KeyboardEvent) => {\n    if (!isActivationKey(event.key)) {\n      return;\n    }\n    event.preventDefault();\n    onClick?.();\n  };\n\n  const itemProps = {\n    style: { width },\n    onHoverStart: () => isHovered.set(1),\n    onHoverEnd: () => isHovered.set(0),\n    onFocus: () => isHovered.set(1),\n    onBlur: () => isHovered.set(0),\n    className: cn(\n      'relative inline-flex items-center justify-center',\n      className\n    ),\n  };\n\n  const content = Children.map(children, (child) =>\n    cloneElement(child as React.ReactElement<DockChildProps>, {\n      width,\n      isHovered,\n    })\n  );\n\n  if (href) {\n    return (\n      <motion.a\n        ref={ref as React.RefObject<HTMLAnchorElement | null>}\n        href={href}\n        onClick={onClick}\n        {...itemProps}\n      >\n        {content}\n      </motion.a>\n    );\n  }\n\n  return (\n    <motion.div\n      ref={ref as React.RefObject<HTMLDivElement | null>}\n      tabIndex={0}\n      role='button'\n      onClick={onClick}\n      onKeyDown={handleKeyDown}\n      {...itemProps}\n    >\n      {content}\n    </motion.div>\n  );\n}\n\nfunction DockLabel({ children, className, ...rest }: DockLabelProps) {\n  const restProps = rest as Record<string, unknown>;\n  const isHovered = restProps['isHovered'] as MotionValue<number>;\n  const [isVisible, setIsVisible] = useState(false);\n\n  useEffect(() => {\n    const unsubscribe = isHovered.on('change', (latest) => {\n      setIsVisible(latest === 1);\n    });\n\n    return () => unsubscribe();\n  }, [isHovered]);\n\n  return (\n    <>\n      <span className='sr-only'>{children}</span>\n      <AnimatePresence>\n        {isVisible && (\n          <motion.div\n            initial={{ opacity: 0, y: 0 }}\n            animate={{ opacity: 1, y: -10 }}\n            exit={{ opacity: 0, y: 0 }}\n            transition={{ duration: 0.2 }}\n            className={cn(\n              'surface-raised absolute -top-6 left-1/2 w-fit rounded-lg px-2 py-0.5 text-xs font-medium whitespace-pre',\n              className\n            )}\n            aria-hidden='true'\n            style={{ x: '-50%' }}\n          >\n            {children}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </>\n  );\n}\n\nfunction DockSeparator({ className }: DockSeparatorProps) {\n  return (\n    <div\n      role='separator'\n      aria-orientation='vertical'\n      className={cn(\n        'hairline-l border-border h-8 shrink-0 self-center',\n        className\n      )}\n    />\n  );\n}\n\nfunction DockIcon({ children, className, ...rest }: DockIconProps) {\n  const restProps = rest as Record<string, unknown>;\n  const width = restProps['width'] as MotionValue<number>;\n\n  const widthTransform = useTransform(width, (val) => val / 2);\n\n  return (\n    <motion.div\n      style={{ width: widthTransform }}\n      className={cn('flex items-center justify-center', className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport { Dock, DockIcon, DockItem, DockLabel, DockSeparator };\n",
      "type": "registry:component",
      "target": "components/core/dock.tsx"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:component"
}