{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "BorderGlow-TS-TW", "title": "BorderGlow", "description": "Glowing mesh-gradient border that follows cursor direction and intensifies near edges.", "type": "registry:component", "files": [ { "type": "registry:component", "path": "BorderGlow/BorderGlow.tsx", "content": "import { useRef, useCallback, useState, useEffect, type ReactNode } from 'react';\n\ninterface BorderGlowProps {\n children?: ReactNode;\n className?: string;\n edgeSensitivity?: number;\n glowColor?: string;\n backgroundColor?: string;\n borderRadius?: number;\n glowRadius?: number;\n glowIntensity?: number;\n coneSpread?: number;\n animated?: boolean;\n colors?: string[];\n fillOpacity?: number;\n}\n\nfunction parseHSL(hslStr: string): { h: number; s: number; l: number } {\n const match = hslStr.match(/([\\d.]+)\\s*([\\d.]+)%?\\s*([\\d.]+)%?/);\n if (!match) return { h: 40, s: 80, l: 80 };\n return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };\n}\n\nfunction buildBoxShadow(glowColor: string, intensity: number): string {\n const { h, s, l } = parseHSL(glowColor);\n const base = `${h}deg ${s}% ${l}%`;\n const layers: [number, number, number, number, number, boolean][] = [\n [0, 0, 0, 1, 100, true], [0, 0, 1, 0, 60, true], [0, 0, 3, 0, 50, true],\n [0, 0, 6, 0, 40, true], [0, 0, 15, 0, 30, true], [0, 0, 25, 2, 20, true],\n [0, 0, 50, 2, 10, true],\n [0, 0, 1, 0, 60, false], [0, 0, 3, 0, 50, false], [0, 0, 6, 0, 40, false],\n [0, 0, 15, 0, 30, false], [0, 0, 25, 2, 20, false], [0, 0, 50, 2, 10, false],\n ];\n return layers.map(([x, y, blur, spread, alpha, inset]) => {\n const a = Math.min(alpha * intensity, 100);\n return `${inset ? 'inset ' : ''}${x}px ${y}px ${blur}px ${spread}px hsl(${base} / ${a}%)`;\n }).join(', ');\n}\n\nfunction easeOutCubic(x: number) { return 1 - Math.pow(1 - x, 3); }\nfunction easeInCubic(x: number) { return x * x * x; }\n\ninterface AnimateOpts {\n start?: number; end?: number; duration?: number; delay?: number;\n ease?: (t: number) => number; onUpdate: (v: number) => void; onEnd?: () => void;\n}\n\nfunction animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }: AnimateOpts) {\n const t0 = performance.now() + delay;\n function tick() {\n const elapsed = performance.now() - t0;\n const t = Math.min(elapsed / duration, 1);\n onUpdate(start + (end - start) * ease(t));\n if (t < 1) requestAnimationFrame(tick);\n else if (onEnd) onEnd();\n }\n setTimeout(() => requestAnimationFrame(tick), delay);\n}\n\nconst GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];\nconst COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];\n\nfunction buildMeshGradients(colors: string[]): string[] {\n const gradients: string[] = [];\n for (let i = 0; i < 7; i++) {\n const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];\n gradients.push(`radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`);\n }\n gradients.push(`linear-gradient(${colors[0]} 0 100%)`);\n return gradients;\n}\n\nconst BorderGlow: React.FC = ({\n children,\n className = '',\n edgeSensitivity = 30,\n glowColor = '40 80 80',\n backgroundColor = '#120F17',\n borderRadius = 28,\n glowRadius = 40,\n glowIntensity = 1.0,\n coneSpread = 25,\n animated = false,\n colors = ['#c084fc', '#f472b6', '#38bdf8'],\n fillOpacity = 0.5,\n}) => {\n const cardRef = useRef(null);\n const [isHovered, setIsHovered] = useState(false);\n const [cursorAngle, setCursorAngle] = useState(45);\n const [edgeProximity, setEdgeProximity] = useState(0);\n const [sweepActive, setSweepActive] = useState(false);\n\n const getCenterOfElement = useCallback((el: HTMLElement) => {\n const { width, height } = el.getBoundingClientRect();\n return [width / 2, height / 2];\n }, []);\n\n const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n let kx = Infinity;\n let ky = Infinity;\n if (dx !== 0) kx = cx / Math.abs(dx);\n if (dy !== 0) ky = cy / Math.abs(dy);\n return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);\n }, [getCenterOfElement]);\n\n const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n if (dx === 0 && dy === 0) return 0;\n const radians = Math.atan2(dy, dx);\n let degrees = radians * (180 / Math.PI) + 90;\n if (degrees < 0) degrees += 360;\n return degrees;\n }, [getCenterOfElement]);\n\n const handlePointerMove = useCallback((e: React.PointerEvent) => {\n const card = cardRef.current;\n if (!card) return;\n const rect = card.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n setEdgeProximity(getEdgeProximity(card, x, y));\n setCursorAngle(getCursorAngle(card, x, y));\n }, [getEdgeProximity, getCursorAngle]);\n\n useEffect(() => {\n if (!animated) return;\n const angleStart = 110;\n const angleEnd = 465;\n setSweepActive(true);\n setCursorAngle(angleStart);\n\n animateValue({ duration: 500, onUpdate: v => setEdgeProximity(v / 100) });\n animateValue({ ease: easeInCubic, duration: 1500, end: 50, onUpdate: v => {\n setCursorAngle((angleEnd - angleStart) * (v / 100) + angleStart);\n }});\n animateValue({ ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100, onUpdate: v => {\n setCursorAngle((angleEnd - angleStart) * (v / 100) + angleStart);\n }});\n animateValue({ ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,\n onUpdate: v => setEdgeProximity(v / 100),\n onEnd: () => setSweepActive(false),\n });\n }, [animated]);\n\n const colorSensitivity = edgeSensitivity + 20;\n const isVisible = isHovered || sweepActive;\n const borderOpacity = isVisible\n ? Math.max(0, (edgeProximity * 100 - colorSensitivity) / (100 - colorSensitivity))\n : 0;\n const glowOpacity = isVisible\n ? Math.max(0, (edgeProximity * 100 - edgeSensitivity) / (100 - edgeSensitivity))\n : 0;\n\n const meshGradients = buildMeshGradients(colors);\n const borderBg = meshGradients.map(g => `${g} border-box`);\n const fillBg = meshGradients.map(g => `${g} padding-box`);\n const angleDeg = `${cursorAngle.toFixed(3)}deg`;\n\n return (\n setIsHovered(true)}\n onPointerLeave={() => setIsHovered(false)}\n className={`relative grid isolate border border-white/15 ${className}`}\n style={{\n background: backgroundColor,\n borderRadius: `${borderRadius}px`,\n transform: 'translate3d(0, 0, 0.01px)',\n boxShadow: 'rgba(0,0,0,0.1) 0 1px 2px, rgba(0,0,0,0.1) 0 2px 4px, rgba(0,0,0,0.1) 0 4px 8px, rgba(0,0,0,0.1) 0 8px 16px, rgba(0,0,0,0.1) 0 16px 32px, rgba(0,0,0,0.1) 0 32px 64px',\n }}\n >\n {/* mesh gradient border */}\n \n\n {/* mesh gradient fill near edges */}\n \n\n {/* outer glow */}\n \n \n \n\n
\n {children}\n
\n \n );\n};\n\nexport default BorderGlow;\n" } ], "registryDependencies": [], "dependencies": [] }