{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-ai-ai-image-grid",
  "title": "Aurora AI Image Grid",
  "author": "jmagar <https://github.com/jmagar>",
  "description": "Aurora-native image candidate grid for AI generation review.",
  "dependencies": [
    "lucide-react@^0.487.0"
  ],
  "registryDependencies": [
    "@aurora/aurora-tokens",
    "@aurora/aurora-button"
  ],
  "files": [
    {
      "path": "registry/aurora/blocks/ai/elements/ai-image-grid.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Grid2x2, RotateCw, Sparkles } from \"lucide-react\"\nimport { Button } from \"@/registry/aurora/ui/button\"\n\n// AiImageGrid — a candidate-variation surface: a 2×2 (or N-up) grid of\n// AI-generated image tiles where exactly one can be selected. A header row\n// carries a grid glyph + caption and a \"Regenerate all\" affordance; the chosen\n// tile gets a cyan selection ring, an Axon-orange \"AI\" identity badge, a model\n// pill and a compact check button. Selection follows a single-select radiogroup pattern.\n//\n// Visual spec ported from the Claude Design source. Axon orange is the\n// AI/automation identity accent; cyan drives selection/focus. All values\n// reference the --aurora-* token layer.\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface AiImageGridProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\" | \"defaultValue\"> {\n  /** Image sources, one per candidate tile. */\n  images: string[]\n  /** Caption shown beside the grid glyph in the header (e.g. \"4 candidates · pick one\"). */\n  caption?: string\n  /** Model label shown in the selected tile's top-right pill (e.g. \"Imagen 3\"). */\n  model?: string\n  /** Controlled selected index. Use with `onSelect`. */\n  value?: number\n  /** Initial selected index for the uncontrolled case. Defaults to 0. */\n  defaultValue?: number\n  /** Selection handler — receives the index of the chosen tile. */\n  onSelect?: (index: number) => void\n  /** Regenerate-all handler — shows the \"Regenerate all\" button in the header. */\n  onRegenerate?: () => void\n  /** Accessible label for the radiogroup. Defaults to \"Image candidates\". */\n  label?: string\n}\n\n// ---------------------------------------------------------------------------\n// Tokens\n// ---------------------------------------------------------------------------\n\nconst AI_ORANGE = \"var(--axon-orange)\"\nconst CYAN = \"var(--aurora-accent-primary)\"\n\n// ---------------------------------------------------------------------------\n// Pills\n// ---------------------------------------------------------------------------\n\nfunction chipStyle(tone: \"neutral\" | \"ai\" = \"neutral\"): React.CSSProperties {\n  return {\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    gap: 5,\n    height: 22,\n    padding: \"0 8px\",\n    borderRadius: 7,\n    background:\n      tone === \"ai\"\n        ? \"color-mix(in srgb, var(--axon-orange) 14%, var(--aurora-page-bg))\"\n        : \"color-mix(in srgb, var(--aurora-page-bg) 64%, transparent)\",\n    border:\n      tone === \"ai\"\n        ? \"1px solid var(--axon-orange-border)\"\n        : \"1px solid color-mix(in srgb, var(--aurora-border-strong) 80%, transparent)\",\n    fontFamily: \"var(--aurora-font-sans)\",\n    fontSize: 11.5,\n    fontWeight: 600,\n    letterSpacing: \"var(--aurora-letter-label)\",\n    color: tone === \"ai\" ? AI_ORANGE : \"var(--aurora-text-primary)\",\n    whiteSpace: \"nowrap\",\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nconst AiImageGrid = (\n    { ref,\n      images,\n      caption,\n      model,\n      value,\n      defaultValue = 0,\n      onSelect,\n      onRegenerate,\n      label = \"Image candidates\",\n      className,\n      style,\n      ...props\n    }: AiImageGridProps & { ref?: React.Ref<HTMLDivElement> }\n  ) => {\n    const isControlled = value !== undefined\n    const [internal, setInternal] = React.useState(defaultValue)\n    const selected = isControlled ? (value as number) : internal\n\n    const select = React.useCallback(\n      (index: number) => {\n        if (!isControlled) setInternal(index)\n        onSelect?.(index)\n      },\n      [isControlled, onSelect]\n    )\n\n    const refs = React.useRef<Array<HTMLButtonElement | null>>([])\n\n    const onKeyDown = (e: React.KeyboardEvent) => {\n      const count = images.length\n      if (count === 0) return\n      let next: number | null = null\n      switch (e.key) {\n        case \"ArrowRight\":\n        case \"ArrowDown\":\n          next = (selected + 1) % count\n          break\n        case \"ArrowLeft\":\n        case \"ArrowUp\":\n          next = (selected - 1 + count) % count\n          break\n        case \"Home\":\n          next = 0\n          break\n        case \"End\":\n          next = count - 1\n          break\n        default:\n          return\n      }\n      e.preventDefault()\n      select(next)\n      refs.current[next]?.focus()\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={[\"aurora-ai-image-grid grid gap-3\", className].filter(Boolean).join(\" \")}\n        style={style}\n        {...props}\n      >\n        {/* Header: grid glyph + caption + regenerate-all */}\n        {caption || onRegenerate ? (\n          <div className=\"flex items-center justify-between gap-3\">\n            <span\n              className=\"inline-flex items-center\"\n              style={{\n                gap: 8,\n                fontFamily: \"var(--aurora-font-sans)\",\n                fontSize: 13.5,\n                fontWeight: 700,\n                color: \"var(--aurora-text-primary)\",\n              }}\n            >\n              <Grid2x2\n                className=\"size-4\"\n                aria-hidden\n                style={{ color: \"var(--aurora-text-muted)\" }}\n              />\n              {caption}\n            </span>\n            {onRegenerate ? (\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={onRegenerate}\n              >\n                <RotateCw className=\"size-3.5\" aria-hidden />\n                Regenerate All\n              </Button>\n            ) : null}\n          </div>\n        ) : null}\n\n        {/* Tile grid */}\n        <div\n          role=\"radiogroup\"\n          aria-label={label}\n          onKeyDown={onKeyDown}\n          style={{\n            display: \"grid\",\n            gridTemplateColumns: \"repeat(2, minmax(0, 1fr))\",\n            gap: 14,\n          }}\n        >\n          {images.map((src, i) => {\n            const isSelected = i === selected\n            return (\n              <Button\n                key={i}\n                type=\"button\"\n                variant=\"plain\"\n                size=\"unstyled\"\n                role=\"radio\"\n                aria-checked={isSelected}\n                aria-label={`Candidate ${i + 1}${isSelected ? \" (selected)\" : \"\"}`}\n                tabIndex={isSelected ? 0 : -1}\n                ref={(el) => {\n                  refs.current[i] = el\n                }}\n                onClick={() => select(i)}\n                className=\"aurora-ai-image-grid-tile group\"\n                style={{\n                  position: \"relative\",\n                  aspectRatio: \"1 / 1\",\n                  width: \"100%\",\n                  padding: 0,\n                  overflow: \"hidden\",\n                  cursor: \"pointer\",\n                  borderRadius: \"var(--aurora-radius-1)\",\n                  display: \"block\",\n                  background:\n                    \"radial-gradient(120% 120% at 50% 34%, color-mix(in srgb, var(--aurora-accent-primary) 24%, var(--aurora-panel-strong)) 0%, var(--aurora-panel-strong) 52%, var(--aurora-page-bg) 100%)\",\n                  border: isSelected\n                    ? `1.5px solid ${CYAN}`\n                    : \"1.5px solid var(--aurora-border-strong)\",\n                  boxShadow: isSelected\n                    ? \"var(--aurora-active-glow), var(--aurora-shadow-medium)\"\n                    : \"var(--aurora-shadow-medium), var(--aurora-highlight-medium)\",\n                  transition:\n                    \"border-color var(--motion-duration-fast, 160ms) var(--motion-ease-out, ease), box-shadow var(--motion-duration-fast, 160ms) var(--motion-ease-out, ease)\",\n                }}\n                onMouseEnter={(e) => {\n                  if (!isSelected) {\n                    e.currentTarget.style.borderColor = `color-mix(in srgb, ${CYAN} 55%, var(--aurora-border-strong))`\n                  }\n                }}\n                onMouseLeave={(e) => {\n                  if (!isSelected) {\n                    e.currentTarget.style.borderColor = \"var(--aurora-border-strong)\"\n                  }\n                }}\n              >\n                <img\n                  src={src}\n                  alt=\"\"\n                  aria-hidden\n                  className=\"absolute inset-0 size-full object-cover\"\n                  draggable={false}\n                />\n\n                {/* Selected tile chrome: AI badge + model pill */}\n                {isSelected ? (\n                  <div\n                    className=\"absolute inset-x-0 top-0 flex items-start justify-between\"\n                    style={{ padding: 10 }}\n                  >\n                    <span style={{ ...chipStyle(\"ai\"), gap: 4 }}>\n                      <Sparkles className=\"size-3\" aria-hidden style={{ color: AI_ORANGE }} />\n                      AI\n                    </span>\n                    {model ? <span style={chipStyle()}>{model}</span> : <span />}\n                  </div>\n                ) : null}\n\n                {/* Selection check */}\n                {isSelected ? (\n                  <span\n                    aria-hidden\n                    className=\"absolute grid place-items-center\"\n                    style={{\n                      bottom: 10,\n                      right: 10,\n                      width: 28,\n                      height: 28,\n                      borderRadius: 999,\n                      background: AI_ORANGE,\n                      color: \"var(--aurora-page-bg)\",\n                      boxShadow: \"0 0 12px color-mix(in srgb, var(--axon-orange) 50%, transparent)\",\n                    }}\n                  >\n                    <Check className=\"size-4\" strokeWidth={2.5} />\n                  </span>\n                ) : null}\n              </Button>\n            )\n          })}\n        </div>\n      </div>\n    )\n  }\nAiImageGrid.displayName = \"AiImageGrid\"\n\nexport { AiImageGrid }\nexport default AiImageGrid\n",
      "type": "registry:component",
      "target": "@components/aurora/ai/ai-image-grid.tsx"
    }
  ],
  "meta": {
    "namespace": "@aurora",
    "style": "aurora",
    "family": "block",
    "requiresTokens": true,
    "sourcePath": "registry/aurora/blocks/ai/elements/ai-image-grid.tsx",
    "installTarget": "@components/aurora/ai/ai-image-grid.tsx"
  },
  "docs": "Aurora block that installs Aurora AI Image Grid.\n\nInstall `aurora-tokens` and `aurora-button` first; the shadcn CLI resolves these automatically through the @aurora registry namespace.\n\nDefault install target: `@components/aurora/ai/ai-image-grid.tsx`.\n\nRegistry dependencies: `aurora-tokens`, `aurora-button`.",
  "categories": [
    "aurora",
    "block",
    "ai"
  ],
  "type": "registry:block"
}