"use client"; import { motion, useMotionValue, useSpring, useTransform, AnimatePresence, MotionValue, } from "framer-motion"; import { useEffect, useMemo, useRef, useState } from "react"; function useDockItemSize( mouseX: MotionValue, baseItemSize: number, magnification: number, distance: number, ref: React.RefObject, spring: { mass: number; stiffness: number; damping: number } ) { const mouseDistance = useTransform(mouseX, (val) => { if (typeof val !== "number" || isNaN(val)) return 0; 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] ); return useSpring(targetSize, spring); } interface DockItemProps { icon: React.ReactNode; label: string; onClick: () => void; mouseX: MotionValue; baseItemSize: number; magnification: number; distance: number; spring: { mass: number; stiffness: number; damping: number }; badgeCount?: number; } function DockItem({ icon, label, onClick, mouseX, baseItemSize, magnification, distance, spring, badgeCount, }: DockItemProps) { const ref = useRef(null); const isHovered = useMotionValue(0); const size = useDockItemSize( mouseX, baseItemSize, magnification, distance, ref, spring ); const [showLabel, setShowLabel] = useState(false); useEffect(() => { const unsubscribe = isHovered.on("change", (value) => setShowLabel(value === 1) ); return () => unsubscribe(); }, [isHovered]); return ( isHovered.set(1)} onHoverEnd={() => isHovered.set(0)} onFocus={() => isHovered.set(1)} onBlur={() => isHovered.set(0)} onClick={onClick} className="relative inline-flex items-center justify-center rounded-full bg-background shadow-md " tabIndex={0} role="button" aria-haspopup="true" >
{icon}
{badgeCount !== undefined && badgeCount > 0 && ( {badgeCount > 99 ? "99+" : badgeCount} )} {showLabel && ( {label} )}
); } interface DockItem { icon: React.ReactNode; label: string; onClick: () => void; badgeCount?: number; } interface DockProps { items: DockItem[]; className?: string; spring?: { mass: number; stiffness: number; damping: number }; magnification?: number; distance?: number; panelHeight?: number; dockHeight?: number; baseItemSize?: number; position?: "bottom" | "top"; } export default function Dock({ items, className = "", spring = { mass: 0.1, stiffness: 150, damping: 12 }, magnification = 70, distance = 200, panelHeight = 64, 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 animatedHeight = useSpring( useTransform(isHovered, [0, 1], [panelHeight, maxHeight]), spring ); return ( { isHovered.set(1); mouseX.set(pageX); }} onMouseLeave={() => { isHovered.set(0); mouseX.set(Infinity); }} className={`absolute bottom-2 left-1/2 -translate-x-1/2 transform flex items-end gap-4 w-fit rounded-2xl border-2 border px-4 pb-2 ${className}`} style={{ height: panelHeight }} role="toolbar" aria-label="Application dock" > {items.map((item, index) => ( ))} ); }