{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"mesh-gradient","type":"registry:component","title":"Mesh Gradient","description":"Soft fields of colour that drift, swirl and blend, with fine film grain.","author":"DiMaac (HankTheTank) <https://dimaac.com>","dependencies":[],"files":[{"path":"registry/dimaac/mesh-gradient/mesh-gradient.tsx","type":"registry:component","target":"components/dimaac/mesh-gradient.tsx","content":"\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore, type CSSProperties, type ReactNode } from \"react\";\n\n// A calm, painterly mesh gradient. One small WebGL1 fragment shader: four or five colour\n// fields drift on slow loops of their own, the space they live in is bent by two layers of\n// noise (domain warping) so the edges swirl like wet paint, and a fine film grain sits on top.\n// The colours lean towards the cursor. No libraries, one full-screen triangle, one draw a frame.\n\nexport type MeshGradientPreset = \"auto\" | \"aurora\" | \"dusk\" | \"lime-ink\" | \"paper\";\n\nexport type MeshGradientProps = {\n  /** Four or five hex colours, or a preset name. \"auto\" is aurora, tuned for the page's light or dark theme. */\n  colors?: readonly string[] | MeshGradientPreset;\n  /** How fast the colours drift, from 0 (still) to 1. */\n  speed?: number;\n  /** How much the noise bends and swirls the colour fields, from 0 (soft blobs) to 1. */\n  warp?: number;\n  /** Film grain strength, from 0 to 0.2. It also hides colour banding. */\n  grain?: number;\n  /** How much the colours lean towards the cursor (or finger), from 0 to 1. */\n  cursorInfluence?: number;\n  /** Which tuning the presets use. \"auto\" follows the page: data-theme, a dark class, or the system setting. */\n  theme?: \"auto\" | \"light\" | \"dark\";\n  /** Extra classes for the outer box. Give it a size (it fills it). */\n  className?: string;\n  /** Content shown over the gradient. It can use var(--mesh-ink) for a text colour that reads on it. */\n  children?: ReactNode;\n};\n\n// ---------- palettes, each tuned once for light pages and once for dark ----------\n\ntype NamedPreset = Exclude<MeshGradientPreset, \"auto\">;\n\n// Dark tunings keep the base deep, because light text sits on them. A bright colour may still\n// appear, but only as the \"accent\" field: a tighter, smaller glow that drifts through the deep\n// colours instead of washing over them. Light tunings are the mirror.\ntype Tuning = { colors: string[]; accent?: number };\nconst presets: Record<NamedPreset, { light: Tuning; dark: Tuning }> = {\n  // The owner's favourite: deep aurora teal into mint\n  aurora: {\n    light: { colors: [\"#e6fbf6\", \"#99f6e4\", \"#5eead4\", \"#0f766e\", \"#f4fbf8\"] },\n    dark: { colors: [\"#021c1a\", \"#0f766e\", \"#0a4d47\", \"#14b8a6\", \"#99f6e4\"], accent: 4 },\n  },\n  dusk: {\n    light: { colors: [\"#fff1e8\", \"#f9a8d4\", \"#fdba74\", \"#c4b5fd\", \"#fde4ef\"] },\n    dark: { colors: [\"#140f2e\", \"#5b21b6\", \"#be185d\", \"#c2410c\", \"#1e1b4b\"] },\n  },\n  // DiMaac's paper, ink and lime. On dark, ink leads and the brand lime is one tight accent\n  // bloom: spread wide, lime mixed into black reads as olive, so it is kept small and bright\n  \"lime-ink\": {\n    light: { colors: [\"#f7f4ee\", \"#e9e4d8\", \"#d6fd51\", \"#f1f7c9\", \"#c8c1b0\"] },\n    dark: { colors: [\"#0d0c0a\", \"#161512\", \"#d6fd51\", \"#1c1a16\", \"#24221d\"], accent: 2 },\n  },\n  paper: {\n    light: { colors: [\"#faf9f6\", \"#ebe6dc\", \"#d8d1c3\", \"#f3f0e9\", \"#bfb7a7\"] },\n    dark: { colors: [\"#100f0e\", \"#24211d\", \"#3d3832\", \"#161512\", \"#5b554b\"] },\n  },\n};\n\n// ---------- shaders ----------\n\nconst vertexShader = `\nattribute vec2 position;\nvarying vec2 uv;\nvoid main() {\n  uv = position * 0.5 + 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}`;\n\nconst fragmentShader = `\nprecision highp float;\nvarying vec2 uv;\nuniform vec3 colors[5];\nuniform float count;\nuniform float time;\nuniform float aspect;\nuniform float warp;\nuniform float grain;\nuniform vec2 pointer;\nuniform float pull;\nuniform float seed;\nuniform float lead;\nuniform float accent;\n\nfloat random(vec2 point) {\n  return fract(sin(dot(point, vec2(12.9898, 78.233))) * 43758.5453);\n}\n// A cleaner hash for the grain, so it reads as film and not as a pattern\nfloat speckle(vec2 point) {\n  vec3 p = fract(vec3(point.xyx) * 0.1031);\n  p += dot(p, p.yzx + 33.33);\n  return fract((p.x + p.y) * p.z);\n}\nfloat noise(vec2 point) {\n  vec2 cell = floor(point);\n  vec2 f = fract(point);\n  vec2 blend = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n  float a = random(cell);\n  float b = random(cell + vec2(1.0, 0.0));\n  float c = random(cell + vec2(0.0, 1.0));\n  float d = random(cell + vec2(1.0, 1.0));\n  return mix(mix(a, b, blend.x), mix(c, d, blend.x), blend.y);\n}\n// A few layers of noise, each smaller and turned, for an organic, non-grid look\nfloat fbm(vec2 point) {\n  float total = 0.0;\n  float amount = 0.5;\n  mat2 turn = mat2(0.8, 0.6, -0.6, 0.8);\n  for (int i = 0; i < 4; i++) {\n    total += amount * noise(point);\n    point = turn * point * 2.03 + 3.1;\n    amount *= 0.5;\n  }\n  return total;\n}\n\nvoid main() {\n  // Measured in shorter-sides, so the fields keep their size on a tall phone as on a wide screen\n  vec2 size = vec2(aspect, 1.0) / min(aspect, 1.0);\n  vec2 point = uv * size;\n  vec2 cursor = pointer * size;\n\n  // The cursor gently draws the space around it in\n  vec2 toCursor = cursor - point;\n  point += toCursor * exp(-dot(toCursor, toCursor) * 3.0) * pull * 0.3;\n\n  // Domain warping: noise moves the point, then noise of that moves it again\n  vec2 q = vec2(fbm(point * 1.2 + vec2(0.0, time * 0.05)), fbm(point * 1.2 + vec2(5.2, 1.3) - time * 0.04));\n  vec2 r = vec2(fbm(point * 1.4 + q * 2.2 + vec2(1.7, 9.2) + time * 0.03), fbm(point * 1.4 + q * 2.2 + vec2(8.3, 2.8) - time * 0.035));\n  vec2 bent = point + (r - 0.5) * warp * 1.4;\n\n  // Each colour is a soft field around a centre that drifts on its own slow loop.\n  // Mixing happens in (roughly) linear light so blends glow instead of going muddy.\n  vec3 sum = vec3(0.0);\n  float total = 0.0;\n  for (int i = 0; i < 5; i++) {\n    if (float(i) >= count) break;\n    float f = float(i);\n    vec2 centre = size * vec2(\n      0.5 + cos(time * (0.21 + f * 0.047) + f * 2.4) * 0.45,\n      0.5 + sin(time * (0.17 + f * 0.039) + f * 1.7) * 0.42\n    );\n    centre += (cursor - centre) * pull * 0.22;\n    vec2 away = bent - centre;\n    // The accent field (if any) is a tighter glow but stronger at its heart, so a bright colour\n    // blooms clearly through the deep base and then falls away fast, instead of flooding\n    bool isAccent = abs(f - accent) < 0.5;\n    float weight = exp(-dot(away, away) * (isAccent ? 9.0 : 4.2)) * (isAccent ? 1.8 : 1.0) + 0.0005;\n    // The first colour (the base) can be given a floor, so it always leads the mix\n    if (i == 0) weight += lead;\n    sum += colors[i] * colors[i] * weight;\n    total += weight;\n  }\n  vec3 color = sqrt(sum / total);\n\n  // A faint sheen along the folds, like light on a brushstroke\n  color *= 1.0 + (r.x - 0.5) * 0.12 * warp;\n\n  // Film grain, new every frame, plus a hair of dither so smooth ramps never band\n  float speck = speckle(gl_FragCoord.xy + seed * 17.0) - 0.5;\n  color += speck * (grain + 1.5 / 255.0);\n  gl_FragColor = vec4(color, 1.0);\n}`;\n\n// ---------- small helpers ----------\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let value = hex.trim().replace(/^#/, \"\");\n  if (value.length === 3) value = value.replace(/./g, (c) => c + c);\n  const n = /^[0-9a-f]{6}$/i.test(value) ? parseInt(value, 16) : 0x808080;\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\n// Is the gradient light overall? Then text on it should be ink, otherwise paper\nfunction isLight(colors: readonly string[]): boolean {\n  const lightness = colors.map((hex) => {\n    const [r, g, b] = hexToRgb(hex);\n    return 0.2126 * r * r + 0.7152 * g * g + 0.0722 * b * b;\n  });\n  return lightness.reduce((a, b) => a + b, 0) / lightness.length > 0.3;\n}\n\nfunction pageIsDark(): boolean {\n  const root = document.documentElement;\n  const picked = root.getAttribute(\"data-theme\");\n  if (picked === \"dark\") return true;\n  if (picked === \"light\") return false;\n  if (root.classList.contains(\"dark\")) return true;\n  if (root.classList.contains(\"light\")) return false;\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n}\n\nfunction watchPageTheme(onChange: () => void): () => void {\n  const observer = new MutationObserver(onChange);\n  observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\", \"class\"] });\n  const query = window.matchMedia(\"(prefers-color-scheme: dark)\");\n  query.addEventListener(\"change\", onChange);\n  return () => {\n    observer.disconnect();\n    query.removeEventListener(\"change\", onChange);\n  };\n}\n\nfunction subscribeToMotion(onChange: () => void) {\n  const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n}\nconst prefersReducedMotion = () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nconst noSubscription = () => () => {};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\n// ---------- the component ----------\n\nexport function MeshGradient({ colors = \"auto\", speed = 0.4, warp = 0.6, grain = 0.06, cursorInfluence = 0.5, theme = \"auto\", className = \"\", children }: MeshGradientProps) {\n  const boxRef = useRef<HTMLDivElement>(null);\n  const hostRef = useRef<HTMLDivElement>(null);\n  const reducedMotion = useSyncExternalStore(subscribeToMotion, prefersReducedMotion, () => false);\n  // The server can't know the page theme, so it renders no colours at all (see style below)\n  const pageDark = useSyncExternalStore(watchPageTheme, pageIsDark, () => true);\n  const hydrated = useSyncExternalStore(noSubscription, () => true, () => false);\n  const dark = theme === \"auto\" ? pageDark : theme === \"dark\";\n\n  // The colours actually used: a preset (in this theme's tuning) or the caller's own\n  const presetName: NamedPreset | null = typeof colors === \"string\" ? (colors === \"auto\" ? \"aurora\" : colors) : null;\n  const tuning = presetName ? presets[presetName][dark ? \"dark\" : \"light\"] : null;\n  const palette = tuning ? tuning.colors : (colors as readonly string[]).slice(0, 5);\n  const accent = tuning?.accent ?? -1;\n  const paletteKey = palette.join(\",\");\n  // Presets follow the theme (pale on light pages, deep on dark); your own colours are measured\n  const light = presetName ? !dark : isLight(palette);\n\n  // Dark presets let the deep base colour always lead, so light text never sits on a bright patch\n  const lead = presetName && dark ? (presetName === \"aurora\" ? 0.35 : 0.45) : 0;\n  const settings = useRef({ palette, speed, warp, grain, cursorInfluence, lead, accent });\n  // In reduced motion there is no loop, so a prop change asks for one fresh still frame\n  const redraw = useRef<(() => void) | null>(null);\n  const [failed, setFailed] = useState(false);\n  // A lost context (GPU reset, or the browser reclaiming contexts on a busy page) is retried\n  const [lost, setLost] = useState(false);\n  const retries = useRef(0);\n  const [ready, setReady] = useState(false);\n\n  // The render loop reads these every frame, so changing a prop never rebuilds WebGL\n  useEffect(() => {\n    settings.current = { palette: paletteKey ? paletteKey.split(\",\") : [], speed, warp, grain, cursorInfluence, lead, accent };\n    redraw.current?.();\n  }, [paletteKey, speed, warp, grain, cursorInfluence, lead, accent]);\n\n  // After a lost context, wait a moment and build a fresh canvas (a few times at most, so a\n  // GPU that keeps failing settles on the CSS fallback instead of flickering)\n  useEffect(() => {\n    if (!lost) return;\n    const id = window.setTimeout(() => {\n      retries.current += 1;\n      setLost(false);\n    }, 1200);\n    return () => window.clearTimeout(id);\n  }, [lost]);\n\n  useEffect(() => {\n    const box = boxRef.current;\n    const host = hostRef.current;\n    if (!box || !host || failed || lost) return;\n\n    // A fresh canvas every time: a canvas whose context was released can't make a new one\n    const canvas = document.createElement(\"canvas\");\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    canvas.style.cssText = \"position:absolute;inset:0;width:100%;height:100%;display:block\";\n    host.appendChild(canvas);\n\n    const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: false, depth: false, powerPreference: \"low-power\" });\n    if (!gl) {\n      canvas.remove();\n      setFailed(true);\n      return;\n    }\n    const context = gl;\n\n    const program = context.createProgram()!;\n    for (const [type, source] of [\n      [context.VERTEX_SHADER, vertexShader],\n      [context.FRAGMENT_SHADER, fragmentShader],\n    ] as const) {\n      const shader = context.createShader(type)!;\n      context.shaderSource(shader, source);\n      context.compileShader(shader);\n      context.attachShader(program, shader);\n      context.deleteShader(shader);\n    }\n    context.bindAttribLocation(program, 0, \"position\");\n    context.linkProgram(program);\n    if (!context.getProgramParameter(program, context.LINK_STATUS)) {\n      context.deleteProgram(program);\n      context.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n      setFailed(true);\n      return;\n    }\n    context.useProgram(program);\n\n    // One triangle that covers the whole screen\n    const buffer = context.createBuffer();\n    context.bindBuffer(context.ARRAY_BUFFER, buffer);\n    context.bufferData(context.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), context.STATIC_DRAW);\n    context.enableVertexAttribArray(0);\n    context.vertexAttribPointer(0, 2, context.FLOAT, false, 0, 0);\n\n    const uniform = (name: string) => context.getUniformLocation(program, name);\n    const at = {\n      colors: uniform(\"colors\"),\n      count: uniform(\"count\"),\n      time: uniform(\"time\"),\n      aspect: uniform(\"aspect\"),\n      warp: uniform(\"warp\"),\n      grain: uniform(\"grain\"),\n      pointer: uniform(\"pointer\"),\n      pull: uniform(\"pull\"),\n      seed: uniform(\"seed\"),\n      lead: uniform(\"lead\"),\n      accent: uniform(\"accent\"),\n    };\n\n    // The shown colours glide to new ones instead of jumping when the palette changes\n    function targetColors() {\n      const list = settings.current.palette.length ? settings.current.palette : presets.aurora.dark.colors;\n      const rgb = list.map(hexToRgb);\n      while (rgb.length < 5) rgb.push(rgb[rgb.length % list.length]!);\n      return { values: rgb.flat(), count: clamp(list.length, 1, 5) };\n    }\n    const shown = targetColors().values;\n\n    let alive = true;\n    let frame = 0;\n    let visible = true;\n    // Start part-way into the loops so the first frame is already a nice composition\n    let time = 14;\n    let lastTime = performance.now();\n    let seed = 0;\n\n    // Where the colours lean: the eased pointer, and how present it is (0 when away)\n    const pointer = { x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5, presence: 0, targetPresence: 0 };\n\n    function resize() {\n      // Soft colour needs few pixels: cap the pixel ratio at 1.5 to stay light on the GPU\n      const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);\n      canvas.width = Math.max(1, Math.round(box!.clientWidth * pixelRatio));\n      canvas.height = Math.max(1, Math.round(box!.clientHeight * pixelRatio));\n      context.viewport(0, 0, canvas.width, canvas.height);\n      if (reducedMotion) drawFrame();\n    }\n\n    function drawFrame() {\n      const { count } = targetColors();\n      const { warp: bend, grain: grainAmount, cursorInfluence: lean, lead, accent } = settings.current;\n      context.uniform3fv(at.colors, shown);\n      context.uniform1f(at.count, count);\n      context.uniform1f(at.time, time);\n      context.uniform1f(at.aspect, canvas.width / canvas.height);\n      context.uniform1f(at.warp, clamp(bend, 0, 1));\n      context.uniform1f(at.grain, clamp(grainAmount, 0, 0.2));\n      context.uniform2f(at.pointer, pointer.x, pointer.y);\n      context.uniform1f(at.pull, clamp(lean, 0, 1) * pointer.presence);\n      context.uniform1f(at.seed, seed);\n      context.uniform1f(at.lead, lead);\n      context.uniform1f(at.accent, accent);\n      context.drawArrays(context.TRIANGLES, 0, 3);\n    }\n\n    // Easing written per 60 Hz frame, turned into the right amount for any frame length\n    const ease = (perFrame: number, frames: number) => 1 - Math.pow(1 - perFrame, frames);\n\n    // Advance by the real time since the last frame (measured in 60 Hz frames), so the drift\n    // is smooth whatever the refresh rate and never stalls or doubles up on a jittery frame\n    function step(frames: number) {\n      time += (frames / 60) * clamp(settings.current.speed, 0, 1) * 1.6;\n      // The lean is slow on purpose: the colours drift towards the cursor, they don't chase it\n      const follow = ease(0.035, frames);\n      pointer.x += (pointer.targetX - pointer.x) * follow;\n      pointer.y += (pointer.targetY - pointer.y) * follow;\n      pointer.presence += (pointer.targetPresence - pointer.presence) * ease(0.03, frames);\n      const glide = ease(0.05, frames);\n      const target = targetColors().values;\n      for (let i = 0; i < shown.length; i++) shown[i]! += (target[i]! - shown[i]!) * glide;\n    }\n\n    function render(now: number) {\n      if (!alive) return;\n      frame = requestAnimationFrame(render);\n      if (!visible) {\n        lastTime = now;\n        return;\n      }\n      // Capped, so coming back to a tab doesn't jump the colours forward\n      const frames = Math.min(Math.max(now - lastTime, 0), 100) / (1000 / 60);\n      lastTime = now;\n      step(frames);\n      seed = (seed + 1.618) % 97;\n      drawFrame();\n    }\n\n    function onPointerMove(event: PointerEvent) {\n      if (!event.isPrimary) return;\n      const rect = box!.getBoundingClientRect();\n      pointer.targetX = (event.clientX - rect.left) / rect.width;\n      pointer.targetY = 1 - (event.clientY - rect.top) / rect.height;\n      pointer.targetPresence = 1;\n    }\n    function onPointerLeave(event: PointerEvent) {\n      if (event.pointerType === \"mouse\") pointer.targetPresence = 0;\n    }\n    // A lifted finger lets the colours drift back by themselves\n    function onPointerUp(event: PointerEvent) {\n      if (event.pointerType !== \"mouse\") pointer.targetPresence = 0;\n    }\n\n    function onContextLost(event: Event) {\n      event.preventDefault();\n      cancelAnimationFrame(frame);\n      if (!alive) return;\n      if (retries.current < 3) setLost(true);\n      else setFailed(true);\n    }\n\n    const resizeObserver = new ResizeObserver(resize);\n    // Nothing to draw while the box is off screen\n    const visibility = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? true;\n    });\n\n    resize();\n    resizeObserver.observe(box);\n    canvas.addEventListener(\"webglcontextlost\", onContextLost);\n\n    if (reducedMotion) {\n      // A still frame: the colours snap straight to the palette and stay put\n      redraw.current = () => {\n        shown.splice(0, shown.length, ...targetColors().values);\n        drawFrame();\n      };\n      redraw.current();\n    } else {\n      visibility.observe(box);\n      box.addEventListener(\"pointermove\", onPointerMove);\n      box.addEventListener(\"pointerdown\", onPointerMove);\n      box.addEventListener(\"pointerleave\", onPointerLeave);\n      box.addEventListener(\"pointerup\", onPointerUp);\n      box.addEventListener(\"pointercancel\", onPointerUp);\n      frame = requestAnimationFrame(render);\n    }\n    setReady(true);\n\n    return () => {\n      alive = false;\n      redraw.current = null;\n      cancelAnimationFrame(frame);\n      resizeObserver.disconnect();\n      visibility.disconnect();\n      box.removeEventListener(\"pointermove\", onPointerMove);\n      box.removeEventListener(\"pointerdown\", onPointerMove);\n      box.removeEventListener(\"pointerleave\", onPointerLeave);\n      box.removeEventListener(\"pointerup\", onPointerUp);\n      box.removeEventListener(\"pointercancel\", onPointerUp);\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n      context.deleteBuffer(buffer);\n      context.deleteProgram(program);\n      // Hand the context back now: browsers only allow a few at a time\n      context.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n      setReady(false);\n    };\n  }, [reducedMotion, failed, lost]);\n\n  // Under the canvas (and instead of it without WebGL): the same colours as soft CSS glows\n  const [base = \"#808080\", ...rest] = palette;\n  const spots = [\"18% 22%\", \"82% 28%\", \"70% 82%\", \"24% 78%\"];\n  // The accent stays a small glow here too, as in the shader\n  const fallback = rest\n    .map((hex, i) => {\n      const reach = i + 1 === accent ? \"34% 34%\" : \"60% 60%\";\n      return `radial-gradient(${reach} at ${spots[i % spots.length]}, ${hex} 0%, transparent 70%)`;\n    })\n    .join(\", \");\n\n  // Until hydration the theme is unknown, so nothing commits to a tuning: no fallback colours\n  // and no ink (text inherits the page colour). That avoids a flash of the dark palette on a\n  // light page. The colours then ease in with the canvas.\n  const style = (\n    hydrated\n      ? {\n          \"--mesh-ink\": light ? \"#191713\" : \"#f7f4ee\",\n          \"--mesh-ink-soft\": light ? \"rgba(25, 23, 19, 0.68)\" : \"rgba(247, 244, 238, 0.78)\",\n          color: \"var(--mesh-ink)\",\n          backgroundColor: base,\n          backgroundImage: fallback,\n          touchAction: \"pan-y\",\n        }\n      : { touchAction: \"pan-y\" }\n  ) as CSSProperties;\n\n  return (\n    <div ref={boxRef} className={`relative overflow-hidden ${className}`} style={style}>\n      {failed ? null : (\n        <div\n          ref={hostRef}\n          aria-hidden=\"true\"\n          className=\"absolute inset-0\"\n          style={{ opacity: ready ? 1 : 0, transition: \"opacity 1.6s cubic-bezier(0.2, 0.7, 0.2, 1)\" }}\n        />\n      )}\n      {/* Pointer events from here bubble up to the box, so the colours still follow the cursor under the content */}\n      {children ? <div className=\"relative h-full w-full\">{children}</div> : null}\n    </div>\n  );\n}\n"}],"categories":["backgrounds","background","gradient","webgl","shader","grain","hero"],"docs":"import { MeshGradient } from \"@/components/dimaac/mesh-gradient\". Give its parent a size: it fills it. Docs, playground and props: https://ui.dimaac.com/components/mesh-gradient","meta":{"tier":"free","preview":"https://ui.dimaac.com/components/mesh-gradient","credits":[]}}