# AmroUI generation

Installable AmroUI product component source.

## Installation

```bash
npx shadcn@latest add https://amro-ui.vercel.app/r/amro-ui-generation.json
```

[Registry JSON](https://amro-ui.vercel.app/r/amro-ui-generation.json)

## Source

### source/components/generation.tsx

```tsx
import {
  ArrowRight,
  Check,
  ChevronRight,
  Cloud,
  Code2,
  Copy,
  Crop,
  Download,
  ExternalLink,
  FolderHeart,
  History,
  Image as ImageIcon,
  Layers3,
  Palette,
  RefreshCw,
  Server,
  ShieldCheck,
  Sparkles,
  Type,
  Upload,
  WandSparkles,
  X
} from "lucide-react";
import { useState, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "../lib/cn.js";
import { Badge } from "./badge.js";
import { Button } from "./button.js";
import { Card } from "./card.js";
import { EmptyNotice, FeaturePanel, StatusIndicator } from "./feature-primitives.js";
import { Input, RangeInput, Select, Textarea } from "./input.js";

function getAccessibleText(value: ReactNode, fallback: string): string {
  return typeof value === "string" || typeof value === "number" ? String(value) : fallback;
}

export interface ImageGenerationBrief {
  prompt: string;
  negativePrompt: string;
  style: string;
  width: number;
  height: number;
}
export interface ImagePromptWorkbenchProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onSubmit" | "defaultValue"
> {
  defaultValue?: Partial<ImageGenerationBrief>;
  referenceCount?: number;
  onSubmit?: (brief: ImageGenerationBrief) => void;
  onAddReference?: () => void;
}
export function ImagePromptWorkbench({
  className,
  defaultValue,
  onAddReference,
  onSubmit,
  referenceCount = 2,
  ...props
}: ImagePromptWorkbenchProps) {
  const [brief, setBrief] = useState<ImageGenerationBrief>({
    prompt:
      defaultValue?.prompt ??
      "Editorial hero showing a governed AI agent coordinating customer operations, calm premium lighting, clear human approval checkpoint",
    negativePrompt:
      defaultValue?.negativePrompt ?? "text, logos, generic robot, cluttered dashboard",
    style: defaultValue?.style ?? "Editorial product realism",
    width: defaultValue?.width ?? 1600,
    height: defaultValue?.height ?? 900
  });
  const update = <K extends keyof ImageGenerationBrief>(key: K, value: ImageGenerationBrief[K]) =>
    setBrief((current) => ({ ...current, [key]: value }));
  return (
    <form
      className={cn("amro-image-workbench", className)}
      onSubmit={(event) => {
        event.preventDefault();
        if (brief.prompt.trim()) onSubmit?.(brief);
      }}
      {...props}
    >
      <FeaturePanel
        eyebrow="Image prompt"
        title="Create a production-ready visual"
        description="Control composition, exclusions, style, dimensions, and references."
        status={<Badge tone="neutral">{referenceCount} references</Badge>}
      >
        <label>
          <span>Prompt</span>
          <Textarea
            onChange={(event) => update("prompt", event.target.value)}
            value={brief.prompt}
          />
          <small>
            {brief.prompt.length} characters · describe subject, composition, mood, and use
          </small>
        </label>
        <label>
          <span>Negative prompt</span>
          <Textarea
            onChange={(event) => update("negativePrompt", event.target.value)}
            value={brief.negativePrompt}
          />
        </label>
        <div>
          <Input
            label="Style"
            value={brief.style}
            onChange={(event) => update("style", event.target.value)}
          />
          <Input
            label="Width"
            min="256"
            step="64"
            type="number"
            value={brief.width}
            onChange={(event) => update("width", Number(event.target.value))}
          />
          <Input
            label="Height"
            min="256"
            step="64"
            type="number"
            value={brief.height}
            onChange={(event) => update("height", Number(event.target.value))}
          />
        </div>
        <footer>
          {onAddReference && (
            <Button onClick={onAddReference} variant="secondary">
              <Upload aria-hidden="true" />
              Add reference
            </Button>
          )}
          <span>
            {brief.width} × {brief.height} · {Math.round((brief.width / brief.height) * 100) / 100}
            :1
          </span>
          <Button disabled={!brief.prompt.trim()} type="submit">
            Generate image
            <Sparkles aria-hidden="true" />
          </Button>
        </footer>
      </FeaturePanel>
    </form>
  );
}

export interface PromptBrief {
  purpose: string;
  audience: string;
  placement: string;
  brand: string;
  direction: string;
}
export interface PromptBriefBuilderProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onSubmit" | "defaultValue"
> {
  defaultValue?: Partial<PromptBrief>;
  onSubmit?: (brief: PromptBrief) => void;
}
export function PromptBriefBuilder({
  className,
  defaultValue,
  onSubmit,
  ...props
}: PromptBriefBuilderProps) {
  const [brief, setBrief] = useState<PromptBrief>({
    purpose: defaultValue?.purpose ?? "Launch campaign hero",
    audience: defaultValue?.audience ?? "AI operations leaders",
    placement: defaultValue?.placement ?? "Website hero and LinkedIn campaign",
    brand: defaultValue?.brand ?? "Amro",
    direction: defaultValue?.direction ?? "Trustworthy, outcome-led, premium, no sci-fi clichés"
  });
  const update = (key: keyof PromptBrief, value: string) =>
    setBrief((current) => ({ ...current, [key]: value }));
  const complete = [
    brief.purpose,
    brief.audience,
    brief.placement,
    brief.brand,
    brief.direction
  ].every((value) => value.trim().length > 0);
  return (
    <form
      className={cn("amro-prompt-brief", className)}
      onSubmit={(event) => {
        event.preventDefault();
        if (complete) onSubmit?.(brief);
      }}
      {...props}
    >
      <FeaturePanel
        eyebrow="Creative brief"
        title="Turn campaign intent into visual direction"
        status={
          <StatusIndicator
            label={complete ? "Ready" : "Incomplete"}
            status={complete ? "success" : "warning"}
          />
        }
      >
        <div>
          {(["purpose", "audience", "placement", "brand", "direction"] as const).map((key) => (
            <Input
              key={key}
              label={
                key === "direction" ? "Visual direction" : `${key[0]?.toUpperCase()}${key.slice(1)}`
              }
              value={brief[key]}
              onChange={(event) => update(key, event.target.value)}
            />
          ))}
        </div>
        <footer>
          <span>
            <WandSparkles aria-hidden="true" />
            AmroVisionAI will enhance this into a production prompt.
          </span>
          <Button disabled={!complete} type="submit">
            Build prompt
            <ArrowRight aria-hidden="true" />
          </Button>
        </footer>
      </FeaturePanel>
    </form>
  );
}

export interface PromptEnhancementDiffProps extends HTMLAttributes<HTMLDivElement> {
  original?: string;
  enhanced?: string;
  additions?: readonly string[];
  onAccept?: (enhanced: string) => void;
  onRevert?: () => void;
}
export function PromptEnhancementDiff({
  additions = [
    "16:9 editorial composition",
    "negative space for headline",
    "brand teal/cyan practical lighting",
    "realistic human oversight"
  ],
  className,
  enhanced = "Editorial 16:9 campaign hero: a governed AI operations workspace coordinating visible workflow steps, with a human approval checkpoint, premium dark-navy environment, practical teal and cyan light, credible people, negative space on the left for a headline, refined product realism, no text or logos.",
  onAccept,
  onRevert,
  original = "AI agent coordinating customer operations with human approval",
  ...props
}: PromptEnhancementDiffProps) {
  return (
    <FeaturePanel
      className={cn("amro-prompt-diff", className)}
      eyebrow="Prompt enhancement"
      title="Production detail added without changing intent"
      {...props}
    >
      <div>
        <section>
          <Badge tone="neutral">Your prompt</Badge>
          <p>{original}</p>
        </section>
        <ArrowRight aria-hidden="true" />
        <section>
          <Badge tone="success">Enhanced prompt</Badge>
          <p>{enhanced}</p>
        </section>
      </div>
      <ul>
        {additions.map((addition) => (
          <li key={addition}>
            <Check aria-hidden="true" />
            {addition}
          </li>
        ))}
      </ul>
      <footer>
        {onRevert && (
          <Button onClick={onRevert} variant="tertiary">
            Use original
          </Button>
        )}
        {onAccept && <Button onClick={() => onAccept(enhanced)}>Use enhanced prompt</Button>}
      </footer>
    </FeaturePanel>
  );
}

export interface ModelProvider {
  id: string;
  name: ReactNode;
  status: "healthy" | "degraded" | "offline";
  latencyMs?: number;
  active?: boolean;
  fallbackOrder?: number;
}
export interface ModelProviderStripProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  providers?: readonly ModelProvider[];
  onSelect?: (provider: ModelProvider) => void;
}
const modelProviders: readonly ModelProvider[] = [
  {
    id: "primary",
    name: "Amro Render Pro",
    status: "healthy",
    latencyMs: 820,
    active: true,
    fallbackOrder: 1
  },
  { id: "fallback-a", name: "Vision Studio", status: "healthy", latencyMs: 1040, fallbackOrder: 2 },
  { id: "fallback-b", name: "Canvas XL", status: "degraded", latencyMs: 1880, fallbackOrder: 3 }
];
export function ModelProviderStrip({
  className,
  onSelect,
  providers = modelProviders,
  ...props
}: ModelProviderStripProps) {
  return (
    <div className={cn("amro-provider-strip", className)} {...props}>
      <span>
        <Cloud aria-hidden="true" />
        <strong>Model routing</strong>
      </span>
      {providers.map((provider) => (
        <button
          data-active={provider.active}
          key={provider.id}
          onClick={() => onSelect?.(provider)}
          type="button"
        >
          <span>
            <strong>{provider.name}</strong>
            <small>
              {provider.active ? "Active provider" : `Fallback ${provider.fallbackOrder}`}
            </small>
          </span>
          <StatusIndicator
            label={`${provider.status}${provider.latencyMs ? ` · ${provider.latencyMs} ms` : ""}`}
            status={
              provider.status === "healthy"
                ? "online"
                : provider.status === "degraded"
                  ? "warning"
                  : "offline"
            }
          />
        </button>
      ))}
    </div>
  );
}

export interface FallbackEventNoticeProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  from?: ReactNode;
  to?: ReactNode;
  reason?: ReactNode;
  impact?: ReactNode;
  title?: ReactNode;
}
export function FallbackEventNotice({
  className,
  from = "Amro Render Pro",
  impact = "Your prompt, dimensions, seed, and storage destination were preserved.",
  reason = "The primary provider exceeded the rendering timeout.",
  title = "A fallback provider completed this image",
  to = "Vision Studio",
  ...props
}: FallbackEventNoticeProps) {
  return (
    <div className={cn("amro-fallback-notice", className)} role="status" {...props}>
      <RefreshCw aria-hidden="true" />
      <span>
        <strong>{title}</strong>
        <small>{reason}</small>
        <span>
          <Badge tone="neutral">{from}</Badge>
          <ArrowRight aria-hidden="true" />
          <Badge tone="success">{to}</Badge>
        </span>
        <p>{impact}</p>
      </span>
    </div>
  );
}

export interface GeneratedImageAsset {
  id: string;
  src?: string;
  alt?: string;
  prompt?: string;
  width?: number;
  height?: number;
  provider?: ReactNode;
}
export type GenerationCanvasState =
  "idle" | "loading" | "preview" | "result" | "error" | "moderation";
export interface GenerationCanvasProps extends HTMLAttributes<HTMLDivElement> {
  state?: GenerationCanvasState;
  asset?: GeneratedImageAsset;
  progress?: number;
  previewLabel?: ReactNode;
  error?: ReactNode;
  moderationReason?: ReactNode;
  onRetry?: () => void;
  onEditPrompt?: () => void;
  onUseAsset?: (asset: GeneratedImageAsset) => void;
}
const sampleGeneratedAsset: GeneratedImageAsset = {
  id: "asset",
  alt: "Governed AI operations campaign visual",
  prompt: "Governed AI workspace with a human approval checkpoint",
  width: 1600,
  height: 900,
  provider: "Amro Render Pro"
};
export function GenerationCanvas({
  asset = sampleGeneratedAsset,
  className,
  error = "The provider could not complete this render.",
  moderationReason = "The request needs revision before it can be generated safely.",
  onEditPrompt,
  onRetry,
  onUseAsset,
  previewLabel = "Composition preview",
  progress = 64,
  state = "result",
  ...props
}: GenerationCanvasProps) {
  return (
    <div
      className={cn("amro-generation-canvas", `amro-generation-canvas--${state}`, className)}
      {...props}
    >
      <header>
        <span>
          <strong>Generation canvas</strong>
          <small>
            {state === "loading"
              ? "Rendering and storing a durable asset"
              : state === "result"
                ? `${asset.width} × ${asset.height} · ${getAccessibleText(asset.provider, "AI provider")}`
                : previewLabel}
          </small>
        </span>
        <StatusIndicator
          label={state}
          status={
            state === "result"
              ? "success"
              : state === "loading"
                ? "loading"
                : state === "moderation" || state === "error"
                  ? "error"
                  : "idle"
          }
        />
      </header>
      <div className="amro-generation-canvas__stage">
        {state === "result" && asset.src ? (
          <img alt={asset.alt ?? "Generated visual"} src={asset.src} />
        ) : state === "result" || state === "preview" ? (
          <div className="amro-generation-canvas__art">
            <Sparkles aria-hidden="true" />
            <span>Governed AI operations</span>
            <small>{asset.prompt}</small>
          </div>
        ) : state === "loading" ? (
          <div className="amro-generation-canvas__loading">
            <Sparkles aria-hidden="true" />
            <strong>Rendering visual…</strong>
            <progress max="100" value={progress}>
              {progress}%
            </progress>
            <small>{progress}% · provider output will be stored automatically</small>
          </div>
        ) : state === "error" || state === "moderation" ? (
          <div className="amro-generation-canvas__error">
            <ShieldCheck aria-hidden="true" />
            <strong>{state === "moderation" ? "Generation paused" : "Render failed"}</strong>
            <p>{state === "moderation" ? moderationReason : error}</p>
            <span>
              {onEditPrompt && (
                <Button onClick={onEditPrompt} variant="secondary">
                  Edit prompt
                </Button>
              )}
              {onRetry && state === "error" && <Button onClick={onRetry}>Try again</Button>}
            </span>
          </div>
        ) : (
          <EmptyNotice
            title="Your visual starts here"
            description="Add a production prompt, dimensions, and references to begin."
          />
        )}
      </div>
      {state === "result" && (
        <footer>
          <span>
            <Cloud aria-hidden="true" />
            Asset stored with reusable URL
          </span>
          {onUseAsset && (
            <Button onClick={() => onUseAsset(asset)}>
              Use asset
              <ArrowRight aria-hidden="true" />
            </Button>
          )}
        </footer>
      )}
    </div>
  );
}

export interface ImageVariant extends GeneratedImageAsset {
  label?: ReactNode;
  selected?: boolean;
}
export interface ImageVariantGridProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  variants?: readonly ImageVariant[];
  value?: string;
  onSelect?: (variant: ImageVariant) => void;
  onCompare?: (variants: readonly ImageVariant[]) => void;
}
const imageVariants: readonly ImageVariant[] = [
  { id: "a", label: "A · Balanced", prompt: "Balanced composition", width: 1600, height: 900 },
  {
    id: "b",
    label: "B · Human-led",
    prompt: "Human approval emphasized",
    width: 1600,
    height: 900
  },
  {
    id: "c",
    label: "C · Product-led",
    prompt: "Product workflow emphasized",
    width: 1600,
    height: 900
  },
  { id: "d", label: "D · Editorial", prompt: "Editorial abstraction", width: 1600, height: 900 }
];
export function ImageVariantGrid({
  className,
  onCompare,
  onSelect,
  value: controlled,
  variants = imageVariants,
  ...props
}: ImageVariantGridProps) {
  const [internal, setInternal] = useState(variants[0]?.id);
  const value = controlled ?? internal;
  return (
    <div className={cn("amro-variant-grid", className)} {...props}>
      <header>
        <span>
          <strong>Generated variants</strong>
          <small>Select one to continue</small>
        </span>
        <Button
          disabled={variants.length < 2}
          onClick={() => onCompare?.(variants)}
          size="sm"
          variant="secondary"
        >
          Compare
        </Button>
      </header>
      <div>
        {variants.map((variant, index) => (
          <button
            aria-pressed={value === variant.id}
            key={variant.id}
            onClick={() => {
              setInternal(variant.id);
              onSelect?.(variant);
            }}
            type="button"
          >
            <span className="amro-variant-grid__art" data-variant={index}>
              <Sparkles aria-hidden="true" />
            </span>
            <span>
              <strong>{variant.label ?? `Variant ${index + 1}`}</strong>
              <small>{variant.prompt}</small>
            </span>
            {value === variant.id && (
              <Badge tone="success">
                <Check aria-hidden="true" />
                Selected
              </Badge>
            )}
          </button>
        ))}
      </div>
    </div>
  );
}

export type VariationAction =
  "more-like-this" | "stronger-brand" | "alternate-framing" | "new-palette";
export interface VariationControlsProps extends HTMLAttributes<HTMLDivElement> {
  disabled?: boolean;
  onAction?: (action: VariationAction, strength: number) => void;
}
const variationActions: readonly {
  id: VariationAction;
  label: string;
  detail: string;
  icon: ReactNode;
}[] = [
  {
    id: "more-like-this",
    label: "More like this",
    detail: "Preserve subject and art direction",
    icon: <Layers3 aria-hidden="true" />
  },
  {
    id: "stronger-brand",
    label: "Stronger brand",
    detail: "Increase palette and style influence",
    icon: <ShieldCheck aria-hidden="true" />
  },
  {
    id: "alternate-framing",
    label: "Alternate framing",
    detail: "Change camera and composition",
    icon: <Crop aria-hidden="true" />
  },
  {
    id: "new-palette",
    label: "New palette",
    detail: "Keep content, shift colour system",
    icon: <Palette aria-hidden="true" />
  }
];
export function VariationControls({
  className,
  disabled = false,
  onAction,
  ...props
}: VariationControlsProps) {
  const [strength, setStrength] = useState(70);
  return (
    <div className={cn("amro-variation-controls", className)} {...props}>
      <header>
        <span>
          <strong>Create variations</strong>
          <small>Control how much of the selected visual is preserved</small>
        </span>
        <label>
          Similarity <strong>{strength}%</strong>
          <RangeInput
            max="100"
            min="10"
            onChange={(event) => setStrength(Number(event.target.value))}
            value={strength}
          />
        </label>
      </header>
      <div>
        {variationActions.map((action) => (
          <button
            disabled={disabled}
            key={action.id}
            onClick={() => onAction?.(action.id, strength)}
            type="button"
          >
            <span>{action.icon}</span>
            <span>
              <strong>{action.label}</strong>
              <small>{action.detail}</small>
            </span>
            <ChevronRight aria-hidden="true" />
          </button>
        ))}
      </div>
    </div>
  );
}

export interface ReferenceImage {
  id: string;
  src?: string;
  name: ReactNode;
  influence: number;
  role?: "style" | "subject" | "composition" | "palette";
}
export interface ReferenceImageTrayProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  references?: readonly ReferenceImage[];
  onChange?: (references: readonly ReferenceImage[]) => void;
  onAdd?: () => void;
}
const referenceImages: readonly ReferenceImage[] = [
  { id: "brand", name: "Amro campaign style", influence: 78, role: "style" },
  { id: "product", name: "Product workspace", influence: 62, role: "composition" }
];
export function ReferenceImageTray({
  className,
  onAdd,
  onChange,
  references = referenceImages,
  ...props
}: ReferenceImageTrayProps) {
  const [items, setItems] = useState([...references]);
  const update = (id: string, patch: Partial<ReferenceImage>) => {
    const next = items.map((item) => (item.id === id ? { ...item, ...patch } : item));
    setItems(next);
    onChange?.(next);
  };
  const remove = (id: string) => {
    const next = items.filter((item) => item.id !== id);
    setItems(next);
    onChange?.(next);
  };
  return (
    <div className={cn("amro-reference-tray", className)} {...props}>
      <header>
        <span>
          <strong>Reference images</strong>
          <small>{items.length} inputs · influence is explicit</small>
        </span>
        {onAdd && (
          <Button onClick={onAdd} size="sm" variant="secondary">
            <Upload aria-hidden="true" />
            Add
          </Button>
        )}
      </header>
      <div>
        {items.map((item, index) => (
          <article key={item.id}>
            <span className="amro-reference-tray__thumb" data-variant={index}>
              {item.src ? <img alt="" src={item.src} /> : <ImageIcon aria-hidden="true" />}
            </span>
            <span>
              <strong>{item.name}</strong>
              <label>
                <span>Influence {item.influence}%</span>
                <RangeInput
                  max="100"
                  min="0"
                  onChange={(event) => update(item.id, { influence: Number(event.target.value) })}
                  value={item.influence}
                />
              </label>
            </span>
            <Select
              aria-label={`Role for ${getAccessibleText(item.name, "reference image")}`}
              onChange={(event) =>
                update(item.id, {
                  role: event.target.value as NonNullable<ReferenceImage["role"]>
                })
              }
              value={item.role}
            >
              <option value="style">Style</option>
              <option value="subject">Subject</option>
              <option value="composition">Composition</option>
              <option value="palette">Palette</option>
            </Select>
            <button
              aria-label={`Remove ${getAccessibleText(item.name, "reference image")}`}
              onClick={() => remove(item.id)}
              type="button"
            >
              <X aria-hidden="true" />
            </button>
          </article>
        ))}
      </div>
    </div>
  );
}

export interface StyleRecipe {
  id: string;
  name: ReactNode;
  description?: ReactNode;
  lighting: ReactNode;
  composition: ReactNode;
  texture: ReactNode;
  palette: readonly string[];
}
export interface StyleRecipeCardProps extends HTMLAttributes<HTMLDivElement> {
  recipe?: StyleRecipe;
  selected?: boolean;
  onUse?: (recipe: StyleRecipe) => void;
  onEdit?: (recipe: StyleRecipe) => void;
}
const sampleRecipe: StyleRecipe = {
  id: "editorial",
  name: "Amro editorial realism",
  description: "Premium product storytelling without sci-fi clichés.",
  lighting: "Soft practical teal/cyan",
  composition: "Human-led, negative space",
  texture: "Clean photographic detail",
  palette: ["accent-teal", "accent-cyan", "deep-navy"]
};
export function StyleRecipeCard({
  className,
  onEdit,
  onUse,
  recipe = sampleRecipe,
  selected = false,
  ...props
}: StyleRecipeCardProps) {
  return (
    <Card
      className={cn("amro-style-recipe", selected && "amro-style-recipe--selected", className)}
      {...props}
    >
      <div className="amro-style-recipe__preview">
        <Palette aria-hidden="true" />
        <span>
          {recipe.palette.map((color) => (
            <i data-color={color} key={color} />
          ))}
        </span>
      </div>
      <header>
        <span>
          <strong>{recipe.name}</strong>
          <small>{recipe.description}</small>
        </span>
        {selected && (
          <Badge tone="success">
            <Check aria-hidden="true" />
            Applied
          </Badge>
        )}
      </header>
      <dl>
        <div>
          <dt>Lighting</dt>
          <dd>{recipe.lighting}</dd>
        </div>
        <div>
          <dt>Composition</dt>
          <dd>{recipe.composition}</dd>
        </div>
        <div>
          <dt>Texture</dt>
          <dd>{recipe.texture}</dd>
        </div>
      </dl>
      <footer>
        {onEdit && (
          <Button onClick={() => onEdit(recipe)} variant="tertiary">
            Edit recipe
          </Button>
        )}
        {onUse && (
          <Button onClick={() => onUse(recipe)}>{selected ? "Use again" : "Use recipe"}</Button>
        )}
      </footer>
    </Card>
  );
}

export interface BrandVisualProfileData {
  name: ReactNode;
  palette: readonly string[];
  logoRule: ReactNode;
  photography: ReactNode;
  exclusions: readonly ReactNode[];
  strength?: number;
}
export interface BrandVisualProfileProps extends HTMLAttributes<HTMLDivElement> {
  profile?: BrandVisualProfileData;
  onEdit?: () => void;
}
const visualProfile: BrandVisualProfileData = {
  name: "Amro visual system",
  palette: ["accent-teal", "accent-cyan", "deep-navy", "soft-mist"],
  logoRule: "Clear space equal to mark width; never place over detail",
  photography: "Credible people, practical work, calm premium light",
  exclusions: ["Generic robots", "Neon sci-fi cities", "Unapproved logo variants"],
  strength: 82
};
export function BrandVisualProfile({
  className,
  onEdit,
  profile = visualProfile,
  ...props
}: BrandVisualProfileProps) {
  return (
    <FeaturePanel
      className={cn("amro-brand-visual", className)}
      eyebrow="Brand visual profile"
      title={profile.name}
      description="Applied to prompts, validation, and variations."
      status={<Badge tone="success">{profile.strength}% influence</Badge>}
      {...props}
    >
      <div className="amro-brand-visual__palette">
        {profile.palette.map((color) => (
          <span data-color={color} key={color}>
            <i />
            <small>{color}</small>
          </span>
        ))}
      </div>
      <dl>
        <div>
          <dt>Logo rules</dt>
          <dd>{profile.logoRule}</dd>
        </div>
        <div>
          <dt>Photography</dt>
          <dd>{profile.photography}</dd>
        </div>
      </dl>
      <section>
        <strong>Exclude</strong>
        {profile.exclusions.map((item, index) => (
          <Badge key={index} tone="neutral">
            <X aria-hidden="true" />
            {item}
          </Badge>
        ))}
      </section>
      {onEdit && (
        <Button onClick={onEdit} variant="secondary">
          Edit visual profile
        </Button>
      )}
    </FeaturePanel>
  );
}

export interface AspectRatioOption {
  id: string;
  label: ReactNode;
  ratio: number;
  width: number;
  height: number;
  use: ReactNode;
}
export interface AspectRatioPickerProps extends Omit<
  HTMLAttributes<HTMLFieldSetElement>,
  "onChange"
> {
  options?: readonly AspectRatioOption[];
  value?: string;
  onChange?: (option: AspectRatioOption) => void;
}
const ratioOptions: readonly AspectRatioOption[] = [
  { id: "social", label: "Social", ratio: 1, width: 1080, height: 1080, use: "Feed" },
  { id: "blog", label: "Blog", ratio: 1.5, width: 1200, height: 800, use: "Article" },
  { id: "ad", label: "Ad", ratio: 1.91, width: 1200, height: 628, use: "Campaign" },
  {
    id: "presentation",
    label: "Slide",
    ratio: 1.78,
    width: 1920,
    height: 1080,
    use: "Presentation"
  },
  { id: "hero", label: "Hero", ratio: 2.22, width: 2000, height: 900, use: "Website" }
];
export function AspectRatioPicker({
  className,
  onChange,
  options = ratioOptions,
  value: controlled,
  ...props
}: AspectRatioPickerProps) {
  const [internal, setInternal] = useState(options[0]?.id);
  const value = controlled ?? internal;
  return (
    <fieldset className={cn("amro-ratio-picker", className)} {...props}>
      <legend>Aspect ratio</legend>
      <div>
        {options.map((option) => (
          <label data-selected={value === option.id} key={option.id}>
            <input
              checked={value === option.id}
              name="aspect-ratio"
              onChange={() => {
                setInternal(option.id);
                onChange?.(option);
              }}
              type="radio"
            />
            <span className="amro-ratio-picker__shape" style={{ aspectRatio: option.ratio }}>
              <ImageIcon aria-hidden="true" />
            </span>
            <span>
              <strong>{option.label}</strong>
              <small>
                {option.width} × {option.height} · {option.use}
              </small>
            </span>
          </label>
        ))}
      </div>
    </fieldset>
  );
}

export interface SafeArea {
  id: string;
  label: ReactNode;
  x: number;
  y: number;
  width: number;
  height: number;
  kind: "title" | "logo" | "crop";
}
export interface SafeTextAreaOverlayProps extends HTMLAttributes<HTMLDivElement> {
  imageSrc?: string;
  imageAlt?: string;
  areas?: readonly SafeArea[];
  active?: boolean;
  onAreaSelect?: (area: SafeArea) => void;
}
const safeAreas: readonly SafeArea[] = [
  { id: "title", label: "Title safe area", x: 6, y: 12, width: 42, height: 34, kind: "title" },
  { id: "logo", label: "Logo safe area", x: 76, y: 8, width: 18, height: 18, kind: "logo" },
  { id: "crop", label: "Mobile crop", x: 30, y: 0, width: 40, height: 100, kind: "crop" }
];
export function SafeTextAreaOverlay({
  active = true,
  areas = safeAreas,
  className,
  imageAlt = "Campaign visual",
  imageSrc,
  onAreaSelect,
  ...props
}: SafeTextAreaOverlayProps) {
  return (
    <div className={cn("amro-safe-overlay", className)} {...props}>
      <header>
        <span>
          <strong>Placement guides</strong>
          <small>Titles, logos, and platform crops</small>
        </span>
        <StatusIndicator
          label={active ? "Guides on" : "Guides off"}
          status={active ? "active" : "idle"}
        />
      </header>
      <div>
        {imageSrc ? (
          <img alt={imageAlt} src={imageSrc} />
        ) : (
          <span className="amro-safe-overlay__art">
            <Sparkles aria-hidden="true" />
          </span>
        )}
        {active &&
          areas.map((area) => (
            <button
              data-kind={area.kind}
              key={area.id}
              onClick={() => onAreaSelect?.(area)}
              style={{
                left: `${area.x}%`,
                top: `${area.y}%`,
                width: `${area.width}%`,
                height: `${area.height}%`
              }}
              type="button"
            >
              <span>
                {area.kind === "title" ? (
                  <Type aria-hidden="true" />
                ) : area.kind === "logo" ? (
                  <ShieldCheck aria-hidden="true" />
                ) : (
                  <Crop aria-hidden="true" />
                )}
                {area.label}
              </span>
            </button>
          ))}
      </div>
    </div>
  );
}

export interface CampaignCrop {
  id: string;
  label: ReactNode;
  width: number;
  height: number;
  status?: "ready" | "generating" | "needs-review";
  src?: string;
}
export interface CampaignCropSetProps extends HTMLAttributes<HTMLDivElement> {
  sourceLabel?: ReactNode;
  crops?: readonly CampaignCrop[];
  onGenerate?: () => void;
  onOpen?: (crop: CampaignCrop) => void;
}
const campaignCrops: readonly CampaignCrop[] = [
  { id: "linkedin", label: "LinkedIn feed", width: 1200, height: 628, status: "ready" },
  { id: "instagram", label: "Instagram", width: 1080, height: 1080, status: "ready" },
  { id: "story", label: "Story", width: 1080, height: 1920, status: "needs-review" },
  { id: "hero", label: "Website hero", width: 2000, height: 900, status: "generating" }
];
export function CampaignCropSet({
  className,
  crops = campaignCrops,
  onGenerate,
  onOpen,
  sourceLabel = "Governed AI launch hero",
  ...props
}: CampaignCropSetProps) {
  return (
    <div className={cn("amro-crop-set", className)} {...props}>
      <header>
        <span>
          <strong>Campaign crop set</strong>
          <small>Source: {sourceLabel}</small>
        </span>
        {onGenerate && (
          <Button onClick={onGenerate} size="sm">
            <Crop aria-hidden="true" />
            Generate all crops
          </Button>
        )}
      </header>
      <div>
        {crops.map((crop, index) => (
          <button key={crop.id} onClick={() => onOpen?.(crop)} type="button">
            <span
              className="amro-crop-set__preview"
              data-variant={index}
              style={{ aspectRatio: crop.width / crop.height }}
            >
              <Crop aria-hidden="true" />
            </span>
            <span>
              <strong>{crop.label}</strong>
              <small>
                {crop.width} × {crop.height}
              </small>
            </span>
            <StatusIndicator
              label={crop.status ?? "ready"}
              status={
                crop.status === "ready"
                  ? "success"
                  : crop.status === "generating"
                    ? "loading"
                    : "warning"
              }
            />
          </button>
        ))}
      </div>
    </div>
  );
}

export interface AssetGalleryCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  asset?: GeneratedImageAsset;
  createdAt?: ReactNode;
  selected?: boolean;
  favourite?: boolean;
  onOpen?: (asset: GeneratedImageAsset) => void;
  onSelect?: (asset: GeneratedImageAsset, selected: boolean) => void;
  onFavourite?: (asset: GeneratedImageAsset, favourite: boolean) => void;
}
export function AssetGalleryCard({
  asset = sampleGeneratedAsset,
  className,
  createdAt = "4 min ago",
  favourite = false,
  onFavourite,
  onOpen,
  onSelect,
  selected = false,
  ...props
}: AssetGalleryCardProps) {
  return (
    <Card
      className={cn(
        "amro-asset-gallery-card",
        selected && "amro-asset-gallery-card--selected",
        className
      )}
      {...props}
    >
      <div className="amro-asset-gallery-card__preview">
        <button
          aria-label={`Open ${asset.alt ?? "asset"}`}
          className="amro-asset-gallery-card__open"
          onClick={() => onOpen?.(asset)}
          type="button"
        >
          {asset.src ? (
            <img alt={asset.alt ?? "Generated asset"} src={asset.src} />
          ) : (
            <span>
              <Sparkles aria-hidden="true" />
            </span>
          )}
        </button>
        <span>
          <label>
            <input
              aria-label="Select asset"
              checked={selected}
              onChange={(event) => onSelect?.(asset, event.target.checked)}
              type="checkbox"
            />
          </label>
          <button
            aria-label={favourite ? "Remove favourite" : "Add favourite"}
            onClick={() => onFavourite?.(asset, !favourite)}
            type="button"
          >
            <FolderHeart aria-hidden="true" />
          </button>
        </span>
      </div>
      <div>
        <span>
          <strong>{asset.alt ?? "Generated campaign asset"}</strong>
          <small>
            {asset.width} × {asset.height} · {asset.provider}
          </small>
        </span>
        <Badge tone="neutral">{createdAt}</Badge>
      </div>
      <p>{asset.prompt}</p>
    </Card>
  );
}

export interface DurableAssetBadgeProps extends Omit<HTMLAttributes<HTMLSpanElement>, "onCopy"> {
  stored?: boolean;
  url?: string;
  provider?: ReactNode;
  onCopy?: (url: string) => void;
}
export function DurableAssetBadge({
  className,
  onCopy,
  provider = "Amro storage",
  stored = true,
  url = "https://assets.amro.ai/a/vis-1842",
  ...props
}: DurableAssetBadgeProps) {
  return (
    <span className={cn("amro-durable-asset-badge", className)} {...props}>
      <Cloud aria-hidden="true" />
      <span>
        <strong>{stored ? "Durable asset" : "Storage pending"}</strong>
        <small>{stored ? provider : "Waiting for durable URL"}</small>
      </span>
      {stored && (
        <button aria-label="Copy asset URL" onClick={() => onCopy?.(url)} type="button">
          <Copy aria-hidden="true" />
        </button>
      )}
    </span>
  );
}

export interface AssetMetadata {
  id: string;
  createdAt: ReactNode;
  dimensions: ReactNode;
  provider: ReactNode;
  model?: ReactNode;
  seed?: ReactNode;
  format?: ReactNode;
  size?: ReactNode;
}
export interface AssetDetailDrawerProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
  open?: boolean;
  title?: ReactNode;
  asset?: GeneratedImageAsset;
  metadata?: AssetMetadata;
  variants?: readonly ImageVariant[];
  promptHistory?: readonly string[];
  onClose?: () => void;
  onDownload?: (asset: GeneratedImageAsset) => void;
}
const assetMetadata: AssetMetadata = {
  id: "vis-1842",
  createdAt: "5 August 2026, 14:42",
  dimensions: "1600 × 900",
  provider: "Amro Render Pro",
  model: "ARP-3",
  seed: "847291",
  format: "PNG",
  size: "2.8 MB"
};
export function AssetDetailDrawer({
  asset = sampleGeneratedAsset,
  className,
  metadata = assetMetadata,
  onClose,
  onDownload,
  open = true,
  promptHistory = [
    "Governed AI workspace",
    "Add visible approval checkpoint",
    "Increase negative space for headline"
  ],
  title = "Asset details",
  variants = imageVariants.slice(0, 3),
  ...props
}: AssetDetailDrawerProps) {
  const [tab, setTab] = useState<"details" | "prompt" | "variants">("details");
  if (!open) return null;
  return (
    <aside
      className={cn("amro-asset-detail", className)}
      aria-label={getAccessibleText(title, "Asset details")}
      {...props}
    >
      <header>
        <span>
          <strong>{title}</strong>
          <small>{asset.alt}</small>
        </span>
        {onClose && (
          <button aria-label="Close asset details" onClick={onClose} type="button">
            <X aria-hidden="true" />
          </button>
        )}
      </header>
      <div className="amro-asset-detail__preview">
        {asset.src ? (
          <img alt={asset.alt ?? "Asset"} src={asset.src} />
        ) : (
          <Sparkles aria-hidden="true" />
        )}
      </div>
      <div role="tablist">
        {(["details", "prompt", "variants"] as const).map((item) => (
          <button
            aria-selected={tab === item}
            key={item}
            onClick={() => setTab(item)}
            role="tab"
            type="button"
          >
            {item}
          </button>
        ))}
      </div>
      <div className="amro-asset-detail__body">
        {tab === "details" && (
          <dl>
            {Object.entries(metadata).map(([key, value]) => (
              <div key={key}>
                <dt>{key}</dt>
                <dd>{value}</dd>
              </div>
            ))}
          </dl>
        )}
        {tab === "prompt" && (
          <ol>
            {promptHistory.map((prompt, index) => (
              <li key={prompt}>
                <Badge tone="neutral">v{index + 1}</Badge>
                {prompt}
              </li>
            ))}
          </ol>
        )}
        {tab === "variants" && (
          <div>
            {variants.map((variant, index) => (
              <span data-variant={index} key={variant.id}>
                <Sparkles aria-hidden="true" />
                <small>{variant.label}</small>
              </span>
            ))}
          </div>
        )}
      </div>
      {onDownload && (
        <footer>
          <Button onClick={() => onDownload(asset)}>
            <Download aria-hidden="true" />
            Download asset
          </Button>
        </footer>
      )}
    </aside>
  );
}

export interface AssetVersion {
  id: string;
  label: ReactNode;
  parentId?: string;
  createdAt: ReactNode;
  kind: "original" | "edited" | "regenerated";
  selected?: boolean;
}
export interface AssetVersionStackProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  versions?: readonly AssetVersion[];
  onSelect?: (version: AssetVersion) => void;
  onCompare?: (version: AssetVersion) => void;
}
const assetVersions: readonly AssetVersion[] = [
  { id: "v1", label: "Original generation", createdAt: "14:42", kind: "original" },
  { id: "v2", label: "Stronger brand", parentId: "v1", createdAt: "14:49", kind: "regenerated" },
  {
    id: "v3",
    label: "Manual crop adjustment",
    parentId: "v2",
    createdAt: "14:53",
    kind: "edited",
    selected: true
  }
];
export function AssetVersionStack({
  className,
  onCompare,
  onSelect,
  versions = assetVersions,
  ...props
}: AssetVersionStackProps) {
  return (
    <div className={cn("amro-version-stack", className)} {...props}>
      <header>
        <span>
          <strong>Asset versions</strong>
          <small>Original and descendants stay recoverable</small>
        </span>
        <Badge tone="neutral">{versions.length}</Badge>
      </header>
      <ol>
        {versions.map((version, index) => (
          <li data-kind={version.kind} key={version.id}>
            <span className="amro-version-stack__thumb" data-variant={index}>
              <Layers3 aria-hidden="true" />
            </span>
            <span>
              <strong>{version.label}</strong>
              <small>
                {version.kind} · {version.createdAt}
                {version.parentId ? ` · from ${version.parentId}` : ""}
              </small>
            </span>
            {version.selected ? (
              <Badge tone="success">Current</Badge>
            ) : (
              <div>
                {onCompare && (
                  <Button onClick={() => onCompare(version)} size="sm" variant="tertiary">
                    Compare
                  </Button>
                )}
                {onSelect && (
                  <Button onClick={() => onSelect(version)} size="sm" variant="secondary">
                    Restore
                  </Button>
                )}
              </div>
            )}
          </li>
        ))}
      </ol>
    </div>
  );
}

export interface PromptHistoryItem {
  id: string;
  prompt: ReactNode;
  seed?: ReactNode;
  provider?: ReactNode;
  createdAt: ReactNode;
  resultCount?: number;
  current?: boolean;
}
export interface PromptHistoryTimelineProps extends HTMLAttributes<HTMLOListElement> {
  items?: readonly PromptHistoryItem[];
  onRestore?: (item: PromptHistoryItem) => void;
}
const promptHistoryItems: readonly PromptHistoryItem[] = [
  {
    id: "p3",
    prompt: "Increase negative space and strengthen brand lighting",
    seed: "847291",
    provider: "Amro Render Pro",
    createdAt: "14:49",
    resultCount: 4,
    current: true
  },
  {
    id: "p2",
    prompt: "Add a visible human approval checkpoint",
    seed: "847291",
    provider: "Amro Render Pro",
    createdAt: "14:44",
    resultCount: 4
  },
  {
    id: "p1",
    prompt: "Governed AI operations workspace",
    seed: "398104",
    provider: "Vision Studio",
    createdAt: "14:38",
    resultCount: 4
  }
];
export function PromptHistoryTimeline({
  className,
  items = promptHistoryItems,
  onRestore,
  ...props
}: PromptHistoryTimelineProps) {
  return (
    <ol className={cn("amro-prompt-history", className)} {...props}>
      {items.map((item) => (
        <li key={item.id}>
          <span>
            <History aria-hidden="true" />
          </span>
          <span>
            <strong>{item.prompt}</strong>
            <small>
              {item.createdAt} · seed {item.seed} · {item.provider} · {item.resultCount} results
            </small>
          </span>
          {item.current ? (
            <Badge tone="success">Current</Badge>
          ) : (
            onRestore && (
              <Button onClick={() => onRestore(item)} size="sm" variant="secondary">
                Restore prompt
              </Button>
            )
          )}
        </li>
      ))}
    </ol>
  );
}

export interface FavouriteCollectionCardProps extends HTMLAttributes<HTMLDivElement> {
  name?: ReactNode;
  client?: ReactNode;
  campaign?: ReactNode;
  assets?: readonly GeneratedImageAsset[];
  updatedAt?: ReactNode;
  onOpen?: () => void;
  onAdd?: () => void;
}
export function FavouriteCollectionCard({
  assets = imageVariants.slice(0, 4),
  campaign = "Governed AI launch",
  className,
  client = "Amro",
  name = "Launch selects",
  onAdd,
  onOpen,
  updatedAt = "Updated today",
  ...props
}: FavouriteCollectionCardProps) {
  return (
    <Card className={cn("amro-favourite-collection", className)} {...props}>
      <div className="amro-favourite-collection__mosaic">
        {assets.slice(0, 4).map((asset, index) => (
          <span data-variant={index} key={asset.id}>
            {asset.src ? <img alt="" src={asset.src} /> : <Sparkles aria-hidden="true" />}
          </span>
        ))}
      </div>
      <header>
        <span>
          <strong>{name}</strong>
          <small>
            {client} · {campaign}
          </small>
        </span>
        <Badge tone="neutral">{assets.length} assets</Badge>
      </header>
      <footer>
        <small>{updatedAt}</small>
        <div>
          {onAdd && (
            <Button onClick={onAdd} size="sm" variant="tertiary">
              Add assets
            </Button>
          )}
          {onOpen && (
            <Button onClick={onOpen} size="sm" variant="secondary">
              Open collection
            </Button>
          )}
        </div>
      </footer>
    </Card>
  );
}

export interface AssetUsage {
  id: string;
  product: ReactNode;
  campaign: ReactNode;
  placement: ReactNode;
  status?: "active" | "scheduled" | "archived";
  usedAt?: ReactNode;
}
export interface AssetUsageTrackerProps extends HTMLAttributes<HTMLDivElement> {
  usages?: readonly AssetUsage[];
  onOpen?: (usage: AssetUsage) => void;
}
const assetUsages: readonly AssetUsage[] = [
  {
    id: "web",
    product: "AmroAgents",
    campaign: "Governed AI launch",
    placement: "Homepage hero",
    status: "active",
    usedAt: "Live now"
  },
  {
    id: "linkedin",
    product: "AmroGen",
    campaign: "Q3 outreach",
    placement: "LinkedIn creative",
    status: "scheduled",
    usedAt: "12 Aug"
  },
  {
    id: "deck",
    product: "AmroUI",
    campaign: "Sales enablement",
    placement: "Presentation slide 4",
    status: "active",
    usedAt: "5 Aug"
  }
];
export function AssetUsageTracker({
  className,
  onOpen,
  usages = assetUsages,
  ...props
}: AssetUsageTrackerProps) {
  const active = usages.filter((usage) => usage.status === "active").length;
  return (
    <FeaturePanel
      className={cn("amro-asset-usage", className)}
      eyebrow="Asset usage"
      title={`${usages.length} placements`}
      description={`${active} currently live`}
      status={<Badge tone="success">Rights tracked</Badge>}
      {...props}
    >
      <ul>
        {usages.map((usage) => (
          <li key={usage.id}>
            <span>
              <strong>{usage.product}</strong>
              <small>
                {usage.campaign} · {usage.placement}
              </small>
            </span>
            <StatusIndicator
              label={usage.status ?? "active"}
              status={
                usage.status === "active"
                  ? "online"
                  : usage.status === "scheduled"
                    ? "pending"
                    : "idle"
              }
            />
            <time>{usage.usedAt}</time>
            {onOpen && (
              <button
                aria-label={`Open ${getAccessibleText(usage.placement, "asset usage")}`}
                onClick={() => onOpen(usage)}
                type="button"
              >
                <ExternalLink aria-hidden="true" />
              </button>
            )}
          </li>
        ))}
      </ul>
    </FeaturePanel>
  );
}

export type DownloadFormat = "original" | "compressed" | "transparent" | "platform";
export interface DownloadFormatOption {
  id: DownloadFormat;
  label: ReactNode;
  detail: ReactNode;
  format: ReactNode;
  size?: ReactNode;
  available?: boolean;
}
export interface DownloadFormatMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  open?: boolean;
  formats?: readonly DownloadFormatOption[];
  onSelect?: (format: DownloadFormatOption) => void;
  onClose?: () => void;
}
const downloadFormats: readonly DownloadFormatOption[] = [
  {
    id: "original",
    label: "Original",
    detail: "Full-quality source",
    format: "PNG",
    size: "2.8 MB",
    available: true
  },
  {
    id: "compressed",
    label: "Compressed web",
    detail: "Optimised delivery",
    format: "WEBP",
    size: "420 KB",
    available: true
  },
  {
    id: "transparent",
    label: "Transparent",
    detail: "Background removed",
    format: "PNG",
    size: "1.9 MB",
    available: false
  },
  {
    id: "platform",
    label: "Platform-ready set",
    detail: "Social and ad crops",
    format: "ZIP",
    size: "8.4 MB",
    available: true
  }
];
export function DownloadFormatMenu({
  className,
  formats = downloadFormats,
  onClose,
  onSelect,
  open = true,
  ...props
}: DownloadFormatMenuProps) {
  if (!open) return null;
  return (
    <div
      className={cn("amro-download-menu", className)}
      role="menu"
      aria-label="Download formats"
      {...props}
    >
      <header>
        <span>
          <strong>Download asset</strong>
          <small>Choose a prepared format</small>
        </span>
        {onClose && (
          <button aria-label="Close download menu" onClick={onClose} type="button">
            <X aria-hidden="true" />
          </button>
        )}
      </header>
      <div>
        {formats.map((format) => (
          <button
            disabled={format.available === false}
            key={format.id}
            onClick={() => onSelect?.(format)}
            role="menuitem"
            type="button"
          >
            <span>
              <Download aria-hidden="true" />
            </span>
            <span>
              <strong>{format.label}</strong>
              <small>{format.detail}</small>
            </span>
            <span>
              <strong>{format.format}</strong>
              <small>{format.size ?? (format.available === false ? "Generate first" : "")}</small>
            </span>
          </button>
        ))}
      </div>
    </div>
  );
}

export interface AltTextGeneratorProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
  assetAlt?: string;
  generated?: boolean;
  confidence?: number;
  onGenerate?: () => void;
  onSubmit?: (altText: string) => void;
}
export function AltTextGenerator({
  assetAlt = "A product interface showing an AI workflow paused at a human approval checkpoint, lit with teal and cyan accents.",
  className,
  confidence = 94,
  generated = true,
  onGenerate,
  onSubmit,
  ...props
}: AltTextGeneratorProps) {
  const [text, setText] = useState(assetAlt);
  return (
    <form
      className={cn("amro-alt-generator", className)}
      onSubmit={(event) => {
        event.preventDefault();
        if (text.trim()) onSubmit?.(text);
      }}
      {...props}
    >
      <header>
        <span>
          <strong>Accessibility description</strong>
          <small>Describe meaning and context, not visual decoration</small>
        </span>
        {generated && <Badge tone="success">{confidence}% confidence</Badge>}
      </header>
      <div className="amro-alt-generator__asset">
        <ImageIcon aria-hidden="true" />
      </div>
      <label>
        <span>Alt text</span>
        <Textarea onChange={(event) => setText(event.target.value)} value={text} />
        <small>{text.length} characters</small>
      </label>
      <footer>
        {onGenerate && (
          <Button onClick={onGenerate} variant="secondary">
            <Sparkles aria-hidden="true" />
            Regenerate
          </Button>
        )}
        <Button disabled={!text.trim()} type="submit">
          Save alt text
        </Button>
      </footer>
    </form>
  );
}

export interface CreditCostItem {
  id: string;
  label: ReactNode;
  quantity: number;
  unitCost: number;
}
export interface CreditCostPreviewProps extends HTMLAttributes<HTMLDivElement> {
  items?: readonly CreditCostItem[];
  balance?: number;
  onGenerate?: () => void;
}
const creditCostItems: readonly CreditCostItem[] = [
  { id: "base", label: "Image generation", quantity: 4, unitCost: 18 },
  { id: "enhance", label: "Prompt enhancement", quantity: 1, unitCost: 4 },
  { id: "store", label: "Durable storage", quantity: 4, unitCost: 2 }
];
export function CreditCostPreview({
  balance = 240,
  className,
  items = creditCostItems,
  onGenerate,
  ...props
}: CreditCostPreviewProps) {
  const total = items.reduce((sum, item) => sum + item.quantity * item.unitCost, 0);
  const enough = balance >= total;
  return (
    <FeaturePanel
      className={cn("amro-credit-cost", className)}
      eyebrow="Generation cost"
      title={`${total} credits`}
      description={`${balance} available`}
      status={
        <StatusIndicator
          label={enough ? "Within balance" : "Insufficient credits"}
          status={enough ? "success" : "blocked"}
        />
      }
      {...props}
    >
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <span>
              {item.label}
              <small>
                {item.quantity} × {item.unitCost}
              </small>
            </span>
            <strong>{item.quantity * item.unitCost}</strong>
          </li>
        ))}
      </ul>
      <footer>
        <span>
          Remaining after generation <strong>{Math.max(0, balance - total)}</strong>
        </span>
        {onGenerate && (
          <Button disabled={!enough} onClick={onGenerate}>
            Generate {items[0]?.quantity ?? 1} variants
          </Button>
        )}
      </footer>
    </FeaturePanel>
  );
}

export interface CreditConsumptionReceiptProps extends HTMLAttributes<HTMLDivElement> {
  provider?: ReactNode;
  assetCount?: number;
  generationCost?: number;
  storageCost?: number;
  total?: number;
  transactionId?: ReactNode;
  createdAt?: ReactNode;
}
export function CreditConsumptionReceipt({
  assetCount = 4,
  className,
  createdAt = "5 August 2026, 14:42",
  generationCost = 76,
  provider = "Amro Render Pro",
  storageCost = 8,
  total = 84,
  transactionId = "cr_9Q8M42",
  ...props
}: CreditConsumptionReceiptProps) {
  return (
    <Card className={cn("amro-credit-receipt", className)} {...props}>
      <header>
        <span>
          <Check aria-hidden="true" />
        </span>
        <span>
          <strong>Generation complete</strong>
          <small>{assetCount} durable assets created</small>
        </span>
        <strong>−{total}</strong>
      </header>
      <dl>
        <div>
          <dt>Provider</dt>
          <dd>{provider}</dd>
        </div>
        <div>
          <dt>Generation</dt>
          <dd>{generationCost} credits</dd>
        </div>
        <div>
          <dt>Storage</dt>
          <dd>{storageCost} credits</dd>
        </div>
        <div>
          <dt>Total</dt>
          <dd>{total} credits</dd>
        </div>
      </dl>
      <footer>
        <span>
          {transactionId} · {createdAt}
        </span>
        <Badge tone="success">Exact cost</Badge>
      </footer>
    </Card>
  );
}

export interface APIAssetCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "onCopy"> {
  asset?: GeneratedImageAsset;
  url?: string;
  apiId?: string;
  contentType?: ReactNode;
  expires?: ReactNode;
  onCopy?: (value: string) => void;
  onOpen?: () => void;
}
export function APIAssetCard({
  apiId = "asset_vis_1842",
  asset = sampleGeneratedAsset,
  className,
  contentType = "image/png",
  expires = "No expiry",
  onCopy,
  onOpen,
  url = "https://assets.amro.ai/v1/asset_vis_1842.png",
  ...props
}: APIAssetCardProps) {
  return (
    <Card className={cn("amro-api-asset", className)} {...props}>
      <header>
        <span>
          <Server aria-hidden="true" />
        </span>
        <span>
          <span className="amro-eyebrow">API-ready asset</span>
          <strong>{asset.alt}</strong>
          <small>
            {apiId} · {contentType} · {expires}
          </small>
        </span>
        <DurableAssetBadge url={url} />
      </header>
      <div>
        <code>{url}</code>
        <Button
          aria-label="Copy durable asset URL"
          onClick={() => onCopy?.(url)}
          size="sm"
          variant="secondary"
        >
          <Copy aria-hidden="true" />
        </Button>
      </div>
      <dl>
        <div>
          <dt>Width</dt>
          <dd>{asset.width}</dd>
        </div>
        <div>
          <dt>Height</dt>
          <dd>{asset.height}</dd>
        </div>
        <div>
          <dt>Provider</dt>
          <dd>{asset.provider}</dd>
        </div>
      </dl>
      {onOpen && (
        <Button onClick={onOpen} variant="tertiary">
          Open API metadata
          <Code2 aria-hidden="true" />
        </Button>
      )}
    </Card>
  );
}

export interface ContentSafetyExplanationProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "title"
> {
  title?: ReactNode;
  category?: ReactNode;
  explanation?: ReactNode;
  safeAlternatives?: readonly ReactNode[];
  onUseAlternative?: (alternative: ReactNode) => void;
  onEditPrompt?: () => void;
}
export function ContentSafetyExplanation({
  category = "Identity misuse",
  className,
  explanation = "This request appears to place a recognisable person in a misleading commercial scene. AmroVisionAI will not generate deceptive identity content.",
  onEditPrompt,
  onUseAlternative,
  safeAlternatives = [
    "Use a fictional professional with no resemblance to a real person",
    "Create an abstract product-led campaign visual",
    "Use an approved brand spokesperson reference"
  ],
  title = "This image cannot be generated as written",
  ...props
}: ContentSafetyExplanationProps) {
  return (
    <div className={cn("amro-safety-explanation", className)} role="alert" {...props}>
      <header>
        <ShieldCheck aria-hidden="true" />
        <span>
          <span className="amro-eyebrow">Generation blocked · {category}</span>
          <strong>{title}</strong>
          <p>{explanation}</p>
        </span>
      </header>
      <section>
        <strong>Safe ways to continue</strong>
        {safeAlternatives.map((alternative, index) => (
          <button key={index} onClick={() => onUseAlternative?.(alternative)} type="button">
            <Check aria-hidden="true" />
            <span>{alternative}</span>
            <ChevronRight aria-hidden="true" />
          </button>
        ))}
      </section>
      {onEditPrompt && (
        <footer>
          <Button onClick={onEditPrompt}>Edit prompt</Button>
        </footer>
      )}
    </div>
  );
}

export interface StarterPrompt {
  id: string;
  category: ReactNode;
  title: ReactNode;
  prompt: string;
}
export interface EmptyGalleryStarterProps extends HTMLAttributes<HTMLDivElement> {
  prompts?: readonly StarterPrompt[];
  categories?: readonly ReactNode[];
  onSelectPrompt?: (prompt: StarterPrompt) => void;
  onStartBlank?: () => void;
}
const starterPrompts: readonly StarterPrompt[] = [
  {
    id: "hero",
    category: "Product hero",
    title: "Outcome-led product story",
    prompt:
      "Premium editorial hero showing a real team using governed AI to complete a measurable business outcome"
  },
  {
    id: "campaign",
    category: "Campaign",
    title: "Multi-channel launch visual",
    prompt:
      "Flexible campaign composition with strong subject focus and safe text areas for social crops"
  },
  {
    id: "article",
    category: "Editorial",
    title: "Concept illustration",
    prompt:
      "Clear editorial metaphor explaining a complex AI governance concept without robots or sci-fi clichés"
  }
];
export function EmptyGalleryStarter({
  categories = ["Product", "Campaign", "Editorial", "Social"],
  className,
  onSelectPrompt,
  onStartBlank,
  prompts = starterPrompts,
  ...props
}: EmptyGalleryStarterProps) {
  return (
    <div className={cn("amro-empty-gallery", className)} {...props}>
      <header>
        <span>
          <ImageIcon aria-hidden="true" />
        </span>
        <h2>Create your first durable visual</h2>
        <p>Start with a production-ready example or build a prompt from scratch.</p>
        {onStartBlank && (
          <Button onClick={onStartBlank}>
            Start from a blank prompt
            <Sparkles aria-hidden="true" />
          </Button>
        )}
      </header>
      <div className="amro-empty-gallery__categories">
        {categories.map((category, index) => (
          <Badge key={index} tone="neutral">
            {category}
          </Badge>
        ))}
      </div>
      <div>
        {prompts.map((prompt, index) => (
          <button
            data-variant={index}
            key={prompt.id}
            onClick={() => onSelectPrompt?.(prompt)}
            type="button"
          >
            <span>
              <Sparkles aria-hidden="true" />
            </span>
            <span>
              <Badge tone="neutral">{prompt.category}</Badge>
              <strong>{prompt.title}</strong>
              <small>{prompt.prompt}</small>
            </span>
            <ChevronRight aria-hidden="true" />
          </button>
        ))}
      </div>
    </div>
  );
}
```



## Usage

Shared product source installed automatically by AmroUI component entries.

