{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Canvas-based generative pixel avatar for AI agents, unique per seed.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useRef } from \"react\";\n\nexport type AgentAvatarProps = Omit<\n  React.CanvasHTMLAttributes<HTMLCanvasElement>,\n  \"children\"\n> & {\n  /** String seed to generate a unique deterministic avatar pattern */\n  seed: string;\n  /** Diameter in pixels */\n  size?: number;\n  /** Enable pixel animation (respects prefers-reduced-motion) */\n  animated?: boolean;\n};\n\nconst GRID_SIZE = 6;\n\n/** Pulse: each pixel oscillates lightness independently */\nconst PULSE_SPEED = 0.002;\nconst PULSE_AMPLITUDE = 22;\n\n/** Breathe: global slow scale oscillation */\nconst BREATHE_SPEED = 0.001;\nconst BREATHE_AMPLITUDE = 10;\n\n/** Wave: diagonal sweep across the grid */\nconst WAVE_SPEED = 0.0015;\nconst WAVE_AMPLITUDE = 15;\nconst WAVE_LENGTH = 3;\n\n/** Sparkle: random bright flashes */\nconst SPARKLE_SPEED = 0.004;\nconst SPARKLE_THRESHOLD = 0.92;\nconst SPARKLE_BOOST = 25;\n\n/** Scale pulse: whole avatar breathes in size */\nconst SCALE_PULSE_SPEED = 0.0008;\nconst SCALE_PULSE_AMOUNT = 0.03;\n\n/** Max hue spread from base — wider for richer color variation */\nconst HUE_SPREAD = 45;\n\nconst GLOW_RADIUS_RATIO = 0.25;\n\n/** Simple deterministic hash from a string */\nconst hashSeed = (str: string): number => {\n  let hash = 0;\n  for (const char of str) {\n    hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0;\n  }\n  return Math.abs(hash);\n};\n\n/** Seeded PRNG (mulberry32) */\nconst createRng = (seed: number) => {\n  let state = seed;\n  return () => {\n    state = (state + 0x6d_2b_79_f5) | 0;\n    let t = Math.imul(state ^ (state >>> 15), 1 | state);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;\n  };\n};\n\ntype HSL = [hue: number, saturation: number, lightness: number];\n\n/** Derive a 3-color palette within the same hue family */\nconst generatePalette = (hash: number): [HSL, HSL, HSL] => {\n  const rng = createRng(hash);\n  const baseHue = rng() * 360;\n  const sat = 75 + rng() * 20; // 75-95%\n\n  return [\n    [baseHue, sat, 55 + rng() * 10],\n    [\n      (baseHue - HUE_SPREAD + rng() * HUE_SPREAD * 2) % 360,\n      sat - 5 + rng() * 10,\n      40 + rng() * 15,\n    ],\n    [\n      (baseHue - HUE_SPREAD + rng() * HUE_SPREAD * 2) % 360,\n      sat - 10 + rng() * 15,\n      60 + rng() * 15,\n    ],\n  ];\n};\n\ntype Cell = {\n  colorIndex: number;\n  phase: number;\n  brightness: number;\n  sparklePhase: number;\n};\n\n/** Build a grid with per-cell metadata */\nconst generateGrid = (hash: number): Cell[][] => {\n  const rng = createRng(hash + 1);\n  const grid: Cell[][] = [];\n\n  for (let y = 0; y < GRID_SIZE; y++) {\n    grid[y] = [];\n    for (let x = 0; x < GRID_SIZE; x++) {\n      grid[y][x] = {\n        brightness: 0.3 + rng() * 0.7,\n        colorIndex: Math.floor(rng() * 3),\n        phase: rng() * Math.PI * 2,\n        sparklePhase: rng() * Math.PI * 2,\n      };\n    }\n  }\n\n  return grid;\n};\n\nconst AgentAvatar = ({\n  seed,\n  size = 64,\n  animated = true,\n  className,\n  ...props\n}: AgentAvatarProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const rafRef = useRef<number>(0);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) {\n      return;\n    }\n\n    const dpr = window.devicePixelRatio || 1;\n    canvas.width = size * dpr;\n    canvas.height = size * dpr;\n    ctx.scale(dpr, dpr);\n\n    const hash = hashSeed(seed);\n    const palette = generatePalette(hash);\n    const grid = generateGrid(hash);\n    const cellSize = size / GRID_SIZE;\n    const half = size / 2;\n\n    const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let shouldAnimate = animated && !motionQuery.matches;\n\n    const draw = (time: number) => {\n      ctx.clearRect(0, 0, size, size);\n\n      // Scale pulse — whole avatar breathes\n      const scale = shouldAnimate\n        ? 1 + Math.sin(time * SCALE_PULSE_SPEED) * SCALE_PULSE_AMOUNT\n        : 1;\n\n      ctx.save();\n      ctx.translate(half, half);\n      ctx.scale(scale, scale);\n      ctx.translate(-half, -half);\n\n      // Clip to circle\n      ctx.beginPath();\n      ctx.arc(half, half, half, 0, Math.PI * 2);\n      ctx.clip();\n\n      // Dark background\n      ctx.fillStyle = \"#08080f\";\n      ctx.fillRect(0, 0, size, size);\n\n      // Global breathe offset for lightness\n      const breatheOffset = shouldAnimate\n        ? Math.sin(time * BREATHE_SPEED) * BREATHE_AMPLITUDE\n        : 0;\n\n      // Draw pixel grid\n      for (let y = 0; y < GRID_SIZE; y++) {\n        for (let x = 0; x < GRID_SIZE; x++) {\n          const cell = grid[y][x];\n          const [h, s, l] = palette[cell.colorIndex];\n\n          // Per-pixel pulse\n          const pulse = shouldAnimate\n            ? Math.sin(time * PULSE_SPEED + cell.phase) * PULSE_AMPLITUDE\n            : 0;\n\n          // Diagonal wave sweep\n          const waveDist = (x + y) / WAVE_LENGTH;\n          const wave = shouldAnimate\n            ? Math.sin(time * WAVE_SPEED + waveDist) * WAVE_AMPLITUDE\n            : 0;\n\n          // Sparkle — occasional bright flash\n          const sparkleVal = shouldAnimate\n            ? Math.sin(time * SPARKLE_SPEED + cell.sparklePhase)\n            : 0;\n          const sparkle =\n            sparkleVal > SPARKLE_THRESHOLD\n              ? ((sparkleVal - SPARKLE_THRESHOLD) / (1 - SPARKLE_THRESHOLD)) *\n                SPARKLE_BOOST\n              : 0;\n\n          const finalLight = Math.min(\n            90,\n            Math.max(\n              20,\n              (l + pulse + breatheOffset + wave + sparkle) * cell.brightness\n            )\n          );\n          const finalSat = Math.min(100, s + 5);\n\n          // Pixel glow — subtle shadow per cell\n          ctx.shadowColor = `hsl(${h}, ${finalSat}%, ${finalLight}%)`;\n          ctx.shadowBlur = cellSize * 0.45;\n\n          ctx.fillStyle = `hsl(${h}, ${finalSat}%, ${finalLight}%)`;\n          ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);\n        }\n      }\n\n      // Reset shadow before restore\n      ctx.shadowBlur = 0;\n      ctx.restore();\n\n      // Outer glow ring\n      const [gh, gs, gl] = palette[0];\n      ctx.save();\n      ctx.globalCompositeOperation = \"screen\";\n      ctx.shadowColor = `hsla(${gh}, ${gs}%, ${gl}%, 0.6)`;\n      ctx.shadowBlur = size * GLOW_RADIUS_RATIO;\n      ctx.beginPath();\n      ctx.arc(half, half, half - 1, 0, Math.PI * 2);\n      ctx.strokeStyle = `hsla(${gh}, ${gs}%, ${gl}%, 0.15)`;\n      ctx.lineWidth = 2;\n      ctx.stroke();\n      ctx.restore();\n\n      if (shouldAnimate) {\n        rafRef.current = requestAnimationFrame(draw);\n      }\n    };\n\n    const handleMotionChange = () => {\n      cancelAnimationFrame(rafRef.current);\n      shouldAnimate = animated && !motionQuery.matches;\n      if (shouldAnimate) {\n        rafRef.current = requestAnimationFrame(draw);\n      } else {\n        draw(0);\n      }\n    };\n\n    motionQuery.addEventListener(\"change\", handleMotionChange);\n\n    if (shouldAnimate) {\n      rafRef.current = requestAnimationFrame(draw);\n    } else {\n      draw(0);\n    }\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      motionQuery.removeEventListener(\"change\", handleMotionChange);\n    };\n  }, [seed, size, animated]);\n\n  return (\n    <canvas\n      aria-label={`Avatar for ${seed}`}\n      className={cn(\"rounded-full\", className)}\n      ref={canvasRef}\n      role=\"img\"\n      style={{ height: size, width: size }}\n      {...props}\n    />\n  );\n};\n\nexport default AgentAvatar;\n","path":"index.tsx","target":"components/smoothui/agent-avatar/index.tsx","type":"registry:ui"}],"name":"agent-avatar","registryDependencies":[],"title":"Agent Avatar","type":"registry:ui"}