{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"glowing-effect","type":"registry:ui","title":"GlowingEffect","description":"Mouse-proximity based glowing border effect with animated conic gradient","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["effects"],"registryDependencies":["https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/glowing-effect/GlowingEffect.tsx","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties } from \"react\";\n\nimport { cn } from \"../../utils\";\n\nexport interface GlowingEffectProps {\n\t/** Blur amount for the glow. */\n\tblur?: number;\n\t/** Inner zone radius ratio where glow deactivates. */\n\tinactiveZone?: number;\n\t/** Extra proximity distance to activate glow. */\n\tproximity?: number;\n\t/** Spread angle of the conic gradient. */\n\tspread?: number;\n\t/** Color variant. */\n\tvariant?: \"default\" | \"white\";\n\t/** Force glow visible. */\n\tglow?: boolean;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n\t/** Disable mouse tracking. */\n\tdisabled?: boolean;\n\t/** Animation speed. */\n\tmovementDuration?: number;\n\t/** Border width in pixels. */\n\tborderWidth?: number;\n}\n\n/** Read live on every tween step; missing `matchMedia` means \"no preference\". */\nfunction prefersReducedMotion(): boolean {\n\treturn (\n\t\ttypeof window !== \"undefined\" &&\n\t\ttypeof window.matchMedia === \"function\" &&\n\t\twindow.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\t);\n}\n\ninterface MotionState {\n\tlastPosition: { x: number; y: number };\n\tanimationFrameId: number;\n\tangleAnimationFrameId: number;\n\tcurrentAngle: number;\n\ttargetAngle: number;\n\tanimating: boolean;\n}\n\nexport function GlowingEffect({\n\tblur = 0,\n\tinactiveZone = 0.7,\n\tproximity = 0,\n\tspread = 20,\n\tvariant = \"default\",\n\tglow = false,\n\tclassName = \"\",\n\tdisabled = true,\n\tmovementDuration = 2,\n\tborderWidth = 1,\n}: GlowingEffectProps) {\n\tconst containerRef = useRef<HTMLDivElement>(null);\n\tconst motionRef = useRef<MotionState>({\n\t\tlastPosition: { x: 0, y: 0 },\n\t\tanimationFrameId: 0,\n\t\tangleAnimationFrameId: 0,\n\t\tcurrentAngle: 0,\n\t\ttargetAngle: 0,\n\t\tanimating: false,\n\t});\n\t// Live mirror of the props the frame handlers read, so a mounted listener\n\t// always sees the latest values (the Svelte closures read reactive props).\n\tconst liveRef = useRef({ inactiveZone, proximity, movementDuration });\n\tuseEffect(() => {\n\t\tliveRef.current = { inactiveZone, proximity, movementDuration };\n\t});\n\t// `disabled` is read once at mount, mirroring the Svelte onMount: toggling\n\t// it later does not attach/detach listeners on either side.\n\tconst disabledAtMount = useRef(disabled);\n\n\tconst containerStyle = {\n\t\t\"--blur\": `${blur}px`,\n\t\t\"--spread\": spread,\n\t\t\"--start\": \"0\",\n\t\t\"--active\": \"0\",\n\t\t\"--glowingeffect-border-width\": `${borderWidth}px`,\n\t\t\"--repeating-conic-gradient-times\": \"5\",\n\t\t\"--gradient\":\n\t\t\tvariant === \"white\"\n\t\t\t\t? `repeating-conic-gradient(from 236.84deg at 50% 50%, var(--black), var(--black) calc(25% / var(--repeating-conic-gradient-times)))`\n\t\t\t\t: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%), radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%), radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%), radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%), repeating-conic-gradient(from 236.84deg at 50% 50%, #dd7bbb 0%, #d79f1e calc(25% / var(--repeating-conic-gradient-times)), #5a922c calc(50% / var(--repeating-conic-gradient-times)), #4c7894 calc(75% / var(--repeating-conic-gradient-times)), #dd7bbb calc(100% / var(--repeating-conic-gradient-times)))`,\n\t} as CSSProperties;\n\n\tuseEffect(() => {\n\t\tif (disabledAtMount.current) return;\n\n\t\tconst state = motionRef.current;\n\n\t\tfunction animateAngle() {\n\t\t\t// Reduced motion: the arc snaps to the target (a duration of 0). The\n\t\t\t// state is still produced, only the glide is dropped.\n\t\t\t// Otherwise lerp speed is inversely proportional to movementDuration.\n\t\t\tconst speed = prefersReducedMotion()\n\t\t\t\t? 1\n\t\t\t\t: 0.08 / Math.max(liveRef.current.movementDuration, 0.1);\n\t\t\tstate.currentAngle = state.currentAngle + (state.targetAngle - state.currentAngle) * speed;\n\n\t\t\tif (containerRef.current) {\n\t\t\t\tcontainerRef.current.style.setProperty(\"--start\", String(state.currentAngle));\n\t\t\t}\n\n\t\t\tif (Math.abs(state.targetAngle - state.currentAngle) > 0.1) {\n\t\t\t\tstate.angleAnimationFrameId = requestAnimationFrame(animateAngle);\n\t\t\t} else {\n\t\t\t\tstate.currentAngle = state.targetAngle;\n\t\t\t\tif (containerRef.current) {\n\t\t\t\t\tcontainerRef.current.style.setProperty(\"--start\", String(state.currentAngle));\n\t\t\t\t}\n\t\t\t\tstate.animating = false;\n\t\t\t}\n\t\t}\n\n\t\tfunction handleMove(e?: { x: number; y: number }) {\n\t\t\tif (!containerRef.current) return;\n\n\t\t\tif (state.animationFrameId) {\n\t\t\t\tcancelAnimationFrame(state.animationFrameId);\n\t\t\t}\n\n\t\t\tstate.animationFrameId = requestAnimationFrame(() => {\n\t\t\t\tconst container = containerRef.current;\n\t\t\t\tif (!container) return;\n\n\t\t\t\tconst { left, top, width, height } = container.getBoundingClientRect();\n\t\t\t\tconst mouseX = e?.x ?? state.lastPosition.x;\n\t\t\t\tconst mouseY = e?.y ?? state.lastPosition.y;\n\n\t\t\t\tif (e) {\n\t\t\t\t\tstate.lastPosition = { x: mouseX, y: mouseY };\n\t\t\t\t}\n\n\t\t\t\tconst center: [number, number] = [left + width * 0.5, top + height * 0.5];\n\t\t\t\tconst distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]);\n\t\t\t\tconst inactiveRadius = 0.5 * Math.min(width, height) * liveRef.current.inactiveZone;\n\n\t\t\t\tif (distanceFromCenter < inactiveRadius) {\n\t\t\t\t\tcontainer.style.setProperty(\"--active\", \"0\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst { proximity: liveProximity } = liveRef.current;\n\t\t\t\tconst isActive =\n\t\t\t\t\tmouseX > left - liveProximity &&\n\t\t\t\t\tmouseX < left + width + liveProximity &&\n\t\t\t\t\tmouseY > top - liveProximity &&\n\t\t\t\t\tmouseY < top + height + liveProximity;\n\n\t\t\t\tcontainer.style.setProperty(\"--active\", isActive ? \"1\" : \"0\");\n\n\t\t\t\tif (!isActive) return;\n\n\t\t\t\tconst rawTarget = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90;\n\n\t\t\t\t// Shortest-path angle difference\n\t\t\t\tconst angleDiff = ((rawTarget - state.currentAngle + 180) % 360) - 180;\n\t\t\t\tstate.targetAngle = state.currentAngle + angleDiff;\n\n\t\t\t\tif (!state.animating) {\n\t\t\t\t\tstate.animating = true;\n\t\t\t\t\tanimateAngle();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tfunction handlePointerMove(e: PointerEvent) {\n\t\t\thandleMove(e);\n\t\t}\n\n\t\tfunction handleScroll() {\n\t\t\thandleMove();\n\t\t}\n\n\t\twindow.addEventListener(\"scroll\", handleScroll, { passive: true });\n\t\tdocument.body.addEventListener(\"pointermove\", handlePointerMove, { passive: true });\n\n\t\treturn () => {\n\t\t\tif (state.animationFrameId) cancelAnimationFrame(state.animationFrameId);\n\t\t\tif (state.angleAnimationFrameId) cancelAnimationFrame(state.angleAnimationFrameId);\n\t\t\tstate.animating = false;\n\t\t\twindow.removeEventListener(\"scroll\", handleScroll);\n\t\t\tdocument.body.removeEventListener(\"pointermove\", handlePointerMove);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 motion-safe:transition-opacity\",\n\t\t\t\t\tglow && \"opacity-100\",\n\t\t\t\t\tvariant === \"white\" && \"border-white\",\n\t\t\t\t\tdisabled && \"!block\"\n\t\t\t\t)}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t></div>\n\t\t\t<div\n\t\t\t\tref={containerRef}\n\t\t\t\tstyle={containerStyle}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 motion-safe:transition-opacity\",\n\t\t\t\t\tglow && \"opacity-100\",\n\t\t\t\t\tblur > 0 && \"blur-[var(--blur)]\",\n\t\t\t\t\tclassName,\n\t\t\t\t\tdisabled && \"!hidden\"\n\t\t\t\t)}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"glow\",\n\t\t\t\t\t\t\"rounded-[inherit]\",\n\t\t\t\t\t\t\"after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))] after:rounded-[inherit] after:content-['']\",\n\t\t\t\t\t\t\"after:[border:var(--glowingeffect-border-width)_solid_transparent]\",\n\t\t\t\t\t\t\"after:[background-attachment:fixed] after:[background:var(--gradient)]\",\n\t\t\t\t\t\t\"after:opacity-[var(--active)] after:motion-safe:transition-opacity after:motion-safe:duration-300\",\n\t\t\t\t\t\t\"after:[mask-clip:padding-box,border-box]\",\n\t\t\t\t\t\t\"after:[mask-composite:intersect]\",\n\t\t\t\t\t\t\"after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]\"\n\t\t\t\t\t)}\n\t\t\t\t></div>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/glowing-effect/GlowingEffect.tsx"},{"path":"react/src/components/glowing-effect/index.ts","content":"\"use client\";\n\nexport { GlowingEffect } from \"./GlowingEffect\";\nexport type { GlowingEffectProps } from \"./GlowingEffect\";\n","type":"registry:ui","target":"@ui/fancy/components/glowing-effect/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/glowing-effect?framework=react"}}