import { type CSSProperties, forwardRef, useCallback, useEffect, useId, useRef, useState } from "react" import { ShaderDisplacementGenerator, fragmentShaders } from "./shader-utils" import { displacementMap, polarDisplacementMap, prominentDisplacementMap } from "./utils" // Generate shader-based displacement map using shaderUtils const generateShaderDisplacementMap = (width: number, height: number): string => { const generator = new ShaderDisplacementGenerator({ width, height, fragment: fragmentShaders.liquidGlass, }) const dataUrl = generator.updateShader() generator.destroy() return dataUrl } const getMap = (mode: "standard" | "polar" | "prominent" | "shader", shaderMapUrl?: string) => { switch (mode) { case "standard": return displacementMap case "polar": return polarDisplacementMap case "prominent": return prominentDisplacementMap case "shader": return shaderMapUrl || displacementMap default: throw new Error(`Invalid mode: ${mode}`) } } /* ---------- SVG filter (edge-only displacement) ---------- */ const GlassFilter: React.FC<{ id: string; displacementScale: number; aberrationIntensity: number; width: number; height: number; mode: "standard" | "polar" | "prominent" | "shader"; shaderMapUrl?: string }> = ({ id, displacementScale, aberrationIntensity, width, height, mode, shaderMapUrl, }) => ( ) /* ---------- container ---------- */ const GlassContainer = forwardRef< HTMLDivElement, React.PropsWithChildren<{ className?: string style?: React.CSSProperties displacementScale?: number blurAmount?: number saturation?: number aberrationIntensity?: number mouseOffset?: { x: number; y: number } onMouseLeave?: () => void onMouseEnter?: () => void onMouseDown?: () => void onMouseUp?: () => void active?: boolean overLight?: boolean cornerRadius?: number padding?: string glassSize?: { width: number; height: number } onClick?: () => void mode?: "standard" | "polar" | "prominent" | "shader" }> >( ( { children, className = "", style, displacementScale = 25, blurAmount = 12, saturation = 180, aberrationIntensity = 2, onMouseEnter, onMouseLeave, onMouseDown, onMouseUp, active = false, overLight = false, cornerRadius = 999, padding = "24px 32px", glassSize = { width: 270, height: 69 }, onClick, mode = "standard", }, ref, ) => { const filterId = useId() const [shaderMapUrl, setShaderMapUrl] = useState("") const isFirefox = navigator.userAgent.toLowerCase().includes("firefox") // Generate shader displacement map when in shader mode useEffect(() => { if (mode === "shader") { const url = generateShaderDisplacementMap(glassSize.width, glassSize.height) setShaderMapUrl(url) } }, [mode, glassSize.width, glassSize.height]) const backdropStyle = { filter: isFirefox ? null : `url(#${filterId})`, backdropFilter: `blur(${(overLight ? 12 : 4) + blurAmount * 32}px) saturate(${saturation}%)`, } return (
{/* backdrop layer that gets wiggly */} {/* user content stays sharp */}
{children}
) }, ) GlassContainer.displayName = "GlassContainer" interface LiquidGlassProps { children: React.ReactNode displacementScale?: number blurAmount?: number saturation?: number aberrationIntensity?: number elasticity?: number cornerRadius?: number globalMousePos?: { x: number; y: number } mouseOffset?: { x: number; y: number } mouseContainer?: React.RefObject | null className?: string padding?: string style?: React.CSSProperties overLight?: boolean mode?: "standard" | "polar" | "prominent" | "shader" onClick?: () => void } export default function LiquidGlass({ children, displacementScale = 70, blurAmount = 0.0625, saturation = 140, aberrationIntensity = 2, elasticity = 0.15, cornerRadius = 999, globalMousePos: externalGlobalMousePos, mouseOffset: externalMouseOffset, mouseContainer = null, className = "", padding = "24px 32px", overLight = false, style = {}, mode = "standard", onClick, }: LiquidGlassProps) { const glassRef = useRef(null) const [isHovered, setIsHovered] = useState(false) const [isActive, setIsActive] = useState(false) const [glassSize, setGlassSize] = useState({ width: 270, height: 69 }) const [internalGlobalMousePos, setInternalGlobalMousePos] = useState({ x: 0, y: 0 }) const [internalMouseOffset, setInternalMouseOffset] = useState({ x: 0, y: 0 }) // Use external mouse position if provided, otherwise use internal const globalMousePos = externalGlobalMousePos || internalGlobalMousePos const mouseOffset = externalMouseOffset || internalMouseOffset // Internal mouse tracking const handleMouseMove = useCallback( (e: MouseEvent) => { const container = mouseContainer?.current || glassRef.current if (!container) { return } const rect = container.getBoundingClientRect() const centerX = rect.left + rect.width / 2 const centerY = rect.top + rect.height / 2 setInternalMouseOffset({ x: ((e.clientX - centerX) / rect.width) * 100, y: ((e.clientY - centerY) / rect.height) * 100, }) setInternalGlobalMousePos({ x: e.clientX, y: e.clientY, }) }, [mouseContainer], ) // Set up mouse tracking if no external mouse position is provided useEffect(() => { if (externalGlobalMousePos && externalMouseOffset) { // External mouse tracking is provided, don't set up internal tracking return } const container = mouseContainer?.current || glassRef.current if (!container) { return } container.addEventListener("mousemove", handleMouseMove) return () => { container.removeEventListener("mousemove", handleMouseMove) } }, [handleMouseMove, mouseContainer, externalGlobalMousePos, externalMouseOffset]) // Calculate directional scaling based on mouse position const calculateDirectionalScale = useCallback(() => { if (!globalMousePos.x || !globalMousePos.y || !glassRef.current) { return "scale(1)" } const rect = glassRef.current.getBoundingClientRect() const pillCenterX = rect.left + rect.width / 2 const pillCenterY = rect.top + rect.height / 2 const pillWidth = glassSize.width const pillHeight = glassSize.height const deltaX = globalMousePos.x - pillCenterX const deltaY = globalMousePos.y - pillCenterY // Calculate distance from mouse to pill edges (not center) const edgeDistanceX = Math.max(0, Math.abs(deltaX) - pillWidth / 2) const edgeDistanceY = Math.max(0, Math.abs(deltaY) - pillHeight / 2) const edgeDistance = Math.sqrt(edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY) // Activation zone: 200px from edges const activationZone = 200 // If outside activation zone, no effect if (edgeDistance > activationZone) { return "scale(1)" } // Calculate fade-in factor (1 at edge, 0 at activation zone boundary) const fadeInFactor = 1 - edgeDistance / activationZone // Normalize the deltas for direction const centerDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) if (centerDistance === 0) { return "scale(1)" } const normalizedX = deltaX / centerDistance const normalizedY = deltaY / centerDistance // Calculate stretch factors with fade-in const stretchIntensity = Math.min(centerDistance / 300, 1) * elasticity * fadeInFactor // X-axis scaling: stretch horizontally when moving left/right, compress when moving up/down const scaleX = 1 + Math.abs(normalizedX) * stretchIntensity * 0.3 - Math.abs(normalizedY) * stretchIntensity * 0.15 // Y-axis scaling: stretch vertically when moving up/down, compress when moving left/right const scaleY = 1 + Math.abs(normalizedY) * stretchIntensity * 0.3 - Math.abs(normalizedX) * stretchIntensity * 0.15 return `scaleX(${Math.max(0.8, scaleX)}) scaleY(${Math.max(0.8, scaleY)})` }, [globalMousePos, elasticity, glassSize]) // Helper function to calculate fade-in factor based on distance from element edges const calculateFadeInFactor = useCallback(() => { if (!globalMousePos.x || !globalMousePos.y || !glassRef.current) { return 0 } const rect = glassRef.current.getBoundingClientRect() const pillCenterX = rect.left + rect.width / 2 const pillCenterY = rect.top + rect.height / 2 const pillWidth = glassSize.width const pillHeight = glassSize.height const edgeDistanceX = Math.max(0, Math.abs(globalMousePos.x - pillCenterX) - pillWidth / 2) const edgeDistanceY = Math.max(0, Math.abs(globalMousePos.y - pillCenterY) - pillHeight / 2) const edgeDistance = Math.sqrt(edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY) const activationZone = 200 return edgeDistance > activationZone ? 0 : 1 - edgeDistance / activationZone }, [globalMousePos, glassSize]) // Helper function to calculate elastic translation const calculateElasticTranslation = useCallback(() => { if (!glassRef.current) { return { x: 0, y: 0 } } const fadeInFactor = calculateFadeInFactor() const rect = glassRef.current.getBoundingClientRect() const pillCenterX = rect.left + rect.width / 2 const pillCenterY = rect.top + rect.height / 2 return { x: (globalMousePos.x - pillCenterX) * elasticity * 0.1 * fadeInFactor, y: (globalMousePos.y - pillCenterY) * elasticity * 0.1 * fadeInFactor, } }, [globalMousePos, elasticity, calculateFadeInFactor]) // Update glass size whenever component mounts or window resizes useEffect(() => { const updateGlassSize = () => { if (glassRef.current) { const rect = glassRef.current.getBoundingClientRect() setGlassSize({ width: rect.width, height: rect.height }) } } updateGlassSize() window.addEventListener("resize", updateGlassSize) return () => window.removeEventListener("resize", updateGlassSize) }, []) const transformStyle = `translate(calc(-50% + ${calculateElasticTranslation().x}px), calc(-50% + ${calculateElasticTranslation().y}px)) ${isActive && Boolean(onClick) ? "scale(0.96)" : calculateDirectionalScale()}` const baseStyle = { ...style, transform: transformStyle, transition: "all ease-out 0.2s", } const positionStyles = { position: baseStyle.position || "relative", top: baseStyle.top || "50%", left: baseStyle.left || "50%", } return ( <> {/* Over light effect */}
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onMouseDown={() => setIsActive(true)} onMouseUp={() => setIsActive(false)} active={isActive} overLight={overLight} onClick={onClick} mode={mode} > {children} {/* Border layer 1 - extracted from glass container */} {/* Border layer 2 - duplicate with mix-blend-overlay */} {/* Hover effects */} {Boolean(onClick) && ( <>
)} ) }