{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grain-cube",
  "title": "Grain Cube",
  "description": "A grainy isometric 3D cube with a flowing gradient on each face. Drag to spin it.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/grain-cube.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type GrainCubeTheme = \"green\" | \"ember\" | \"ice\" | \"violet\" | \"mono\";\n\nexport const GRAIN_CUBE_THEMES: Record<GrainCubeTheme, string[]> = {\n  green: [\"#52c4dc\", \"#f2f6ea\", \"#25e15f\", \"#0b5a2b\", \"#020604\", \"#5a410c\", \"#e6b92c\", \"#25e15f\"],\n  ember: [\"#ffe9c2\", \"#ffb02e\", \"#ff5a1f\", \"#7a1408\", \"#050101\", \"#3a0f3a\", \"#e0459b\", \"#ff5a1f\"],\n  ice: [\"#eaf7ff\", \"#7fd0ff\", \"#2b6cff\", \"#0a1f6b\", \"#02040f\", \"#1a2a5e\", \"#6f8bff\", \"#7fd0ff\"],\n  violet: [\"#f5ecff\", \"#d18bff\", \"#7a2cf0\", \"#2a0a6b\", \"#050212\", \"#5a1a5a\", \"#ff6ad5\", \"#7a2cf0\"],\n  mono: [\"#ffffff\", \"#cfcfcf\", \"#8a8a8a\", \"#3a3a3a\", \"#050505\", \"#2a2a2a\", \"#9a9a9a\", \"#ffffff\"],\n};\n\nexport interface GrainCubeProps {\n  theme?: GrainCubeTheme;\n  colors?: string[];\n  size?: number;\n  scale?: number;\n  warp?: number;\n  grain?: number;\n  animate?: boolean;\n  speed?: number;\n  interactive?: boolean;\n  resolution?: number;\n  style?: React.CSSProperties;\n  className?: string;\n}\n\nconst MAX_STOPS = 8;\nconst DEFAULT_PITCH = 0.6155;\nconst DEFAULT_YAW = Math.PI / 4;\n\nfunction parseColor(input: string): [number, number, number] {\n  const s = input.trim();\n  if (s.startsWith(\"#\")) {\n    let c = s.slice(1);\n    if (c.length === 3 || c.length === 4)\n      c = c\n        .split(\"\")\n        .map((x) => x + x)\n        .join(\"\");\n    if (c.length >= 6) {\n      return [\n        parseInt(c.slice(0, 2), 16) / 255,\n        parseInt(c.slice(2, 4), 16) / 255,\n        parseInt(c.slice(4, 6), 16) / 255,\n      ];\n    }\n  }\n  const m = s.match(/rgba?\\(([^)]+)\\)/i);\n  if (m) {\n    const [r, g, b] = m[1].split(\",\").map((x) => parseFloat(x));\n    return [r / 255, g / 255, b / 255];\n  }\n  return [0, 0, 0];\n}\n\nfunction buildPalette(colors: string[]) {\n  const rgb = colors.slice(0, MAX_STOPS).map(parseColor);\n  const data = new Float32Array(MAX_STOPS * 3);\n  for (let i = 0; i < MAX_STOPS; i++) data.set(rgb[Math.min(i, rgb.length - 1)], i * 3);\n  return { data, count: rgb.length };\n}\n\nfunction rotation(yaw: number, pitch: number) {\n  const cy = Math.cos(yaw);\n  const sy = Math.sin(yaw);\n  const cp = Math.cos(pitch);\n  const sp = Math.sin(pitch);\n  return new Float32Array([cy, 0, sy, sp * sy, cp, -sp * cy, -cp * sy, sp, cp * cy]);\n}\n\ntype Live = {\n  palette: { data: Float32Array; count: number };\n  size: number;\n  scale: number;\n  warp: number;\n  grain: number;\n  animate: boolean;\n  speed: number;\n  interactive: boolean;\n  resolution: number;\n};\n\nconst VERTEX = `\nattribute vec2 a_pos;\nvoid main() { gl_Position = vec4(a_pos, 0.0, 1.0); }\n`;\n\nconst FRAGMENT = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec3 u_pal[8];\nuniform float u_n;\nuniform mat3 u_rinv;\nuniform float u_size;\nuniform float u_scale;\nuniform float u_warp;\nuniform float u_grain;\n\nvec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }\n\nfloat snoise(vec2 v) {\n  const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);\n  vec2 i = floor(v + dot(v, C.yy));\n  vec2 x0 = v - i + dot(i, C.xx);\n  vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n  vec4 x12 = x0.xyxy + C.xxzz;\n  x12.xy -= i1;\n  i = mod(i, 289.0);\n  vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));\n  vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);\n  m = m * m;\n  m = m * m;\n  vec3 x = 2.0 * fract(p * C.www) - 1.0;\n  vec3 h = abs(x) - 0.5;\n  vec3 ox = floor(x + 0.5);\n  vec3 a0 = x - ox;\n  m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);\n  vec3 g;\n  g.x = a0.x * x0.x + h.x * x0.y;\n  g.yz = a0.yz * x12.xz + h.yz * x12.yw;\n  return 130.0 * dot(m, g);\n}\n\nfloat fbm(vec2 p) {\n  return 0.6 * snoise(p) + 0.25 * snoise(p * 2.0 + vec2(17.1, 9.3));\n}\n\nvec3 pal(float t) {\n  t = clamp(t, 0.0, 1.0) * (u_n - 1.0);\n  vec3 c = u_pal[0];\n  for (int i = 0; i < 7; i++) {\n    float k = clamp(t - float(i), 0.0, 1.0);\n    k = k * k * (3.0 - 2.0 * k);\n    c = mix(c, u_pal[i + 1], k);\n  }\n  return c;\n}\n\nbool box(vec3 ro, vec3 rd, out float tN, out vec3 nrm) {\n  vec3 inv = 1.0 / rd;\n  vec3 t1 = (-1.0 - ro) * inv;\n  vec3 t2 = (1.0 - ro) * inv;\n  vec3 tmin = min(t1, t2);\n  vec3 tmax = max(t1, t2);\n  tN = max(max(tmin.x, tmin.y), tmin.z);\n  float tF = min(min(tmax.x, tmax.y), tmax.z);\n  if (tF <= tN || tF <= 0.0) return false;\n  vec3 s = -sign(rd);\n  nrm = tN == tmin.x ? vec3(s.x, 0.0, 0.0) : (tN == tmin.y ? vec3(0.0, s.y, 0.0) : vec3(0.0, 0.0, s.z));\n  return true;\n}\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);\n}\n\nvoid main() {\n  vec2 fc = gl_FragCoord.xy - 0.5 * u_res;\n  float px = 2.0 * (1.732 / u_size) / u_res.y;\n  vec3 rd = u_rinv * vec3(0.0, 0.0, -1.0) + vec3(1e-5);\n\n  float cov = 0.0;\n  bool any = false;\n  vec3 hitP = vec3(0.0);\n  vec3 hitN = vec3(0.0);\n  for (int i = 0; i < 4; i++) {\n    vec2 o = i == 0 ? vec2(-0.25, -0.125) : (i == 1 ? vec2(0.125, -0.25) : (i == 2 ? vec2(0.25, 0.125) : vec2(-0.125, 0.25)));\n    vec3 ro = u_rinv * vec3((fc + o) * px, 6.0);\n    float t;\n    vec3 n;\n    if (box(ro, rd, t, n)) {\n      cov += 0.25;\n      if (!any) {\n        any = true;\n        hitP = ro + rd * t;\n        hitN = n;\n      }\n    }\n  }\n  if (!any) {\n    gl_FragColor = vec4(0.0);\n    return;\n  }\n\n  vec2 fu;\n  float seed;\n  vec3 a = abs(hitN);\n  if (a.x > 0.5) {\n    fu = hitP.yz;\n    seed = hitN.x > 0.0 ? 1.0 : 2.0;\n  } else if (a.y > 0.5) {\n    fu = hitP.xz;\n    seed = hitN.y > 0.0 ? 3.0 : 4.0;\n  } else {\n    fu = hitP.xy;\n    seed = hitN.z > 0.0 ? 5.0 : 6.0;\n  }\n\n  vec2 p = fu * 0.3 / u_scale + vec2(seed * 13.7, seed * 7.3);\n  float t = u_time * 0.15;\n  vec2 q = vec2(fbm(p + vec2(t, 0.0)), fbm(p + vec2(5.2, 1.3 - t)));\n  float f = fbm(p + u_warp * 0.9 * q);\n\n  float g = hash(gl_FragCoord.xy);\n  float lean = 0.24 + 0.4 * fract(sin(seed * 91.7) * 437.5);\n  float v = f * 1.0 + lean + (g - 0.5) * u_grain * 0.25;\n  vec3 c = pal(v);\n\n  vec3 nw = hitN * u_rinv;\n  float lit = clamp(dot(nw, normalize(vec3(-0.35, 0.8, 0.5))), 0.0, 1.0);\n  c *= 0.86 + 0.14 * lit;\n  c += (g - 0.5) * u_grain * 0.06;\n  c = clamp(c, 0.0, 1.0);\n\n  gl_FragColor = vec4(c * cov, cov);\n}\n`;\n\nfunction GrainCube({\n  theme = \"green\",\n  colors,\n  size = 0.7,\n  scale = 1,\n  warp = 1,\n  grain = 0.05,\n  animate = true,\n  speed = 1,\n  interactive = true,\n  resolution = 1,\n  style,\n  className,\n}: GrainCubeProps) {\n  const hostRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const syncRef = React.useRef<(() => void) | null>(null);\n  const [failed, setFailed] = React.useState(false);\n  const [nonce, setNonce] = React.useState(0);\n\n  const list = colors && colors.length >= 2 ? colors : GRAIN_CUBE_THEMES[theme];\n  const listKey = JSON.stringify(list);\n  const palette = React.useMemo(() => buildPalette(JSON.parse(listKey)), [listKey]);\n\n  const live = React.useRef<Live>({\n    palette,\n    size,\n    scale,\n    warp,\n    grain,\n    animate,\n    speed,\n    interactive,\n    resolution,\n  });\n\n  React.useEffect(() => {\n    live.current = { palette, size, scale, warp, grain, animate, speed, interactive, resolution };\n    syncRef.current?.();\n  });\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const host = hostRef.current;\n    if (!canvas || !host) return;\n\n    const gl = canvas.getContext(\"webgl\", {\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: false,\n      depth: false,\n      stencil: false,\n      powerPreference: \"high-performance\",\n    });\n    if (!gl) {\n      setFailed(true);\n      return;\n    }\n\n    const compile = (type: number, src: string) => {\n      const shader = gl.createShader(type)!;\n      gl.shaderSource(shader, src);\n      gl.compileShader(shader);\n      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n        console.warn(\"GrainCube shader:\", gl.getShaderInfoLog(shader));\n        return null;\n      }\n      return shader;\n    };\n\n    const vs = compile(gl.VERTEX_SHADER, VERTEX);\n    const fs = compile(gl.FRAGMENT_SHADER, FRAGMENT);\n    const program = gl.createProgram()!;\n    if (!vs || !fs) {\n      setFailed(true);\n      return;\n    }\n    gl.attachShader(program, vs);\n    gl.attachShader(program, fs);\n    gl.linkProgram(program);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      console.warn(\"GrainCube link:\", gl.getProgramInfoLog(program));\n      setFailed(true);\n      return;\n    }\n    gl.useProgram(program);\n\n    const buffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const aPos = gl.getAttribLocation(program, \"a_pos\");\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n    gl.clearColor(0, 0, 0, 0);\n\n    const u = (name: string) => gl.getUniformLocation(program, name);\n    const loc = {\n      res: u(\"u_res\"),\n      time: u(\"u_time\"),\n      pal: u(\"u_pal[0]\"),\n      n: u(\"u_n\"),\n      rinv: u(\"u_rinv\"),\n      size: u(\"u_size\"),\n      scale: u(\"u_scale\"),\n      warp: u(\"u_warp\"),\n      grain: u(\"u_grain\"),\n    };\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const rot = { yaw: DEFAULT_YAW, pitch: DEFAULT_PITCH, vel: 0, dragging: false, x: 0, y: 0 };\n    let raf = 0;\n    let visible = true;\n    let last = 0;\n    let time = Math.random() * 50;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const res = Math.min(1, Math.max(0.5, live.current.resolution));\n      const w = Math.max(1, Math.round(host.clientWidth * dpr * res));\n      const h = Math.max(1, Math.round(host.clientHeight * dpr * res));\n      if (canvas.width !== w || canvas.height !== h) {\n        canvas.width = w;\n        canvas.height = h;\n      }\n    };\n\n    const draw = () => {\n      const L = live.current;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.uniform2f(loc.res, canvas.width, canvas.height);\n      gl.uniform1f(loc.time, time);\n      gl.uniform3fv(loc.pal, L.palette.data);\n      gl.uniform1f(loc.n, L.palette.count);\n      gl.uniformMatrix3fv(loc.rinv, false, rotation(rot.yaw, rot.pitch));\n      gl.uniform1f(loc.size, Math.min(1, Math.max(0.2, L.size)));\n      gl.uniform1f(loc.scale, Math.max(0.1, L.scale));\n      gl.uniform1f(loc.warp, L.warp);\n      gl.uniform1f(loc.grain, L.grain);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      const L = live.current;\n      const moving = L.animate && L.speed !== 0 && !reduced;\n      const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;\n      last = now;\n\n      if (moving) {\n        time += dt * L.speed;\n        if (!rot.dragging) rot.yaw += dt * L.speed * 0.5;\n      }\n      if (!rot.dragging) {\n        rot.yaw += rot.vel * dt;\n        rot.vel *= Math.pow(0.05, dt);\n        rot.pitch += (DEFAULT_PITCH - rot.pitch) * Math.min(1, dt * 2.5);\n      }\n      draw();\n\n      const settling =\n        rot.dragging || Math.abs(rot.vel) > 0.01 || Math.abs(rot.pitch - DEFAULT_PITCH) > 0.002;\n      if ((moving || settling) && visible && !document.hidden) raf = requestAnimationFrame(tick);\n    };\n\n    const schedule = () => {\n      if (raf) return;\n      last = 0;\n      raf = requestAnimationFrame(tick);\n    };\n\n    syncRef.current = () => {\n      resize();\n      schedule();\n    };\n\n    const onDown = (e: PointerEvent) => {\n      if (!live.current.interactive) return;\n      rot.dragging = true;\n      rot.vel = 0;\n      rot.x = e.clientX;\n      rot.y = e.clientY;\n      host.setPointerCapture(e.pointerId);\n      host.style.cursor = \"grabbing\";\n      schedule();\n    };\n    const onMove = (e: PointerEvent) => {\n      if (!rot.dragging) return;\n      const dx = e.clientX - rot.x;\n      const dy = e.clientY - rot.y;\n      rot.x = e.clientX;\n      rot.y = e.clientY;\n      rot.yaw += dx * 0.012;\n      rot.pitch = Math.min(1.2, Math.max(-1.2, rot.pitch + dy * 0.008));\n      rot.vel = dx * 0.012 * 60;\n      schedule();\n    };\n    const onUp = (e: PointerEvent) => {\n      if (!rot.dragging) return;\n      rot.dragging = false;\n      if (host.hasPointerCapture(e.pointerId)) host.releasePointerCapture(e.pointerId);\n      host.style.cursor = live.current.interactive ? \"grab\" : \"\";\n      schedule();\n    };\n    host.addEventListener(\"pointerdown\", onDown);\n    host.addEventListener(\"pointermove\", onMove);\n    host.addEventListener(\"pointerup\", onUp);\n    host.addEventListener(\"pointercancel\", onUp);\n\n    resize();\n    schedule();\n\n    const ro = new ResizeObserver(() => syncRef.current?.());\n    ro.observe(host);\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) schedule();\n    });\n    io.observe(host);\n\n    const onVisibility = () => {\n      if (!document.hidden) schedule();\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    const onLost = (e: Event) => {\n      e.preventDefault();\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n    const onRestored = () => setNonce((n) => n + 1);\n    canvas.addEventListener(\"webglcontextlost\", onLost);\n    canvas.addEventListener(\"webglcontextrestored\", onRestored);\n\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      syncRef.current = null;\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      canvas.removeEventListener(\"webglcontextlost\", onLost);\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored);\n      host.removeEventListener(\"pointerdown\", onDown);\n      host.removeEventListener(\"pointermove\", onMove);\n      host.removeEventListener(\"pointerup\", onUp);\n      host.removeEventListener(\"pointercancel\", onUp);\n      gl.deleteBuffer(buffer);\n      gl.deleteProgram(program);\n      gl.deleteShader(vs);\n      gl.deleteShader(fs);\n    };\n  }, [nonce]);\n\n  return (\n    <div\n      ref={hostRef}\n      className={cn(\"relative aspect-square w-full select-none\", className)}\n      style={{\n        cursor: interactive ? \"grab\" : undefined,\n        touchAction: interactive ? \"pan-y\" : undefined,\n        ...style,\n      }}\n    >\n      {failed ? (\n        <div\n          aria-hidden=\"true\"\n          className=\"absolute top-1/2 left-1/2 aspect-square -translate-x-1/2 -translate-y-1/2 rotate-45 rounded-md\"\n          style={{\n            width: `${size * 55}%`,\n            backgroundImage: `linear-gradient(135deg, ${list.join(\", \")})`,\n          }}\n        />\n      ) : (\n        <canvas ref={canvasRef} aria-hidden=\"true\" className=\"block h-full w-full\" />\n      )}\n    </div>\n  );\n}\n\nGrainCube.displayName = \"GrainCube\";\n\nexport { GrainCube };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}