"use client"; import "./Dock.css"; import { motion, MotionValue, useMotionValue, useSpring, useTransform, type SpringOptions, AnimatePresence, } from "framer-motion"; import React, { Children, cloneElement, useEffect, useMemo, useRef, useState, } from "react"; export type DockItemData = { icon: React.ReactNode; label: React.ReactNode; onClick: () => void; className?: string; }; export type DockProps = { items: DockItemData[]; className?: string; distance?: number; panelHeight?: number; baseItemSize?: number; dockHeight?: number; magnification?: number; spring?: SpringOptions; }; type DockItemProps = { className?: string; children: React.ReactNode; onClick?: () => void; mouseX: MotionValue; spring: SpringOptions; distance: number; baseItemSize: number; magnification: number; }; function DockItem({ children, className = "", onClick, mouseX, spring, distance, magnification, baseItemSize, }: DockItemProps) { const ref = useRef(null); const isHovered = useMotionValue(0); const mouseDistance = useTransform(mouseX, (val) => { const rect = ref.current?.getBoundingClientRect() ?? { x: 0, width: baseItemSize, }; return val - rect.x - baseItemSize / 2; }); const targetSize = useTransform( mouseDistance, [-distance, 0, distance], [baseItemSize, magnification, baseItemSize] ); const size = useSpring(targetSize, spring); return ( isHovered.set(1)} onHoverEnd={() => isHovered.set(0)} onFocus={() => isHovered.set(1)} onBlur={() => isHovered.set(0)} onClick={onClick} className={`dock-item ${className}`} tabIndex={0} role="button" aria-haspopup="true" > {Children.map(children, (child) => cloneElement(child as React.ReactElement, { }) )} ); } type DockLabelProps = { className?: string; children: React.ReactNode; }; function DockLabel({ children, className = "", ...rest }: DockLabelProps) { const { isHovered } = rest as { isHovered: MotionValue }; const [isVisible, setIsVisible] = useState(false); useEffect(() => { const unsubscribe = isHovered.on("change", (latest) => { setIsVisible(latest === 1); }); return () => unsubscribe(); }, [isHovered]); return ( {isVisible && ( {children} )} ); } type DockIconProps = { className?: string; children: React.ReactNode; }; function DockIcon({ children, className = "" }: DockIconProps) { return
{children}
; } export default function Dock({ items, className = "", spring = { mass: 0.1, stiffness: 150, damping: 12 }, magnification = 70, distance = 200, panelHeight = 68, dockHeight = 256, baseItemSize = 50, }: DockProps) { const mouseX = useMotionValue(Infinity); const isHovered = useMotionValue(0); const maxHeight = useMemo( () => Math.max(dockHeight, magnification + magnification / 2 + 4), [magnification, dockHeight] ); const heightRow = useTransform(isHovered, [0, 1], [panelHeight, maxHeight]); const height = useSpring(heightRow, spring); return ( { isHovered.set(1); mouseX.set(pageX); }} onMouseLeave={() => { isHovered.set(0); mouseX.set(Infinity); }} className={`dock-panel ${className}`} style={{ height: panelHeight }} role="toolbar" aria-label="Application dock" > {items.map((item, index) => ( {item.icon} {item.label} ))} ); }