diff --git a/frontend/components/ui/dock.tsx b/frontend/components/ui/dock.tsx new file mode 100644 index 0000000..1e744f2 --- /dev/null +++ b/frontend/components/ui/dock.tsx @@ -0,0 +1,155 @@ +"use client" + +import React, { PropsWithChildren, useRef } from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { + motion, + MotionValue, + useMotionValue, + useSpring, + useTransform, +} from "framer-motion" +import type { MotionProps } from "framer-motion" + +import { cn } from "@/lib/utils" + +export interface DockProps extends VariantProps { + className?: string + iconSize?: number + iconMagnification?: number + disableMagnification?: boolean + iconDistance?: number + direction?: "top" | "middle" | "bottom" + children: React.ReactNode +} + +const DEFAULT_SIZE = 40 +const DEFAULT_MAGNIFICATION = 60 +const DEFAULT_DISTANCE = 140 +const DEFAULT_DISABLEMAGNIFICATION = false + +const dockVariants = cva( + "supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md" +) + +const Dock = React.forwardRef( + ( + { + className, + children, + iconSize = DEFAULT_SIZE, + iconMagnification = DEFAULT_MAGNIFICATION, + disableMagnification = DEFAULT_DISABLEMAGNIFICATION, + iconDistance = DEFAULT_DISTANCE, + direction = "middle", + ...props + }, + ref + ) => { + const mouseX = useMotionValue(Infinity) + + const renderChildren = () => { + return React.Children.map(children, (child) => { + if ( + React.isValidElement(child) && + child.type === DockIcon + ) { + return React.cloneElement(child, { + ...child.props, + mouseX: mouseX, + size: iconSize, + magnification: iconMagnification, + disableMagnification: disableMagnification, + distance: iconDistance, + }) + } + return child + }) + } + + return ( + mouseX.set(e.pageX)} + onMouseLeave={() => mouseX.set(Infinity)} + {...props} + className={cn(dockVariants({ className }), { + "items-start": direction === "top", + "items-center": direction === "middle", + "items-end": direction === "bottom", + })} + > + {renderChildren()} + + ) + } +) + +Dock.displayName = "Dock" + +export interface DockIconProps extends Omit< + MotionProps & React.HTMLAttributes, + "children" +> { + size?: number + magnification?: number + disableMagnification?: boolean + distance?: number + mouseX?: MotionValue + className?: string + children?: React.ReactNode + props?: PropsWithChildren +} + +const DockIcon = ({ + size = DEFAULT_SIZE, + magnification = DEFAULT_MAGNIFICATION, + disableMagnification, + distance = DEFAULT_DISTANCE, + mouseX, + className, + children, + ...props +}: DockIconProps) => { + const ref = useRef(null) + const padding = Math.max(6, size * 0.2) + const defaultMouseX = useMotionValue(Infinity) + + const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => { + const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 } + return val - bounds.x - bounds.width / 2 + }) + + const targetSize = disableMagnification ? size : magnification + + const sizeTransform = useTransform( + distanceCalc, + [-distance, 0, distance], + [size, targetSize, size] + ) + + const scaleSize = useSpring(sizeTransform, { + mass: 0.1, + stiffness: 150, + damping: 12, + }) + + return ( + +
{children}
+
+ ) +} + +DockIcon.displayName = "DockIcon" + +export { Dock, DockIcon, dockVariants } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a8d384e..2db880e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "framer-motion": "^12.38.0", "jose": "^6.2.2", "lucide-react": "^1.8.0", + "motion": "^12.38.0", "next": "16.2.4", "next-auth": "^5.0.0-beta.31", "react": "19.2.4", @@ -6531,6 +6532,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", + "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", + "dependencies": { + "framer-motion": "^12.38.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/motion-dom": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 179c46d..017f66d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "framer-motion": "^12.38.0", "jose": "^6.2.2", "lucide-react": "^1.8.0", + "motion": "^12.38.0", "next": "16.2.4", "next-auth": "^5.0.0-beta.31", "react": "19.2.4",