Fortura/apps/www/public/r/spring-element.json
2025-08-20 04:12:49 -06:00

19 lines
6.2 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "spring-element",
"type": "registry:ui",
"title": "Spring Element",
"description": "A flexible, animated spring component that attaches a draggable element (avatar, text, icon, or any React node) to its origin with a spring line.",
"dependencies": [
"motion"
],
"registryDependencies": [],
"files": [
{
"path": "registry/components/spring-element/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport {\n type HTMLMotionProps,\n motion,\n useMotionValue,\n useSpring,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nconst generateSpringPath = (\n x1: number,\n y1: number,\n x2: number,\n y2: number,\n springConfig: {\n coilCount?: number;\n amplitudeMin?: number;\n amplitudeMax?: number;\n curveRatioMin?: number;\n curveRatioMax?: number;\n bezierOffset?: number;\n } = {},\n) => {\n const {\n coilCount = 8,\n amplitudeMin = 8,\n amplitudeMax = 20,\n curveRatioMin = 0.5,\n curveRatioMax = 1,\n bezierOffset = 8,\n } = springConfig;\n\n const dx = x2 - x1;\n const dy = y2 - y1;\n const dist = Math.sqrt(dx * dx + dy * dy);\n if (dist < 2) return `M${x1},${y1}`;\n const d = dist / coilCount;\n const h = Math.max(0.8, 1 - (dist - 40) / 200);\n const amplitude = Math.max(\n amplitudeMin,\n Math.min(amplitudeMax, amplitudeMax * h),\n );\n const curveRatio =\n dist <= 40\n ? curveRatioMax\n : dist <= 120\n ? curveRatioMax - ((dist - 40) / 80) * (curveRatioMax - curveRatioMin)\n : curveRatioMin;\n const ux = dx / dist,\n uy = dy / dist;\n const perpX = -uy,\n perpY = ux;\n\n let path = [];\n for (let i = 0; i < coilCount; i++) {\n const sx = x1 + ux * (i * d);\n const sy = y1 + uy * (i * d);\n const ex = x1 + ux * ((i + 1) * d);\n const ey = y1 + uy * ((i + 1) * d);\n\n const mx = x1 + ux * ((i + 0.5) * d) + perpX * amplitude;\n const my = y1 + uy * ((i + 0.5) * d) + perpY * amplitude;\n\n const c1x = sx + d * curveRatio * ux;\n const c1y = sy + d * curveRatio * uy;\n const c2x = mx + ux * bezierOffset;\n const c2y = my + uy * bezierOffset;\n const c3x = mx - ux * bezierOffset;\n const c3y = my - uy * bezierOffset;\n const c4x = ex - d * curveRatio * ux;\n const c4y = ey - d * curveRatio * uy;\n\n if (i === 0) path.push(`M${sx},${sy}`);\n else path.push(`L${sx},${sy}`);\n path.push(`C${c1x},${c1y} ${c2x},${c2y} ${mx},${my}`);\n path.push(`C${c3x},${c3y} ${c4x},${c4y} ${ex},${ey}`);\n }\n return path.join(' ');\n};\n\nfunction useMotionValueValue(mv: any) {\n return React.useSyncExternalStore(\n (callback) => {\n const unsub = mv.on('change', callback);\n return unsub;\n },\n () => mv.get(),\n () => mv.get(),\n );\n}\n\ntype SpringAvatarProps = {\n children: React.ReactElement;\n className?: string;\n springClassName?: string;\n dragElastic?: number;\n springConfig?: { stiffness?: number; damping?: number };\n springPathConfig?: {\n coilCount?: number;\n amplitudeMin?: number;\n amplitudeMax?: number;\n curveRatioMin?: number;\n curveRatioMax?: number;\n bezierOffset?: number;\n };\n} & HTMLMotionProps<'div'>;\n\nfunction SpringElement({\n ref,\n children,\n className,\n springClassName,\n dragElastic = 0.2,\n springConfig = { stiffness: 200, damping: 16 },\n springPathConfig = {},\n ...props\n}: SpringAvatarProps) {\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n\n const springX = useSpring(x, {\n stiffness: springConfig.stiffness,\n damping: springConfig.damping,\n });\n const springY = useSpring(y, {\n stiffness: springConfig.stiffness,\n damping: springConfig.damping,\n });\n\n const sx = useMotionValueValue(springX);\n const sy = useMotionValueValue(springY);\n\n const childRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(ref, () => childRef.current as HTMLDivElement);\n const [center, setCenter] = React.useState({ x: 0, y: 0 });\n const [isDragging, setIsDragging] = React.useState(false);\n\n React.useLayoutEffect(() => {\n function update() {\n if (childRef.current) {\n const rect = childRef.current.getBoundingClientRect();\n setCenter({\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height / 2,\n });\n }\n }\n update();\n window.addEventListener('resize', update);\n window.addEventListener('scroll', update, true);\n return () => {\n window.removeEventListener('resize', update);\n window.removeEventListener('scroll', update, true);\n };\n }, []);\n\n React.useEffect(() => {\n if (isDragging) {\n document.body.style.cursor = 'grabbing';\n } else {\n document.body.style.cursor = 'default';\n }\n }, [isDragging]);\n\n const path = generateSpringPath(\n center.x,\n center.y,\n center.x + sx,\n center.y + sy,\n springPathConfig,\n );\n\n return (\n <>\n <svg\n width=\"100vw\"\n height=\"100vh\"\n className=\"fixed inset-0 w-screen h-screen pointer-events-none z-40 inset-0\"\n >\n <path\n d={path}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={cn(\n 'stroke-2 stroke-neutral-900 dark:stroke-neutral-100 fill-none',\n springClassName,\n )}\n />\n </svg>\n <motion.div\n ref={childRef}\n className={cn(\n 'z-50',\n isDragging ? 'cursor-grabbing' : 'cursor-grab',\n className,\n )}\n style={{\n x: springX,\n y: springY,\n }}\n drag\n dragElastic={dragElastic}\n dragMomentum={false}\n onDragStart={() => {\n setIsDragging(true);\n }}\n onDrag={(_, info) => {\n x.set(info.offset.x);\n y.set(info.offset.y);\n }}\n onDragEnd={() => {\n x.set(0);\n y.set(0);\n setIsDragging(false);\n }}\n {...props}\n >\n {children}\n </motion.div>\n </>\n );\n}\n\nexport { SpringElement };\n",
"type": "registry:ui",
"target": "components/animate-ui/components/spring-element.tsx"
}
]
}