frontend: add MagicUI dock component

- Install dock component via shadcn
- Fix import path from motion/react to framer-motion
This commit is contained in:
2026-04-16 16:55:11 +00:00
parent 300039b14e
commit baf2b26de0
3 changed files with 182 additions and 0 deletions

View 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 }

View File

@@ -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",

View File

@@ -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",