{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A GlowHoverCards component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type CSSProperties,\n  cloneElement,\n  type ReactElement,\n  type Ref,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport interface GlowHoverTheme {\n  hue: number;\n  lightness: number;\n  saturation: number;\n}\n\nexport interface GlowHoverItem {\n  element: ReactElement;\n  id: string;\n  theme?: GlowHoverTheme;\n}\n\nexport interface GlowHoverProps {\n  className?: string;\n  glowIntensity?: number;\n  items: GlowHoverItem[];\n  maskSize?: number;\n}\n\n// Legacy types for backward compatibility\nexport type GlowHoverCardTheme = GlowHoverTheme;\nexport type GlowHoverCardItem = GlowHoverItem;\nexport type GlowHoverCardsProps = GlowHoverProps;\n\nexport default function GlowHover({\n  items,\n  className = \"\",\n  maskSize = 400,\n  glowIntensity = 0.15,\n}: GlowHoverProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const itemRefs = useRef<(HTMLElement | null)[]>([]);\n  const overlayItemRefs = useRef<(HTMLElement | null)[]>([]);\n  const [mousePosition, setMousePosition] = useState<{\n    x: number;\n    y: number;\n    opacity: number;\n  }>({ opacity: 0, x: 0, y: 0 });\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container || shouldReduceMotion) {\n      return;\n    }\n\n    const handlePointerMove = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      // Use clientX/clientY for viewport coordinates, then subtract container position\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n\n      setMousePosition({\n        opacity: 1,\n        x,\n        y,\n      });\n    };\n\n    const handlePointerLeave = () => {\n      setMousePosition((prev) => ({ ...prev, opacity: 0 }));\n    };\n\n    container.addEventListener(\"pointermove\", handlePointerMove);\n    container.addEventListener(\"pointerleave\", handlePointerLeave);\n\n    return () => {\n      container.removeEventListener(\"pointermove\", handlePointerMove);\n      container.removeEventListener(\"pointerleave\", handlePointerLeave);\n    };\n  }, [shouldReduceMotion]);\n\n  // Sync overlay card sizes and positions with original cards\n  useEffect(() => {\n    if (shouldReduceMotion || !overlayRef.current || !containerRef.current) {\n      return;\n    }\n\n    const syncCards = () => {\n      const container = containerRef.current;\n      const overlay = overlayRef.current;\n      if (!(container && overlay)) {\n        return;\n      }\n\n      itemRefs.current.forEach((itemEl, index) => {\n        const overlayItemEl = overlayItemRefs.current[index];\n        if (!(itemEl && overlayItemEl)) {\n          return;\n        }\n\n        const itemRect = itemEl.getBoundingClientRect();\n        const containerRect = container.getBoundingClientRect();\n\n        // Calculate position relative to container\n        const left = itemRect.left - containerRect.left;\n        const top = itemRect.top - containerRect.top;\n\n        overlayItemEl.style.position = \"absolute\";\n        overlayItemEl.style.left = `${left}px`;\n        overlayItemEl.style.top = `${top}px`;\n        overlayItemEl.style.width = `${itemRect.width}px`;\n        overlayItemEl.style.height = `${itemRect.height}px`;\n      });\n    };\n\n    const observers: ResizeObserver[] = [];\n    const mutationObserver = new MutationObserver(syncCards);\n\n    // Sync on resize\n    for (const itemEl of itemRefs.current) {\n      if (!itemEl) {\n        continue;\n      }\n\n      const observer = new ResizeObserver(() => {\n        syncCards();\n      });\n\n      observer.observe(itemEl);\n      observers.push(observer);\n    }\n\n    // Sync on DOM mutations\n    if (containerRef.current) {\n      mutationObserver.observe(containerRef.current, {\n        attributes: true,\n        childList: true,\n        subtree: true,\n      });\n    }\n\n    // Initial sync\n    syncCards();\n\n    // Sync on scroll and resize\n    window.addEventListener(\"scroll\", syncCards, { passive: true });\n    window.addEventListener(\"resize\", syncCards);\n\n    return () => {\n      for (const observer of observers) {\n        observer.disconnect();\n      }\n      mutationObserver.disconnect();\n      window.removeEventListener(\"scroll\", syncCards);\n      window.removeEventListener(\"resize\", syncCards);\n    };\n  }, [shouldReduceMotion]);\n\n  // Apply glow effect styles to an element\n  const applyGlowStyles = (\n    element: ReactElement,\n    theme?: GlowHoverTheme,\n    isOverlay = false\n  ): ReactElement => {\n    if (!isOverlay) {\n      return element;\n    }\n\n    const props = element.props as {\n      style?: CSSProperties;\n      className?: string;\n    };\n    const existingStyle = props.style || {};\n    const existingClassName = props.className || \"\";\n\n    let glowStyles: CSSProperties;\n\n    if (theme) {\n      // Use theme HSL colors\n      const hsl = `${theme.hue}, ${theme.saturation}%, ${theme.lightness}%`;\n      glowStyles = {\n        backgroundColor: `hsla(${hsl}, ${glowIntensity})`,\n        borderColor: `hsla(${hsl}, 1)`,\n        boxShadow: `0 0 0 1px inset hsl(${hsl}), 0 0 20px hsla(${hsl}, ${glowIntensity})`,\n      };\n    } else {\n      // Use brand color from CSS variable (OKLCH format supports / opacity)\n      const brandColor = \"var(--color-brand)\";\n      // OKLCH format: oklch(L C H / opacity)\n      const brandWithOpacity = `color-mix(in oklch, ${brandColor}, transparent ${(1 - glowIntensity) * 100}%)`;\n      glowStyles = {\n        backgroundColor: brandWithOpacity,\n        borderColor: brandColor,\n        boxShadow: `0 0 0 1px inset ${brandColor}, 0 0 20px ${brandWithOpacity}`,\n      };\n    }\n\n    // Merge with existing styles\n    const mergedStyle = {\n      ...existingStyle,\n      ...glowStyles,\n    };\n\n    return cloneElement(element, {\n      ...props,\n      className: cn(existingClassName, \"glow-overlay-item\"),\n      style: mergedStyle,\n      // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n    } as any);\n  };\n\n  return (\n    <div\n      className={cn(\"relative\", className)}\n      ref={containerRef}\n      style={shouldReduceMotion ? undefined : { willChange: \"contents\" }}\n    >\n      {/* Original Items */}\n      <div className=\"contents\">\n        {items.map((item, index) =>\n          cloneElement(item.element, {\n            key: item.id,\n            ref: (el: HTMLElement | null) => {\n              itemRefs.current[index] = el;\n              // Preserve existing ref if any\n              const elementProps = item.element.props as {\n                ref?: Ref<HTMLElement>;\n              };\n              const existingRef = elementProps?.ref;\n              if (typeof existingRef === \"function\") {\n                existingRef(el);\n              } else if (existingRef && typeof existingRef === \"object\") {\n                (existingRef as { current: HTMLElement | null }).current = el;\n              }\n            },\n            // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n          } as any)\n        )}\n      </div>\n\n      {/* Overlay with Glow Effect */}\n      {!shouldReduceMotion && (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 select-none\"\n          ref={overlayRef}\n          style={{\n            maskImage: `radial-gradient(${maskSize}px ${maskSize}px at ${mousePosition.x}px ${mousePosition.y}px, #000 1%, transparent 50%)`,\n            opacity: mousePosition.opacity,\n            transition:\n              \"opacity 200ms ease, mask-image 200ms ease, -webkit-mask-image 200ms ease\",\n            WebkitMaskImage: `radial-gradient(${maskSize}px ${maskSize}px at ${mousePosition.x}px ${mousePosition.y}px, #000 1%, transparent 50%)`,\n            willChange: \"mask-image, opacity\",\n          }}\n        >\n          {items.map((item, index) => {\n            const glowElement = applyGlowStyles(item.element, item.theme, true);\n            return cloneElement(glowElement, {\n              key: item.id,\n              ref: (el: HTMLElement | null) => {\n                overlayItemRefs.current[index] = el;\n              },\n              // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n            } as any);\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n\n// Legacy export for backward compatibility\nexport function GlowHoverCards(props: GlowHoverCardsProps) {\n  return <GlowHover {...props} />;\n}\n","path":"index.tsx","target":"components/smoothui/glow-hover-card/index.tsx","type":"registry:ui"}],"name":"glow-hover-card","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Glow Hover Card","type":"registry:ui"}