frontend: add MagicUI dock component
- Install dock component via shadcn - Fix import path from motion/react to framer-motion
This commit is contained in:
155
frontend/components/ui/dock.tsx
Normal file
155
frontend/components/ui/dock.tsx
Normal file
@@ -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<typeof dockVariants> {
|
||||||
|
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<HTMLDivElement, DockProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
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<DockIconProps>(child) &&
|
||||||
|
child.type === DockIcon
|
||||||
|
) {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
...child.props,
|
||||||
|
mouseX: mouseX,
|
||||||
|
size: iconSize,
|
||||||
|
magnification: iconMagnification,
|
||||||
|
disableMagnification: disableMagnification,
|
||||||
|
distance: iconDistance,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return child
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
onMouseMove={(e) => 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()}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Dock.displayName = "Dock"
|
||||||
|
|
||||||
|
export interface DockIconProps extends Omit<
|
||||||
|
MotionProps & React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
"children"
|
||||||
|
> {
|
||||||
|
size?: number
|
||||||
|
magnification?: number
|
||||||
|
disableMagnification?: boolean
|
||||||
|
distance?: number
|
||||||
|
mouseX?: MotionValue<number>
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
style={{ width: scaleSize, height: scaleSize, padding }}
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-square cursor-pointer items-center justify-center rounded-full",
|
||||||
|
disableMagnification && "hover:bg-muted-foreground transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div>{children}</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DockIcon.displayName = "DockIcon"
|
||||||
|
|
||||||
|
export { Dock, DockIcon, dockVariants }
|
||||||
26
frontend/package-lock.json
generated
26
frontend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
"jose": "^6.2.2",
|
"jose": "^6.2.2",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
|
"motion": "^12.38.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"next-auth": "^5.0.0-beta.31",
|
"next-auth": "^5.0.0-beta.31",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
@@ -6531,6 +6532,31 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/motion-dom": {
|
||||||
"version": "12.38.0",
|
"version": "12.38.0",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
"jose": "^6.2.2",
|
"jose": "^6.2.2",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
|
"motion": "^12.38.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"next-auth": "^5.0.0-beta.31",
|
"next-auth": "^5.0.0-beta.31",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user