{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cover-art",
  "title": "Cover Art",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Cover pictures drawn from a seed in your theme's chart colors: color fields, rings, bars, dots or waves. The same seed always draws the same picture, so an item without an image still looks finished.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/cover-art.tsx",
      "content": "// Ballmac UI: Cover Art. https://ui.ballmac.com/components/cover-art\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype CoverArtVariant = \"mesh\" | \"rings\" | \"bars\" | \"dots\" | \"waves\"\n\ntype CoverArtProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Anything stable: an id, a name, a slug. The same seed always draws the same picture. */\n  seed: string | number\n  /** Which picture to draw. Leave it out (or \"auto\") to let the seed choose among the five. */\n  variant?: CoverArtVariant | \"auto\"\n  /** Lay a faint grid over the picture. */\n  grid?: boolean\n}\n\nconst variants: CoverArtVariant[] = [\"mesh\", \"rings\", \"bars\", \"dots\", \"waves\"]\nconst tones = [\"var(--chart-1)\", \"var(--chart-2)\", \"var(--chart-3)\", \"var(--chart-4)\", \"var(--chart-5)\"]\n\n/** A small deterministic random source (mulberry32). Integer maths only, so the server and every browser draw the same numbers. */\nfunction random(seed: string | number) {\n  let h = 2166136261\n  for (const ch of String(seed)) h = Math.imul(h ^ ch.charCodeAt(0), 16777619)\n  let a = h >>> 0\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0\n    let t = Math.imul(a ^ (a >>> 15), 1 | a)\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t\n    return (((t ^ (t >>> 14)) >>> 0) % 100000) / 100000\n  }\n}\n\n/** Rounded so the markup is short and identical wherever it renders. */\nconst n = (v: number) => Math.round(v * 10) / 10\n\n/**\n * A cover picture drawn from a seed, in the theme's chart colors: soft color fields, rings, bars, dots or waves. Use it where an\n * item has no image of its own: a project card, a profile cover, a channel or agent avatar. It fills its parent, never takes\n * focus, and is hidden from assistive technology.\n */\nfunction CoverArt({ seed, variant = \"auto\", grid = false, className, ...props }: CoverArtProps) {\n  const rnd = random(seed)\n  const pick = <T,>(list: T[]) => list[Math.floor(rnd() * list.length)]!\n  const kind = variant === \"auto\" ? pick(variants) : variant\n  const first = Math.floor(rnd() * tones.length)\n  const tone = tones[first]!\n  const alt = tones[(first + 1 + Math.floor(rnd() * 3)) % tones.length]!\n  const third = tones[(first + 2 + Math.floor(rnd() * 2)) % tones.length]!\n\n  let shapes: React.ReactNode = null\n  let field: React.CSSProperties[\"background\"] = `linear-gradient(135deg, color-mix(in oklab, ${tone} 22%, transparent), color-mix(in oklab, ${alt} 14%, transparent))`\n\n  if (kind === \"mesh\") {\n    // one blob in each third of the width, so a wide cover is never empty on one side\n    const slots = [0, 1, 2]\n    const at = (slot: number) => `${Math.round(slot * 34 + rnd() * 32)}% ${Math.round(8 + rnd() * 84)}%`\n    field = [\n      `radial-gradient(60% 90% at ${at(slots[0]!)}, color-mix(in oklab, ${tone} 60%, transparent), transparent 70%)`,\n      `radial-gradient(55% 85% at ${at(slots[1]!)}, color-mix(in oklab, ${alt} 50%, transparent), transparent 70%)`,\n      `radial-gradient(70% 90% at ${at(slots[2]!)}, color-mix(in oklab, ${third} 42%, transparent), transparent 72%)`,\n    ].join(\",\")\n  } else if (kind === \"rings\") {\n    const cx = 190 + rnd() * 90\n    const cy = 50 + rnd() * 80\n    shapes = (\n      <>\n        {[26, 52, 80, 112, 148].map((r, i) => (\n          <circle key={r} cx={n(cx)} cy={n(cy)} r={r} fill=\"none\" stroke={i % 2 ? alt : tone} strokeWidth={i === 0 ? 2 : 1.4} strokeOpacity={n(0.7 - i * 0.12)} />\n        ))}\n        <circle cx={n(cx)} cy={n(cy)} r=\"9\" fill={tone} fillOpacity=\"0.9\" />\n        <circle cx={n(cx - 62 - rnd() * 20)} cy={n(cy + 30 + rnd() * 20)} r=\"5\" fill={third} fillOpacity=\"0.8\" />\n      </>\n    )\n  } else if (kind === \"bars\") {\n    const count = 11 + Math.floor(rnd() * 4)\n    const width = 320 / count\n    const heights = Array.from({ length: count }, (_, i) => n(36 + rnd() * 96 + i * 2))\n    const peak = heights.indexOf(Math.max(...heights))\n    shapes = (\n      <>\n        {heights.map((h, i) => (\n          <rect key={i} x={n(i * width + width * 0.2)} y={n(180 - h)} width={n(width * 0.6)} height={h} rx={n(width * 0.3)} fill={i === peak ? alt : tone} fillOpacity={i === peak ? 0.95 : n(0.28 + (i / count) * 0.4)} />\n        ))}\n      </>\n    )\n  } else if (kind === \"dots\") {\n    const fx = 80 + rnd() * 170\n    const fy = 40 + rnd() * 100\n    const dots: React.ReactNode[] = []\n    for (let row = 0; row < 8; row++)\n      for (let col = 0; col < 15; col++) {\n        const x = 12 + col * 21.4\n        const y = 14 + row * 22.5\n        // size falls off with distance from the focus, using squares instead of a square root\n        const d2 = ((x - fx) * (x - fx) + (y - fy) * (y - fy)) / 9000\n        const r = Math.max(1.2, 6.2 - d2 * 4.2)\n        dots.push(<circle key={`${row}-${col}`} cx={n(x)} cy={n(y)} r={n(r)} fill={d2 < 0.7 ? alt : tone} fillOpacity={n(Math.max(0.18, 0.9 - d2 * 0.55))} />)\n      }\n    shapes = dots\n  } else {\n    const wave = (y: number, amp: number) => {\n      const a = n(y + (rnd() - 0.5) * amp)\n      const b = n(y + (rnd() - 0.5) * amp * 1.6)\n      const c = n(y + (rnd() - 0.5) * amp)\n      return `M0 ${n(y)} C 80 ${a}, 120 ${b}, 175 ${c} S 270 ${n(y + (rnd() - 0.5) * amp)}, 320 ${n(y + (rnd() - 0.5) * amp)}`\n    }\n    shapes = (\n      <>\n        {[\n          { y: 62, w: 2.4, c: tone, o: 0.9 },\n          { y: 92, w: 1.8, c: alt, o: 0.7 },\n          { y: 120, w: 1.4, c: third, o: 0.55 },\n          { y: 146, w: 1.1, c: tone, o: 0.35 },\n        ].map((l) => (\n          <path key={l.y} d={wave(l.y, 56)} fill=\"none\" stroke={l.c} strokeWidth={l.w} strokeOpacity={l.o} strokeLinecap=\"round\" />\n        ))}\n      </>\n    )\n  }\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-slot=\"cover-art\"\n      data-variant={kind}\n      className={cn(\"bg-muted/60 pointer-events-none relative isolate size-full overflow-hidden\", className)}\n      {...props}\n    >\n      <div className=\"absolute inset-0\" style={{ background: field }} />\n      {shapes ? (\n        <svg viewBox=\"0 0 320 180\" preserveAspectRatio=\"xMidYMid slice\" className=\"absolute inset-0 size-full\">\n          {shapes}\n        </svg>\n      ) : null}\n      {grid ? (\n        <svg viewBox=\"0 0 320 180\" preserveAspectRatio=\"xMidYMid slice\" className=\"text-foreground absolute inset-0 size-full opacity-[0.06]\">\n          <path d={`${Array.from({ length: 9 }, (_, i) => `M${(i + 1) * 32} 0V180`).join(\"\")}${Array.from({ length: 5 }, (_, i) => `M0 ${(i + 1) * 30}H320`).join(\"\")}`} stroke=\"currentColor\" strokeWidth=\"1\" vectorEffect=\"non-scaling-stroke\" fill=\"none\" />\n        </svg>\n      ) : null}\n    </div>\n  )\n}\n\nexport { CoverArt, type CoverArtProps, type CoverArtVariant }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/cover-art.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "cover",
      "artwork",
      "generative",
      "placeholder",
      "avatar",
      "thumbnail",
      "gradient",
      "seed"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "<CoverArt seed={project.id} /> fills its parent with a picture chosen and drawn from the seed; variant: 'mesh' | 'rings' | 'bars' | 'dots' | 'waves' | 'auto'; grid adds a faint grid. Put it in a sized container (an aspect-video div, a size-10 rounded avatar). It is decorative and hidden from screen readers, so keep the item's name as text next to it.",
      "whenToUse": [
        "The fallback for a card, channel, project or agent that has no image",
        "A profile cover or a thumbnail while images load"
      ],
      "whenNotToUse": [
        "A real photograph or logo (use media)",
        "An animated hero background (use aurora-background or beams-background)"
      ],
      "composesWith": [
        "media",
        "card",
        "avatar"
      ],
      "a11y": [
        {
          "keys": "None",
          "action": "Decorative: aria-hidden, no focus. Never the only way to tell items apart."
        }
      ],
      "customization": [
        "seed picks the picture and the chart colors it uses, so a theme change recolors every picture",
        "className sets size and corners: <CoverArt seed={id} className=\"aspect-video rounded-xl\" />",
        "Use it as the fallback of Media: <Media media={cover} fallback={<CoverArt seed={id} />} fill />"
      ]
    },
    "examples": [
      "cover-art-demo",
      "cover-art-seeds"
    ],
    "url": "https://ui.ballmac.com/components/cover-art"
  },
  "categories": [
    "backgrounds"
  ],
  "type": "registry:ui"
}