{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fallback-avatar",
  "title": "Fallback Avatar",
  "description": "A unique gradient avatar generated from a name string.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spell-ui/fallback-avatar.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// --- Hash utilities ---\n\nfunction hashString(str: string): [number, number] {\n  let h1 = 0xdeadbeef;\n  let h2 = 0x41c6ce57;\n  for (let i = 0; i < str.length; i++) {\n    const ch = str.charCodeAt(i);\n    h1 = Math.imul(h1 ^ ch, 2654435761);\n    h2 = Math.imul(h2 ^ ch, 1597334677);\n  }\n  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n  h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n  h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n  return [h1 >>> 0, h2 >>> 0];\n}\n\nfunction mulberry32(seed: number) {\n  return function () {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction deriveHue(hash: [number, number]): number {\n  const bytes: number[] = [];\n  for (let i = 0; i < 4; i++) {\n    bytes.push((hash[0] >> (i * 8)) & 0xff);\n    bytes.push((hash[1] >> (i * 8)) & 0xff);\n  }\n  return bytes.reduce((a, b) => a + b, 0) % 360;\n}\n\nfunction oklchToRgb(\n  L: number,\n  C: number,\n  H: number\n): [number, number, number] {\n  const hRad = (H * Math.PI) / 180;\n  const a = C * Math.cos(hRad);\n  const b = C * Math.sin(hRad);\n  const l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n  const m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n  const s_ = L - 0.0894841775 * a - 1.291485548 * b;\n  const l = l_ * l_ * l_;\n  const m = m_ * m_ * m_;\n  const s = s_ * s_ * s_;\n  let R = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;\n  let G = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;\n  let B = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;\n  const gamma = (v: number) =>\n    v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n  return [\n    Math.round(Math.max(0, Math.min(1, gamma(R))) * 255),\n    Math.round(Math.max(0, Math.min(1, gamma(G))) * 255),\n    Math.round(Math.max(0, Math.min(1, gamma(B))) * 255),\n  ];\n}\n\nfunction getColors(\n  hash: [number, number]\n): [[number, number, number], [number, number, number]] {\n  const hue = deriveHue(hash);\n  return [oklchToRgb(0.8, 0.18, hue), oklchToRgb(0.45, 0.18, hue)];\n}\n\n// --- Uniforms ---\n\ninterface Uniforms {\n  S: number;\n  H: number;\n  P: [number, number, number, number];\n  Q: [number, number, number, number];\n  C1: [number, number, number];\n  C2: [number, number, number];\n}\n\nfunction computeUniforms(name: string): Uniforms {\n  const hash = hashString(name);\n  const rng = mulberry32(hash[0]);\n  const [c1, c2] = getColors(hash);\n  const p: number[] = [];\n  for (let i = 0; i < 8; i++) p.push(rng());\n  return {\n    S: hash[0] / 4294967296,\n    H: deriveHue(hash) / 360,\n    P: [p[0], p[1], p[2], p[3]],\n    Q: [p[4], p[5], p[6], p[7]],\n    C1: [c1[0] / 255, c1[1] / 255, c1[2] / 255],\n    C2: [c2[0] / 255, c2[1] / 255, c2[2] / 255],\n  };\n}\n\n// --- WebGL ---\n\nconst VERT_SRC = \"attribute vec2 a;void main(){gl_Position=vec4(a,0,1);}\";\n\nconst FRAG_SRC = `precision mediump float;\nuniform vec2 R;\nuniform float S,H;\nuniform vec4 P,Q;\nuniform vec3 C1,C2;\nuniform float T;\n#define UV (gl_FragCoord.xy/R)\n\nvoid main(){\n  vec2 uv=UV;\n  vec2 b1=vec2(P.x,P.y);\n  vec2 b2=vec2(P.z,P.w);\n  vec2 b3=vec2(Q.x,Q.y);\n  vec2 c1=b1+vec2(sin(T*.7+b1.x*6.)*.08, cos(T*.9+b1.y*6.)*.08);\n  vec2 c2=b2+vec2(sin(T*.6+2.1)*.1, cos(T*.8+1.3)*.07);\n  vec2 c3=b3+vec2(sin(T*.5+4.2)*.07, cos(T*1.1+3.7)*.09);\n  float breath=1.+sin(T*1.3)*.06;\n  float d1=(1.-length(uv-c1)*1.5)*breath;\n  float d2=(1.-length(uv-c2)*1.5)*(1.+sin(T*1.7+1.)*.05);\n  float d3=(1.-length(uv-c3)*1.5)*(1.+sin(T*1.1+2.)*.05);\n  vec3 col=vec3(0);\n  col=1.-(1.-col)*(1.-C1*max(d1,0.));\n  col=1.-(1.-col)*(1.-C2*max(d2,0.));\n  vec3 c3col=mix(C1,C2,.5+sin(T*.4)*.15);\n  col=1.-(1.-col)*(1.-c3col*max(d3,0.));\n  col=clamp(col,0.,1.);\n  gl_FragColor=vec4(col,1);\n}`;\n\nlet _glCanvas: HTMLCanvasElement | null = null;\nlet _gl: WebGLRenderingContext | null = null;\nlet _vShader: WebGLShader | null = null;\nlet _qBuf: WebGLBuffer | null = null;\nlet _prog: WebGLProgram | null = null;\n\nfunction ensureGL() {\n  if (_gl) return;\n  _glCanvas = document.createElement(\"canvas\");\n  _glCanvas.width = 64;\n  _glCanvas.height = 64;\n  _gl = _glCanvas.getContext(\"webgl\", {\n    antialias: false,\n    depth: false,\n    preserveDrawingBuffer: true,\n  });\n  if (!_gl) return;\n\n  _vShader = _gl.createShader(_gl.VERTEX_SHADER)!;\n  _gl.shaderSource(_vShader, VERT_SRC);\n  _gl.compileShader(_vShader);\n\n  _qBuf = _gl.createBuffer();\n  _gl.bindBuffer(_gl.ARRAY_BUFFER, _qBuf);\n  _gl.bufferData(\n    _gl.ARRAY_BUFFER,\n    new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),\n    _gl.STATIC_DRAW\n  );\n\n  const fs = _gl.createShader(_gl.FRAGMENT_SHADER)!;\n  _gl.shaderSource(fs, FRAG_SRC);\n  _gl.compileShader(fs);\n\n  _prog = _gl.createProgram()!;\n  _gl.attachShader(_prog, _vShader);\n  _gl.attachShader(_prog, fs);\n  _gl.linkProgram(_prog);\n}\n\nfunction renderToCanvas(\n  targetCanvas: HTMLCanvasElement,\n  uniforms: Uniforms,\n  time: number\n) {\n  ensureGL();\n  if (!_gl || !_glCanvas || !_prog || !_qBuf) return;\n\n  const pxW = targetCanvas.width;\n  const pxH = targetCanvas.height;\n  if (_glCanvas.width !== pxW || _glCanvas.height !== pxH) {\n    _glCanvas.width = pxW;\n    _glCanvas.height = pxH;\n  }\n\n  _gl.viewport(0, 0, pxW, pxH);\n  _gl.useProgram(_prog);\n  _gl.bindBuffer(_gl.ARRAY_BUFFER, _qBuf);\n\n  const pos = _gl.getAttribLocation(_prog, \"a\");\n  _gl.enableVertexAttribArray(pos);\n  _gl.vertexAttribPointer(pos, 2, _gl.FLOAT, false, 0, 0);\n\n  _gl.uniform2f(_gl.getUniformLocation(_prog, \"R\"), pxW, pxH);\n  _gl.uniform1f(_gl.getUniformLocation(_prog, \"S\"), uniforms.S);\n  _gl.uniform1f(_gl.getUniformLocation(_prog, \"H\"), uniforms.H);\n  _gl.uniform4fv(_gl.getUniformLocation(_prog, \"P\"), uniforms.P);\n  _gl.uniform4fv(_gl.getUniformLocation(_prog, \"Q\"), uniforms.Q);\n  _gl.uniform3fv(_gl.getUniformLocation(_prog, \"C1\"), uniforms.C1);\n  _gl.uniform3fv(_gl.getUniformLocation(_prog, \"C2\"), uniforms.C2);\n  _gl.uniform1f(_gl.getUniformLocation(_prog, \"T\"), time);\n\n  _gl.drawArrays(_gl.TRIANGLES, 0, 6);\n\n  const ctx2d = targetCanvas.getContext(\"2d\");\n  if (ctx2d) {\n    ctx2d.clearRect(0, 0, pxW, pxH);\n    ctx2d.drawImage(_glCanvas, 0, 0);\n  }\n}\n\n// --- Component ---\n\ninterface FallbackAvatarProps {\n  name: string;\n  size?: number;\n  animated?: boolean;\n  className?: string;\n}\n\nexport default function FallbackAvatar({\n  name,\n  size = 32,\n  animated = true,\n  className,\n}: FallbackAvatarProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const animRef = useRef<number>(0);\n  const isHovering = useRef(false);\n  const timeRef = useRef(0);\n  const uniformsRef = useRef<Uniforms | null>(null);\n\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n    return () => setIsMounted(false);\n  }, []);\n\n  const getUniforms = useCallback(() => {\n    if (!uniformsRef.current) {\n      uniformsRef.current = computeUniforms(name);\n    }\n    return uniformsRef.current;\n  }, [name]);\n\n  useEffect(() => {\n    uniformsRef.current = null;\n  }, [name]);\n\n  // Initial static render\n  useEffect(() => {\n    if (!isMounted) return;\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    renderToCanvas(canvas, getUniforms(), 0);\n  }, [name, size, isMounted, getUniforms]);\n\n  // Hover animation\n  useEffect(() => {\n    if (!animated || !isMounted) return;\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    let startTime: number | null = null;\n\n    const animate = (timestamp: number) => {\n      if (!isHovering.current) return;\n      if (startTime === null) startTime = timestamp - timeRef.current * 1000;\n      const elapsed = (timestamp - startTime) / 1000;\n      timeRef.current = elapsed;\n      renderToCanvas(canvas, getUniforms(), elapsed);\n      animRef.current = requestAnimationFrame(animate);\n    };\n\n    const onEnter = () => {\n      isHovering.current = true;\n      startTime = null;\n      animRef.current = requestAnimationFrame(animate);\n    };\n\n    const onLeave = () => {\n      isHovering.current = false;\n      cancelAnimationFrame(animRef.current);\n      renderToCanvas(canvas, getUniforms(), timeRef.current);\n    };\n\n    canvas.addEventListener(\"mouseenter\", onEnter);\n    canvas.addEventListener(\"mouseleave\", onLeave);\n\n    return () => {\n      canvas.removeEventListener(\"mouseenter\", onEnter);\n      canvas.removeEventListener(\"mouseleave\", onLeave);\n      cancelAnimationFrame(animRef.current);\n    };\n  }, [animated, isMounted, getUniforms]);\n\n  const dpr = isMounted ? window.devicePixelRatio || 1 : 1;\n\n  return (\n    <canvas\n      ref={canvasRef}\n      width={size * dpr}\n      height={size * dpr}\n      className={cn(\"rounded-full\", className)}\n      style={{ width: size, height: size }}\n    />\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}