{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A GlassCard component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion, useSpring } from \"motion/react\";\nimport {\n  type CSSProperties,\n  type ReactNode,\n  type PointerEvent as ReactPointerEvent,\n  useCallback,\n  useEffect,\n  useId,\n  useState,\n} from \"react\";\n\nconst DEFAULT_BLUR = 11;\nconst DEFAULT_RADIUS = 26;\nconst DEFAULT_REFRACTION = 24;\nconst DEFAULT_RIM_WIDTH = 12;\nconst DEFAULT_TINT = \"oklch(1 0 0 / 0.1)\";\n\nconst HOVER_QUERY = \"(hover: hover) and (pointer: fine)\";\nconst SPECULAR_SIZE = 260;\nconst SPECULAR_BASE_X = 0.3;\nconst SPECULAR_BASE_Y = 0.28;\nconst SWEEP_WIDTH = 42;\nconst RIM_SHIFT = 6;\nconst CONTENT_PADDING_MIN = 20;\nconst CONTENT_PADDING_GAP = 10;\nconst INNER_RADIUS_MIN = 6;\n/**\n * The rim stays nearly sharp. Blur it much past a pixel and the displacement\n * is smeared away — the bend has to read as a bend, not as more frost.\n */\nconst RIM_BLUR_RATIO = 0.12;\n\n/** Critically damped: light on glass should settle, never wobble. */\nconst SPRING = { damping: 34, mass: 0.5, stiffness: 250 } as const;\nconst LIFT = { scale: 1.006, y: -3 };\n\nconst SPECULAR_GRADIENT =\n  \"radial-gradient(closest-side, oklch(1 0 0 / 0.3), oklch(1 0 0 / 0.07) 48%, transparent 74%)\";\nconst SWEEP_GRADIENT =\n  \"linear-gradient(to bottom, oklch(1 0 0 / 0.92), oklch(1 0 0 / 0.22) 55%, transparent)\";\nconst SWEEP_MASK =\n  \"linear-gradient(90deg, transparent, oklch(0 0 0) 42%, oklch(0 0 0) 58%, transparent)\";\n/** Cyan on one edge, magenta on the other: glass splits light as it bends it. */\nconst CHROMATIC_RIM =\n  \"linear-gradient(125deg, oklch(0.84 0.12 215 / 0.4) 0%, transparent 36%, transparent 64%, oklch(0.78 0.15 340 / 0.34) 100%)\";\n/** Polished inner edge, so the rim reads as one thick pane, not a nested box. */\nconst BODY_EDGE =\n  \"inset 0 1px 0 oklch(1 0 0 / 0.22), inset 0 0 0 1px oklch(1 0 0 / 0.07)\";\nconst BOTTOM_SHADE =\n  \"linear-gradient(to bottom, transparent 48%, oklch(0 0 0 / 0.2) 100%)\";\nconst INNER_RING =\n  \"inset 0 1px 0 oklch(1 0 0 / 0.55), inset 0 0 0 1px oklch(1 0 0 / 0.15), inset 0 -1px 0 oklch(1 0 0 / 0.12)\";\nconst DROP_SHADOW =\n  \"0 26px 60px -26px oklch(0 0 0 / 0.6), 0 10px 26px -18px oklch(0 0 0 / 0.45)\";\n\n/**\n * Clips a layer to a ring of `width` px along the border box, so the\n * refractive edge only ever samples the backdrop at the rim.\n */\nconst ringMask = (width: number): CSSProperties => ({\n  boxSizing: \"border-box\",\n  maskClip: \"content-box, border-box\",\n  maskComposite: \"exclude\",\n  maskImage:\n    \"linear-gradient(oklch(0 0 0), oklch(0 0 0)), linear-gradient(oklch(0 0 0), oklch(0 0 0))\",\n  padding: width,\n  WebkitMaskClip: \"content-box, border-box\",\n  WebkitMaskComposite: \"xor\",\n  WebkitMaskImage:\n    \"linear-gradient(oklch(0 0 0), oklch(0 0 0)), linear-gradient(oklch(0 0 0), oklch(0 0 0))\",\n});\n\nexport interface GlassCardProps {\n  /** Backdrop blur strength of the pane body, in px. */\n  blur?: number;\n  /** Draws the inset highlight line that reads as the polished inner edge. */\n  border?: boolean;\n  children: ReactNode;\n  className?: string;\n  /** Tracks the pointer for the specular sweep and adds a subtle hover lift. */\n  interactive?: boolean;\n  /** Corner radius in px. */\n  radius?: number;\n  /**\n   * Displacement strength of the refractive rim, in px. `0` keeps the rim but\n   * removes the bend, which is the honest before/after comparison.\n   */\n  refraction?: number;\n  /** Thickness of the refractive rim, in px. Thin rims read as a plain border. */\n  rimWidth?: number;\n  /** Casts a layered drop shadow so the pane floats above its backdrop. */\n  shadow?: boolean;\n  /** Renders the pointer-tracked specular highlight and top-edge sweep. */\n  specular?: boolean;\n  /** Frosted body tint (any valid CSS color). */\n  tint?: string;\n}\n\nexport default function GlassCard({\n  blur = DEFAULT_BLUR,\n  border = true,\n  children,\n  className,\n  interactive = true,\n  radius = DEFAULT_RADIUS,\n  refraction = DEFAULT_REFRACTION,\n  rimWidth = DEFAULT_RIM_WIDTH,\n  shadow = true,\n  specular = true,\n  tint = DEFAULT_TINT,\n}: GlassCardProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const rawId = useId();\n  const filterId = `glass-refraction-${rawId.replace(/:/g, \"\")}`;\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const [canDisplace, setCanDisplace] = useState(false);\n\n  const specularX = useSpring(0, SPRING);\n  const specularY = useSpring(0, SPRING);\n  const sweepX = useSpring(0, SPRING);\n  const rimX = useSpring(0, SPRING);\n  const rimY = useSpring(0, SPRING);\n\n  const tracksPointer = interactive && isHoverDevice && !shouldReduceMotion;\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) {\n      return;\n    }\n    const query = window.matchMedia(HOVER_QUERY);\n    setIsHoverDevice(query.matches);\n    const onChange = (event: MediaQueryListEvent) =>\n      setIsHoverDevice(event.matches);\n    query.addEventListener(\"change\", onChange);\n    return () => query.removeEventListener(\"change\", onChange);\n  }, []);\n\n  // Only Chromium composites an SVG displacement inside `backdrop-filter`.\n  // Everywhere else the rim keeps its blur and drops the bend instead of\n  // invalidating the whole declaration.\n  useEffect(() => {\n    if (typeof CSS === \"undefined\" || typeof CSS.supports !== \"function\") {\n      return;\n    }\n    const probe = `url(#${filterId}) blur(2px)`;\n    setCanDisplace(\n      CSS.supports(\"backdrop-filter\", probe) ||\n        CSS.supports(\"-webkit-backdrop-filter\", probe)\n    );\n  }, [filterId]);\n\n  const handlePointerMove = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      const rect = event.currentTarget.getBoundingClientRect();\n      const pointerX = event.clientX - rect.left;\n      const pointerY = event.clientY - rect.top;\n      specularX.set(pointerX - rect.width * SPECULAR_BASE_X);\n      specularY.set(pointerY - rect.height * SPECULAR_BASE_Y);\n      sweepX.set(pointerX - rect.width / 2);\n      rimX.set((pointerX / rect.width - 0.5) * RIM_SHIFT);\n      rimY.set((pointerY / rect.height - 0.5) * RIM_SHIFT);\n    },\n    [rimX, rimY, specularX, specularY, sweepX]\n  );\n\n  const handlePointerLeave = useCallback(() => {\n    for (const value of [specularX, specularY, sweepX, rimX, rimY]) {\n      value.set(0);\n    }\n  }, [rimX, rimY, specularX, specularY, sweepX]);\n\n  const bodyFilters = `blur(${blur}px) saturate(160%) brightness(1.02)`;\n  const rimBlur = Math.max(1, Math.round(blur * RIM_BLUR_RATIO));\n  const rimFilters = `blur(${rimBlur}px) saturate(172%) brightness(1.07)`;\n  const displaces = refraction > 0 && canDisplace;\n  const rimBackdrop = displaces\n    ? `url(#${filterId}) ${rimFilters}`\n    : rimFilters;\n  const contentPadding = Math.max(\n    CONTENT_PADDING_MIN,\n    rimWidth + CONTENT_PADDING_GAP\n  );\n\n  return (\n    <motion.div\n      className={cn(\"relative overflow-hidden\", className)}\n      onPointerLeave={tracksPointer ? handlePointerLeave : undefined}\n      onPointerMove={tracksPointer ? handlePointerMove : undefined}\n      style={{\n        borderRadius: radius,\n        boxShadow: shadow ? DROP_SHADOW : undefined,\n      }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : { bounce: 0, duration: 0.3, type: \"spring\" }\n      }\n      whileHover={tracksPointer ? LIFT : undefined}\n    >\n      {displaces ? (\n        <svg\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute h-0 w-0\"\n          focusable=\"false\"\n        >\n          <title>Refractive glass edge</title>\n          <defs>\n            <filter id={filterId}>\n              <feTurbulence\n                baseFrequency=\"0.016 0.024\"\n                numOctaves={2}\n                result=\"noise\"\n                seed={7}\n                type=\"fractalNoise\"\n              />\n              <feGaussianBlur in=\"noise\" result=\"soft\" stdDeviation={1.4} />\n              <feDisplacementMap\n                in=\"SourceGraphic\"\n                in2=\"soft\"\n                scale={refraction}\n                xChannelSelector=\"R\"\n                yChannelSelector=\"G\"\n              />\n            </filter>\n          </defs>\n        </svg>\n      ) : null}\n\n      {/* Frosted body, inset so the rim below samples the raw backdrop. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute\"\n        style={{\n          backdropFilter: bodyFilters,\n          backgroundColor: tint,\n          borderRadius: Math.max(radius - rimWidth, INNER_RADIUS_MIN),\n          boxShadow: BODY_EDGE,\n          inset: rimWidth,\n          WebkitBackdropFilter: bodyFilters,\n        }}\n      />\n\n      {/* Refractive rim: the edge that visibly bends what sits behind it. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 rounded-[inherit]\"\n        style={{\n          ...ringMask(rimWidth),\n          backdropFilter: rimBackdrop,\n          WebkitBackdropFilter: rimBackdrop,\n        }}\n      />\n\n      {/* Light layers, isolated so blending never leaks past the pane. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 isolate overflow-hidden rounded-[inherit]\"\n      >\n        <motion.div\n          className=\"absolute inset-0 rounded-[inherit]\"\n          style={{\n            ...ringMask(rimWidth),\n            backgroundImage: CHROMATIC_RIM,\n            mixBlendMode: \"plus-lighter\",\n            x: rimX,\n            y: rimY,\n          }}\n        />\n\n        {specular ? (\n          <motion.div\n            className=\"absolute\"\n            style={{\n              backgroundImage: SPECULAR_GRADIENT,\n              height: SPECULAR_SIZE,\n              left: `${SPECULAR_BASE_X * 100}%`,\n              marginLeft: -SPECULAR_SIZE / 2,\n              marginTop: -SPECULAR_SIZE / 2,\n              mixBlendMode: \"plus-lighter\",\n              top: `${SPECULAR_BASE_Y * 100}%`,\n              width: SPECULAR_SIZE,\n              willChange: \"transform\",\n              x: specularX,\n              y: specularY,\n            }}\n          />\n        ) : null}\n\n        {specular ? (\n          <motion.div\n            className=\"absolute top-0\"\n            style={{\n              backgroundImage: SWEEP_GRADIENT,\n              height: rimWidth + 2,\n              left: `${50 - SWEEP_WIDTH / 2}%`,\n              maskImage: SWEEP_MASK,\n              mixBlendMode: \"plus-lighter\",\n              WebkitMaskImage: SWEEP_MASK,\n              width: `${SWEEP_WIDTH}%`,\n              willChange: \"transform\",\n              x: sweepX,\n            }}\n          />\n        ) : null}\n\n        <div\n          className=\"absolute inset-0 rounded-[inherit]\"\n          style={{ backgroundImage: BOTTOM_SHADE }}\n        />\n      </div>\n\n      {border ? (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-[inherit]\"\n          style={{ boxShadow: INNER_RING }}\n        />\n      ) : null}\n\n      <div className=\"relative\" style={{ padding: contentPadding }}>\n        {children}\n      </div>\n    </motion.div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/glass-card/index.tsx","type":"registry:ui"}],"name":"glass-card","registryDependencies":[],"title":"Glass Card","type":"registry:ui"}