# AmroUI shared

Installable AmroUI product component source.

## Installation

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

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

## Source

### source/components/shared.tsx

```tsx
import {
  ArrowRight,
  CalendarDays,
  Check,
  ChevronDown,
  ChevronRight,
  CircleAlert,
  Clock3,
  Coins,
  Command,
  Copy,
  ExternalLink,
  Maximize2,
  Menu,
  MessageCircle,
  PanelRightClose,
  RotateCcw,
  Search,
  ShieldCheck,
  Sparkles,
  UserRoundCheck,
  X
} from "lucide-react";
import {
  forwardRef,
  useId,
  useMemo,
  useState,
  type ButtonHTMLAttributes,
  type HTMLAttributes,
  type ReactNode
} from "react";
import { cn } from "../lib/cn.js";
import { AmroUILogo } from "./amro-ui-logo.js";
import { Badge } from "./badge.js";
import { Button, type ButtonProps } from "./button.js";
import { Card } from "./card.js";
import { EmptyNotice, FeaturePanel, StatusIndicator } from "./feature-primitives.js";
import { Skeleton } from "./skeleton.js";

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

export interface AmroAppHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
  product?: ReactNode;
  navigation?: readonly { id: string; label: ReactNode; href: string; active?: boolean }[];
  actions?: ReactNode;
  onMenuToggle?: () => void;
}

export const AmroAppHeader = forwardRef<HTMLElement, AmroAppHeaderProps>(
  (
    {
      actions,
      className,
      navigation = [
        { id: "workspace", label: "Workspace", href: "#workspace", active: true },
        { id: "activity", label: "Activity", href: "#activity" }
      ],
      onMenuToggle,
      product = "Agents",
      ...props
    },
    ref
  ) => (
    <header ref={ref} className={cn("amro-app-header", className)} {...props}>
      <button
        aria-label="Open navigation"
        className="amro-app-header__menu"
        onClick={onMenuToggle}
        type="button"
      >
        <Menu aria-hidden="true" />
      </button>
      <a className="amro-app-header__brand" href="/" aria-label="AmroUI home">
        <AmroUILogo mode="full" variant="dark" />
        <span>{product}</span>
      </a>
      <nav aria-label="Product navigation">
        {navigation.map((item) => (
          <a aria-current={item.active ? "page" : undefined} href={item.href} key={item.id}>
            {item.label}
          </a>
        ))}
      </nav>
      <div className="amro-app-header__actions">{actions}</div>
    </header>
  )
);
AmroAppHeader.displayName = "AmroAppHeader";

export interface AmroProductIdentityProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  name?: ReactNode;
  description?: ReactNode;
  status?: "online" | "working" | "offline";
  icon?: ReactNode;
}

export const AmroProductIdentity = forwardRef<HTMLDivElement, AmroProductIdentityProps>(
  (
    {
      className,
      description = "Outcome-driven AI workspace",
      icon = <Sparkles aria-hidden="true" />,
      name = "Amro Agents",
      status = "online",
      ...props
    },
    ref
  ) => (
    <div ref={ref} className={cn("amro-product-identity", className)} {...props}>
      <span className="amro-product-identity__mark">{icon}</span>
      <span>
        <strong>{name}</strong>
        <small>{description}</small>
      </span>
      <StatusIndicator
        label={status === "working" ? "Working" : status === "online" ? "Online" : "Offline"}
        status={status === "working" ? "active" : status}
      />
    </div>
  )
);
AmroProductIdentity.displayName = "AmroProductIdentity";

export interface TalkNowButtonProps extends ButtonProps {
  availability?: "available" | "busy" | "offline";
}

export const TalkNowButton = forwardRef<HTMLButtonElement, TalkNowButtonProps>(
  ({ availability = "available", children, className, disabled, ...props }, ref) => (
    <Button
      ref={ref}
      className={cn("amro-talk-now", className)}
      disabled={disabled || availability !== "available"}
      {...props}
    >
      <span className="amro-talk-now__pulse" aria-hidden="true" />
      <MessageCircle aria-hidden="true" />
      {children ?? (availability === "available" ? "Talk now" : "Currently unavailable")}
    </Button>
  )
);
TalkNowButton.displayName = "TalkNowButton";

export const GradientPrimaryButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
  <Button ref={ref} variant="primary" {...props} />
));
GradientPrimaryButton.displayName = "GradientPrimaryButton";

export const OutlineSecondaryButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
  <Button ref={ref} variant="secondary" {...props} />
));
OutlineSecondaryButton.displayName = "OutlineSecondaryButton";

export const TextTertiaryAction = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
  <Button ref={ref} variant="tertiary" {...props} />
));
TextTertiaryAction.displayName = "TextTertiaryAction";

export interface LivePulseBadgeProps extends HTMLAttributes<HTMLSpanElement> {
  state?: "live" | "processing" | "paused";
  label?: ReactNode;
}

export const LivePulseBadge = forwardRef<HTMLSpanElement, LivePulseBadgeProps>(
  ({ className, label, state = "live", ...props }, ref) => (
    <span
      ref={ref}
      className={cn("amro-live-pulse", `amro-live-pulse--${state}`, className)}
      {...props}
    >
      <span aria-hidden="true" />
      {label ?? (state === "live" ? "Live" : state === "processing" ? "Processing" : "Paused")}
    </span>
  )
);
LivePulseBadge.displayName = "LivePulseBadge";

export interface MetricTileProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  label?: ReactNode;
  value?: ReactNode;
  change?: ReactNode;
  trend?: "up" | "down" | "neutral";
  icon?: ReactNode;
}

export const MetricTile = forwardRef<HTMLDivElement, MetricTileProps>(
  (
    {
      change = "+18% this week",
      className,
      icon = <Sparkles aria-hidden="true" />,
      label = "Verified outcomes",
      trend = "up",
      value = "128",
      ...props
    },
    ref
  ) => (
    <Card ref={ref} className={cn("amro-metric-tile", className)} {...props}>
      <span className="amro-metric-tile__icon">{icon}</span>
      <span>{label}</span>
      <strong>{value}</strong>
      <small data-trend={trend}>{change}</small>
    </Card>
  )
);
MetricTile.displayName = "MetricTile";

export interface ProcessDiagramNode {
  id: string;
  label: ReactNode;
  detail?: ReactNode;
  status?: "pending" | "active" | "complete" | "blocked";
}

export interface ExpandableProcessDiagramProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "title"
> {
  title?: ReactNode;
  nodes?: readonly ProcessDiagramNode[];
  defaultExpanded?: boolean;
}

const defaultProcessNodes: readonly ProcessDiagramNode[] = [
  { id: "understand", label: "Understand", detail: "Ground the request", status: "complete" },
  { id: "work", label: "Execute", detail: "Coordinate specialists", status: "active" },
  { id: "approve", label: "Approve", detail: "Human control boundary", status: "pending" }
];

export const ExpandableProcessDiagram = forwardRef<HTMLDivElement, ExpandableProcessDiagramProps>(
  (
    {
      className,
      defaultExpanded = false,
      nodes = defaultProcessNodes,
      title = "How this works",
      ...props
    },
    ref
  ) => {
    const [expanded, setExpanded] = useState(defaultExpanded);
    return (
      <div ref={ref} className={cn("amro-process-diagram", className)} {...props}>
        <button
          aria-expanded={expanded}
          onClick={() => setExpanded((value) => !value)}
          type="button"
        >
          <span>
            <Sparkles aria-hidden="true" /> {title}
          </span>
          <ChevronDown aria-hidden="true" />
        </button>
        {expanded && (
          <ol>
            {nodes.map((node, index) => (
              <li key={node.id} data-status={node.status ?? "pending"}>
                <span>{node.status === "complete" ? <Check aria-hidden="true" /> : index + 1}</span>
                <span>
                  <strong>{node.label}</strong>
                  {node.detail && <small>{node.detail}</small>}
                </span>
                {index < nodes.length - 1 && <ArrowRight aria-hidden="true" />}
              </li>
            ))}
          </ol>
        )}
      </div>
    );
  }
);
ExpandableProcessDiagram.displayName = "ExpandableProcessDiagram";

export interface ProductPreviewFrameProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  urlLabel?: ReactNode;
  toolbar?: ReactNode;
  onFullscreen?: () => void;
}

export const ProductPreviewFrame = forwardRef<HTMLDivElement, ProductPreviewFrameProps>(
  (
    {
      children,
      className,
      onFullscreen,
      title = "Product preview",
      toolbar,
      urlLabel = "app.amro.ai",
      ...props
    },
    ref
  ) => (
    <div ref={ref} className={cn("amro-product-preview", className)} {...props}>
      <header>
        <span aria-hidden="true">
          <i />
          <i />
          <i />
        </span>
        <strong>{title}</strong>
        <span className="amro-product-preview__url">{urlLabel}</span>
        {toolbar}
        {onFullscreen && (
          <button aria-label="Open fullscreen preview" onClick={onFullscreen} type="button">
            <Maximize2 aria-hidden="true" />
          </button>
        )}
      </header>
      <div className="amro-product-preview__canvas">
        {children ?? (
          <section className="amro-product-preview__starter">
            <span className="amro-eyebrow">Governed AI workflow</span>
            <strong>Ready for human review</strong>
            <p>The agent completed its work, verified the result, and paused before action.</p>
            <div>
              <span>Verified sources</span>
              <b>12</b>
            </div>
          </section>
        )}
      </div>
    </div>
  )
);
ProductPreviewFrame.displayName = "ProductPreviewFrame";

export interface GalleryAsset {
  id: string;
  src: string;
  alt: string;
  caption?: ReactNode;
}

export interface FullscreenGalleryViewerProps extends HTMLAttributes<HTMLDivElement> {
  assets?: readonly GalleryAsset[];
  initialIndex?: number;
  onClose?: () => void;
  onIndexChange?: (index: number) => void;
}

export const FullscreenGalleryViewer = forwardRef<HTMLDivElement, FullscreenGalleryViewerProps>(
  ({ assets = [], className, initialIndex = 0, onClose, onIndexChange, ...props }, ref) => {
    const [index, setIndex] = useState(Math.min(initialIndex, Math.max(assets.length - 1, 0)));
    const select = (next: number) => {
      if (!assets.length) return;
      const bounded = (next + assets.length) % assets.length;
      setIndex(bounded);
      onIndexChange?.(bounded);
    };
    const asset = assets[index];
    return (
      <div
        ref={ref}
        className={cn("amro-gallery-viewer", className)}
        role="dialog"
        aria-modal="true"
        aria-label="Asset gallery"
        {...props}
      >
        <header>
          <span>{assets.length ? `${index + 1} of ${assets.length}` : "Gallery"}</span>
          {onClose && (
            <button aria-label="Close gallery" onClick={onClose} type="button">
              <X aria-hidden="true" />
            </button>
          )}
        </header>
        {asset ? (
          <figure>
            <img alt={asset.alt} src={asset.src} />
            <figcaption>{asset.caption ?? asset.alt}</figcaption>
          </figure>
        ) : (
          <EmptyNotice
            title="No assets to preview"
            description="Generated assets will appear here."
          />
        )}
        {assets.length > 1 && (
          <footer>
            <Button
              aria-label="Previous asset"
              onClick={() => select(index - 1)}
              variant="secondary"
            >
              Previous
            </Button>
            <div>
              {assets.map((item, assetIndex) => (
                <button
                  aria-label={`View ${item.alt}`}
                  aria-current={assetIndex === index}
                  key={item.id}
                  onClick={() => select(assetIndex)}
                  type="button"
                />
              ))}
            </div>
            <Button aria-label="Next asset" onClick={() => select(index + 1)} variant="secondary">
              Next
            </Button>
          </footer>
        )}
      </div>
    );
  }
);
FullscreenGalleryViewer.displayName = "FullscreenGalleryViewer";

export interface FeatureProofCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  proof?: ReactNode;
  source?: ReactNode;
  action?: ReactNode;
}

export const FeatureProofCard = forwardRef<HTMLDivElement, FeatureProofCardProps>(
  (
    {
      action,
      className,
      description = "Grounded in approved workspace knowledge.",
      proof = "96% confidence",
      source = "12 verified sources",
      title = "Evidence-backed result",
      ...props
    },
    ref
  ) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-feature-proof", className)}
      eyebrow="Why trust this"
      title={title}
      description={description}
      status={
        <Badge tone="success">
          <ShieldCheck aria-hidden="true" /> Verified
        </Badge>
      }
      footer={action}
      {...props}
    >
      <div>
        <strong>{proof}</strong>
        <span>{source}</span>
      </div>
    </FeaturePanel>
  )
);
FeatureProofCard.displayName = "FeatureProofCard";

export interface CreditBalanceBadgeProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  balance?: number;
  unit?: ReactNode;
  lowAt?: number;
}

export const CreditBalanceBadge = forwardRef<HTMLButtonElement, CreditBalanceBadgeProps>(
  ({ balance = 240, className, lowAt = 50, unit = "credits", ...props }, ref) => (
    <button
      ref={ref}
      className={cn(
        "amro-credit-balance",
        balance <= lowAt && "amro-credit-balance--low",
        className
      )}
      type="button"
      {...props}
    >
      <Coins aria-hidden="true" />
      <strong>{balance.toLocaleString()}</strong>
      <span>{unit}</span>
      <ChevronRight aria-hidden="true" />
    </button>
  )
);
CreditBalanceBadge.displayName = "CreditBalanceBadge";

export interface HumanApprovalNoticeProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  reviewer?: ReactNode;
  onReview?: () => void;
}

export const HumanApprovalNotice = forwardRef<HTMLDivElement, HumanApprovalNoticeProps>(
  (
    {
      className,
      description = "Nothing external happens until a person reviews this action.",
      onReview,
      reviewer = "Workspace approver",
      title = "Human approval required",
      ...props
    },
    ref
  ) => (
    <div ref={ref} className={cn("amro-approval-notice", className)} role="status" {...props}>
      <UserRoundCheck aria-hidden="true" />
      <span>
        <strong>{title}</strong>
        <small>{description}</small>
      </span>
      <Badge tone="info">{reviewer}</Badge>
      {onReview && (
        <Button onClick={onReview} size="sm">
          Review
        </Button>
      )}
    </div>
  )
);
HumanApprovalNotice.displayName = "HumanApprovalNotice";

export interface EmbeddedBookingCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  host?: ReactNode;
  title?: ReactNode;
  duration?: ReactNode;
  timezone?: ReactNode;
  slots?: readonly { id: string; label: ReactNode }[];
  onSelectSlot?: (id: string) => void;
}

export const EmbeddedBookingCard = forwardRef<HTMLDivElement, EmbeddedBookingCardProps>(
  (
    {
      className,
      duration = "30 min",
      host = "Amro team",
      onSelectSlot,
      slots = [
        { id: "one", label: "10:30" },
        { id: "two", label: "14:00" },
        { id: "three", label: "16:30" }
      ],
      timezone = "Local time",
      title = "Continue in a meeting",
      ...props
    },
    ref
  ) => {
    const [selected, setSelected] = useState<string>();
    return (
      <FeaturePanel
        ref={ref}
        className={cn("amro-embedded-booking", className)}
        eyebrow="AmroMeet"
        title={title}
        description={host}
        status={
          <Badge tone="neutral">
            <Clock3 aria-hidden="true" /> {duration}
          </Badge>
        }
        footer={
          <span>
            <CalendarDays aria-hidden="true" /> {timezone}
          </span>
        }
        {...props}
      >
        <div>
          {slots.map((slot) => (
            <button
              aria-pressed={selected === slot.id}
              key={slot.id}
              onClick={() => {
                setSelected(slot.id);
                onSelectSlot?.(slot.id);
              }}
              type="button"
            >
              {slot.label}
            </button>
          ))}
        </div>
        {selected && <Button onClick={() => onSelectSlot?.(selected)}>Continue</Button>}
      </FeaturePanel>
    );
  }
);
EmbeddedBookingCard.displayName = "EmbeddedBookingCard";

export interface AmroToastProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  tone?: "success" | "info" | "warning" | "error";
  action?: ReactNode;
  onDismiss?: () => void;
}

export const AmroToast = forwardRef<HTMLDivElement, AmroToastProps>(
  (
    {
      action,
      className,
      description,
      onDismiss,
      title = "Changes saved",
      tone = "success",
      ...props
    },
    ref
  ) => (
    <div
      ref={ref}
      className={cn("amro-toast", `amro-toast--${tone}`, className)}
      role={tone === "error" ? "alert" : "status"}
      {...props}
    >
      <span>
        {tone === "success" ? <Check aria-hidden="true" /> : <CircleAlert aria-hidden="true" />}
      </span>
      <span>
        <strong>{title}</strong>
        {description && <small>{description}</small>}
      </span>
      {action}
      {onDismiss && (
        <button aria-label="Dismiss notification" onClick={onDismiss} type="button">
          <X aria-hidden="true" />
        </button>
      )}
    </div>
  )
);
AmroToast.displayName = "AmroToast";

export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  icon?: ReactNode;
  title?: ReactNode;
  description?: ReactNode;
  primaryAction?: ReactNode;
  secondaryAction?: ReactNode;
}

export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
  (
    {
      className,
      description = "Start a workflow to see results here.",
      icon = <Sparkles aria-hidden="true" />,
      primaryAction,
      secondaryAction,
      title = "Ready when you are",
      ...props
    },
    ref
  ) => (
    <div ref={ref} className={cn("amro-empty-state", className)} {...props}>
      <span>{icon}</span>
      <strong>{title}</strong>
      <p>{description}</p>
      {(primaryAction || secondaryAction) && (
        <div>
          {primaryAction}
          {secondaryAction}
        </div>
      )}
    </div>
  )
);
EmptyState.displayName = "EmptyState";

export interface SkeletonCardProps extends HTMLAttributes<HTMLDivElement> {
  rows?: number;
  showMedia?: boolean;
}

export const SkeletonCard = forwardRef<HTMLDivElement, SkeletonCardProps>(
  ({ className, rows = 3, showMedia = true, ...props }, ref) => (
    <Card
      ref={ref}
      className={cn("amro-skeleton-card", className)}
      aria-busy="true"
      aria-label="Loading content"
      {...props}
    >
      {showMedia && <Skeleton className="amro-skeleton-card__media" />}
      <Skeleton className="amro-skeleton-card__title" />
      {Array.from({ length: rows }, (_, index) => (
        <Skeleton className="amro-skeleton-card__row" key={index} />
      ))}
    </Card>
  )
);
SkeletonCard.displayName = "SkeletonCard";

export interface ErrorRecoveryCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  errorId?: ReactNode;
  retryLabel?: ReactNode;
  onRetry?: () => void;
  onViewDetails?: () => void;
}

export const ErrorRecoveryCard = forwardRef<HTMLDivElement, ErrorRecoveryCardProps>(
  (
    {
      className,
      description = "Your work is safe. Retry the last step or inspect the failure details.",
      errorId,
      onRetry,
      onViewDetails,
      retryLabel = "Try again",
      title = "This step could not finish",
      ...props
    },
    ref
  ) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-error-recovery", className)}
      eyebrow="Recovery"
      title={title}
      description={description}
      status={<StatusIndicator label="Needs attention" status="error" />}
      footer={errorId && <code>{errorId}</code>}
      {...props}
    >
      <div>
        {onRetry && (
          <Button onClick={onRetry}>
            <RotateCcw aria-hidden="true" /> {retryLabel}
          </Button>
        )}
        {onViewDetails && (
          <Button onClick={onViewDetails} variant="tertiary">
            View details
          </Button>
        )}
      </div>
    </FeaturePanel>
  )
);
ErrorRecoveryCard.displayName = "ErrorRecoveryCard";

export interface CommandPaletteItem {
  id: string;
  label: ReactNode;
  description?: ReactNode;
  keywords?: string;
  shortcut?: ReactNode;
  icon?: ReactNode;
  disabled?: boolean;
}

export interface CommandPaletteProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  items?: readonly CommandPaletteItem[];
  open?: boolean;
  defaultOpen?: boolean;
  placeholder?: string;
  onOpenChange?: (open: boolean) => void;
  onSelect?: (item: CommandPaletteItem) => void;
}

const defaultCommands: readonly CommandPaletteItem[] = [
  {
    id: "new",
    label: "Start a workflow",
    description: "Create from a goal",
    keywords: "new create",
    icon: <Sparkles aria-hidden="true" />,
    shortcut: "⌘N"
  },
  {
    id: "search",
    label: "Search activity",
    description: "Find outcomes and runs",
    keywords: "find",
    icon: <Search aria-hidden="true" />,
    shortcut: "⌘K"
  },
  { id: "copy", label: "Copy current link", icon: <Copy aria-hidden="true" /> }
];

export const CommandPalette = forwardRef<HTMLDivElement, CommandPaletteProps>(
  (
    {
      className,
      defaultOpen = true,
      items = defaultCommands,
      onOpenChange,
      onSelect,
      open: controlledOpen,
      placeholder = "Type a command…",
      ...props
    },
    ref
  ) => {
    const [internalOpen, setInternalOpen] = useState(defaultOpen);
    const [query, setQuery] = useState("");
    const open = controlledOpen ?? internalOpen;
    const results = useMemo(
      () =>
        items.filter((item) =>
          `${getAccessibleText(item.label, "")} ${item.keywords ?? ""}`
            .toLowerCase()
            .includes(query.toLowerCase())
        ),
      [items, query]
    );
    const setOpen = (next: boolean) => {
      setInternalOpen(next);
      onOpenChange?.(next);
    };
    if (!open) return null;
    return (
      <div
        ref={ref}
        className={cn("amro-command-palette", className)}
        role="dialog"
        aria-modal="true"
        aria-label="Command palette"
        {...props}
      >
        <header>
          <Command aria-hidden="true" />
          <input
            aria-label="Search commands"
            autoFocus
            onChange={(event) => setQuery(event.target.value)}
            placeholder={placeholder}
            value={query}
          />
          <kbd>Esc</kbd>
          <button aria-label="Close command palette" onClick={() => setOpen(false)} type="button">
            <X aria-hidden="true" />
          </button>
        </header>
        <div role="listbox" aria-label="Commands">
          {results.length ? (
            results.map((item) => (
              <button
                disabled={item.disabled}
                key={item.id}
                onClick={() => onSelect?.(item)}
                role="option"
                type="button"
              >
                <span>{item.icon}</span>
                <span>
                  <strong>{item.label}</strong>
                  {item.description && <small>{item.description}</small>}
                </span>
                {item.shortcut && <kbd>{item.shortcut}</kbd>}
              </button>
            ))
          ) : (
            <EmptyNotice title="No matching commands" description="Try a different search." />
          )}
        </div>
      </div>
    );
  }
);
CommandPalette.displayName = "CommandPalette";

export interface ContextDrawerProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
  open?: boolean;
  title?: ReactNode;
  description?: ReactNode;
  footer?: ReactNode;
  onOpenChange?: (open: boolean) => void;
}

export const ContextDrawer = forwardRef<HTMLElement, ContextDrawerProps>(
  (
    {
      children,
      className,
      description,
      footer,
      onOpenChange,
      open = true,
      title = "Context",
      ...props
    },
    ref
  ) => {
    const titleId = useId();
    if (!open) return null;
    return (
      <aside
        ref={ref}
        className={cn("amro-context-drawer", className)}
        aria-labelledby={titleId}
        {...props}
      >
        <header>
          <span>
            <strong id={titleId}>{title}</strong>
            {description && <small>{description}</small>}
          </span>
          <button
            aria-label="Close context drawer"
            onClick={() => onOpenChange?.(false)}
            type="button"
          >
            <PanelRightClose aria-hidden="true" />
          </button>
        </header>
        <div className="amro-context-drawer__body">{children}</div>
        {footer && <footer>{footer}</footer>}
      </aside>
    );
  }
);
ContextDrawer.displayName = "ContextDrawer";

export interface ActivityTimelineItem {
  id: string;
  title: ReactNode;
  detail?: ReactNode;
  timestamp?: ReactNode;
  status?: "complete" | "active" | "pending" | "error";
  icon?: ReactNode;
}

export interface ActivityTimelineProps extends HTMLAttributes<HTMLOListElement> {
  items?: readonly ActivityTimelineItem[];
}

const defaultTimeline: readonly ActivityTimelineItem[] = [
  {
    id: "one",
    title: "Research completed",
    detail: "12 sources verified",
    timestamp: "2 min ago",
    status: "complete"
  },
  {
    id: "two",
    title: "Drafting recommendation",
    detail: "Sales specialist is working",
    timestamp: "Now",
    status: "active"
  },
  { id: "three", title: "Human review", detail: "Waiting at control boundary", status: "pending" }
];

export const ActivityTimeline = forwardRef<HTMLOListElement, ActivityTimelineProps>(
  ({ className, items = defaultTimeline, ...props }, ref) => (
    <ol ref={ref} className={cn("amro-activity-timeline", className)} {...props}>
      {items.length ? (
        items.map((item) => (
          <li data-status={item.status ?? "pending"} key={item.id}>
            <span>
              {item.icon ??
                (item.status === "complete" ? (
                  <Check aria-hidden="true" />
                ) : (
                  <Clock3 aria-hidden="true" />
                ))}
            </span>
            <span>
              <strong>{item.title}</strong>
              {item.detail && <small>{item.detail}</small>}
            </span>
            {item.timestamp && <time>{item.timestamp}</time>}
          </li>
        ))
      ) : (
        <li>
          <EmptyNotice title="No activity yet" />
        </li>
      )}
    </ol>
  )
);
ActivityTimeline.displayName = "ActivityTimeline";

export interface AuditLogEntry {
  id: string;
  timestamp: ReactNode;
  actor: ReactNode;
  action: ReactNode;
  target?: ReactNode;
  outcome?: "success" | "pending" | "blocked" | "failed";
}

export interface AuditLogTableProps extends HTMLAttributes<HTMLDivElement> {
  entries?: readonly AuditLogEntry[];
  caption?: ReactNode;
  onOpenEntry?: (entry: AuditLogEntry) => void;
}

const defaultAuditEntries: readonly AuditLogEntry[] = [
  {
    id: "one",
    timestamp: "10:42",
    actor: "Sales agent",
    action: "Enriched account",
    target: "Northstar",
    outcome: "success"
  },
  {
    id: "two",
    timestamp: "10:45",
    actor: "Amro",
    action: "Requested approval",
    target: "Outbound sequence",
    outcome: "pending"
  }
];

export const AuditLogTable = forwardRef<HTMLDivElement, AuditLogTableProps>(
  (
    {
      caption = "Agent audit log",
      className,
      entries = defaultAuditEntries,
      onOpenEntry,
      ...props
    },
    ref
  ) => (
    <div ref={ref} className={cn("amro-audit-log", className)} {...props}>
      <table>
        <caption>{caption}</caption>
        <thead>
          <tr>
            <th scope="col">Time</th>
            <th scope="col">Actor</th>
            <th scope="col">Action</th>
            <th scope="col">Target</th>
            <th scope="col">Outcome</th>
            {onOpenEntry && (
              <th scope="col">
                <span className="amro-sr-only">Open</span>
              </th>
            )}
          </tr>
        </thead>
        <tbody>
          {entries.map((entry) => (
            <tr key={entry.id}>
              <td>{entry.timestamp}</td>
              <td>{entry.actor}</td>
              <td>{entry.action}</td>
              <td>{entry.target ?? "—"}</td>
              <td>
                <StatusIndicator
                  label={entry.outcome ?? "success"}
                  status={entry.outcome === "failed" ? "error" : (entry.outcome ?? "success")}
                />
              </td>
              {onOpenEntry && (
                <td>
                  <button
                    aria-label={`Open audit entry ${entry.id}`}
                    onClick={() => onOpenEntry(entry)}
                    type="button"
                  >
                    <ExternalLink aria-hidden="true" />
                  </button>
                </td>
              )}
            </tr>
          ))}
        </tbody>
      </table>
      {entries.length === 0 && (
        <EmptyNotice title="No audit events" description="Agent actions will be recorded here." />
      )}
    </div>
  )
);
AuditLogTable.displayName = "AuditLogTable";
```



## Usage

Shared product source installed automatically by AmroUI component entries.

