# AmroUI signature Flows

Installable AmroUI product component source.

## Installation

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

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

## Source

### source/components/signature-flows.tsx

```tsx
import { Check, ChevronLeft, ChevronRight, Mic, Pause, Play, ShieldCheck } from "lucide-react";
import {
  forwardRef,
  useMemo,
  useState,
  type HTMLAttributes,
  type KeyboardEvent,
  type ReactNode
} from "react";
import { cn } from "../lib/cn.js";
import { Badge } from "./badge.js";
import { Button } from "./button.js";
import { AssistantPresenceOrb, OmniPromptComposer } from "./conversational.js";
import { FeaturePanel, StatusIndicator, type AmroProcessStatus } from "./feature-primitives.js";
import { RangeInput } from "./input.js";

export interface WorkflowStep {
  id: string;
  label: ReactNode;
  description?: ReactNode;
  status: AmroProcessStatus;
}

export interface WorkflowStepRailProps extends HTMLAttributes<HTMLOListElement> {
  steps?: readonly WorkflowStep[];
  orientation?: "horizontal" | "vertical";
}

const defaultSteps: readonly WorkflowStep[] = [
  { id: "brief", label: "Brief", status: "complete" },
  { id: "research", label: "Research", status: "active" },
  { id: "review", label: "Human review", status: "pending" }
];

export const WorkflowStepRail = forwardRef<HTMLOListElement, WorkflowStepRailProps>(
  ({ className, orientation = "horizontal", steps = defaultSteps, ...props }, ref) => (
    <ol
      ref={ref}
      aria-label="Workflow progress"
      className={cn("amro-step-rail", `amro-step-rail--${orientation}`, className)}
      {...props}
    >
      {steps.map((step, index) => (
        <li aria-current={step.status === "active" ? "step" : undefined} key={step.id}>
          <span className="amro-step-rail__marker" data-status={step.status}>
            {step.status === "complete" ? <Check aria-hidden="true" /> : index + 1}
          </span>
          <span className="amro-step-rail__copy">
            <strong>{step.label}</strong>
            {step.description !== undefined && <small>{step.description}</small>}
          </span>
        </li>
      ))}
    </ol>
  )
);
WorkflowStepRail.displayName = "WorkflowStepRail";

export type ResearchProgressRailProps = WorkflowStepRailProps;
export const ResearchProgressRail = WorkflowStepRail;

export type EditorialPipelineProps = WorkflowStepRailProps;
export const EditorialPipeline = WorkflowStepRail;

export type CourseGenerationPipelineProps = WorkflowStepRailProps;
export const CourseGenerationPipeline = WorkflowStepRail;

export type GenerationProgressStoryProps = WorkflowStepRailProps;
export const GenerationProgressStory = WorkflowStepRail;

export type MeetCreationProgressProps = WorkflowStepRailProps;
export const MeetCreationProgress = WorkflowStepRail;

export interface BookingCalendarProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  monthLabel?: string;
  days?: readonly number[];
  availableDays?: readonly number[];
  value?: number;
  onChange?: (day: number) => void;
  onPreviousMonth?: () => void;
  onNextMonth?: () => void;
}

export const BookingCalendar = forwardRef<HTMLDivElement, BookingCalendarProps>(
  (
    {
      availableDays = [4, 5, 8, 11, 12, 15, 18, 19, 22, 25, 26, 29],
      className,
      days = Array.from({ length: 30 }, (_, index) => index + 1),
      monthLabel = "August 2026",
      onChange,
      onNextMonth,
      onPreviousMonth,
      value,
      ...props
    },
    ref
  ) => {
    const [internalValue, setInternalValue] = useState(value ?? availableDays[0]);
    const selected = value ?? internalValue;
    const available = useMemo(() => new Set(availableDays), [availableDays]);
    const select = (day: number) => {
      if (!available.has(day)) return;
      setInternalValue(day);
      onChange?.(day);
    };
    const move = (event: KeyboardEvent<HTMLButtonElement>, day: number) => {
      const offset =
        event.key === "ArrowRight"
          ? 1
          : event.key === "ArrowLeft"
            ? -1
            : event.key === "ArrowDown"
              ? 7
              : event.key === "ArrowUp"
                ? -7
                : 0;
      if (offset === 0) return;
      event.preventDefault();
      const candidates = days.filter((candidate) => available.has(candidate));
      const current = candidates.indexOf(day);
      const next =
        candidates[Math.max(0, Math.min(candidates.length - 1, current + Math.sign(offset)))] ??
        day;
      select(next);
      const grid = event.currentTarget.parentElement;
      requestAnimationFrame(() =>
        grid?.querySelector<HTMLButtonElement>(`[data-amro-day="${next}"]`)?.focus()
      );
    };

    return (
      <div ref={ref} className={cn("amro-booking-calendar", className)} {...props}>
        <header>
          <button aria-label="Previous month" onClick={onPreviousMonth} type="button">
            <ChevronLeft aria-hidden="true" />
          </button>
          <strong aria-live="polite">{monthLabel}</strong>
          <button aria-label="Next month" onClick={onNextMonth} type="button">
            <ChevronRight aria-hidden="true" />
          </button>
        </header>
        <div aria-label={monthLabel} className="amro-booking-calendar__grid" role="grid">
          {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((label) => (
            <span aria-hidden="true" key={label}>
              {label}
            </span>
          ))}
          {days.map((day) => (
            <button
              aria-label={`${monthLabel} ${day}${available.has(day) ? ", available" : ", unavailable"}`}
              aria-selected={selected === day}
              data-amro-day={day}
              disabled={!available.has(day)}
              key={day}
              onClick={() => select(day)}
              onKeyDown={(event) => move(event, day)}
              role="gridcell"
              tabIndex={selected === day ? 0 : -1}
              type="button"
            >
              {day}
            </button>
          ))}
        </div>
      </div>
    );
  }
);
BookingCalendar.displayName = "BookingCalendar";

export interface BeforeAfterCompareProps extends HTMLAttributes<HTMLDivElement> {
  before?: ReactNode;
  after?: ReactNode;
  value?: number;
  onValueChange?: (value: number) => void;
}

export const BeforeAfterCompare = forwardRef<HTMLDivElement, BeforeAfterCompareProps>(
  (
    {
      after = "Enhanced image",
      before = "Original image",
      className,
      onValueChange,
      value,
      ...props
    },
    ref
  ) => {
    const [internalValue, setInternalValue] = useState(50);
    const current = value ?? internalValue;
    const update = (next: number) => {
      setInternalValue(next);
      onValueChange?.(next);
    };
    return (
      <div ref={ref} className={cn("amro-compare", className)} {...props}>
        <div className="amro-compare__canvas">
          <div className="amro-compare__before">
            <Badge>Before</Badge>
            {before}
          </div>
          <div
            className="amro-compare__after"
            style={{ clipPath: `inset(0 ${100 - current}% 0 0)` }}
          >
            <Badge tone="info">After</Badge>
            {after}
          </div>
          <span className="amro-compare__divider" style={{ left: `${current}%` }} />
        </div>
        <label>
          <span>Comparison position</span>
          <RangeInput
            aria-label="Comparison position"
            max={100}
            min={0}
            onChange={(event) => update(event.currentTarget.valueAsNumber)}
            value={current}
          />
        </label>
      </div>
    );
  }
);
BeforeAfterCompare.displayName = "BeforeAfterCompare";

export interface WeeklyAvailabilityGridProps extends HTMLAttributes<HTMLDivElement> {
  days?: readonly string[];
  slots?: readonly string[];
  value?: readonly string[];
  onValueChange?: (slots: string[]) => void;
}

export const WeeklyAvailabilityGrid = forwardRef<HTMLDivElement, WeeklyAvailabilityGridProps>(
  (
    {
      className,
      days = ["Mon", "Tue", "Wed", "Thu", "Fri"],
      onValueChange,
      slots = ["09:00", "13:00", "17:00"],
      value,
      ...props
    },
    ref
  ) => {
    const [internal, setInternal] = useState<string[]>(["Mon-09:00", "Tue-13:00", "Wed-09:00"]);
    const selected = value ?? internal;
    const toggle = (id: string) => {
      const next = selected.includes(id)
        ? selected.filter((item) => item !== id)
        : [...selected, id];
      setInternal(next);
      onValueChange?.(next);
    };
    return (
      <div ref={ref} className={cn("amro-availability", className)} {...props}>
        <span className="amro-visually-hidden">Select available weekly time slots</span>
        <div className="amro-availability__grid">
          {days.flatMap((day) =>
            slots.map((slot) => {
              const id = `${day}-${slot}`;
              return (
                <button
                  aria-pressed={selected.includes(id)}
                  key={id}
                  onClick={() => toggle(id)}
                  type="button"
                >
                  <strong>{day}</strong>
                  <span>{slot}</span>
                </button>
              );
            })
          )}
        </div>
      </div>
    );
  }
);
WeeklyAvailabilityGrid.displayName = "WeeklyAvailabilityGrid";

export interface PublicationGateProps extends HTMLAttributes<HTMLDivElement> {
  checks?: readonly { id: string; label: ReactNode; passed: boolean }[];
  onPublish?: () => void;
  onResolve?: () => void;
}

export const PublicationGate = forwardRef<HTMLDivElement, PublicationGateProps>(
  (
    {
      checks = [
        { id: "facts", label: "Claims verified", passed: true },
        { id: "approval", label: "Editor approval", passed: false }
      ],
      className,
      onPublish,
      onResolve,
      ...props
    },
    ref
  ) => {
    const blocked = checks.some((check) => !check.passed);
    return (
      <FeaturePanel
        ref={ref}
        className={cn("amro-publication-gate", className)}
        eyebrow="Editorial control"
        title="Publication gate"
        status={
          <StatusIndicator
            label={blocked ? "Blocked" : "Ready"}
            status={blocked ? "blocked" : "complete"}
          />
        }
        {...props}
      >
        <ul>
          {checks.map((check) => (
            <li key={check.id}>
              <StatusIndicator label={check.label} status={check.passed ? "complete" : "blocked"} />
            </li>
          ))}
        </ul>
        <div className="amro-action-row">
          {blocked && (
            <Button onClick={onResolve} variant="secondary">
              Resolve blockers
            </Button>
          )}
          <Button
            disabled={blocked}
            onClick={() => {
              if (!blocked) onPublish?.();
            }}
          >
            Publish
          </Button>
        </div>
      </FeaturePanel>
    );
  }
);
PublicationGate.displayName = "PublicationGate";

export interface AssistantCommandCenterProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "status" | "onSubmit"
> {
  assistantName?: string;
  status?: "idle" | "listening" | "thinking" | "speaking" | "error";
  onSubmit?: (message: string) => void;
}

export const AssistantCommandCenter = forwardRef<HTMLDivElement, AssistantCommandCenterProps>(
  (
    {
      assistantName = "Amro Assistant",
      className,
      onSubmit = () => {},
      status = "thinking",
      ...props
    },
    ref
  ) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-signature-flow", className)}
      eyebrow="AmroAgents"
      title="Assistant command center"
      status={<AssistantPresenceOrb size="sm" status={status} />}
      {...props}
    >
      <div className="amro-signature-flow__hero">
        <AssistantPresenceOrb status={status} />
        <div>
          <strong>{assistantName}</strong>
          <span>Coordinates knowledge, tools, approvals, and handoffs.</span>
        </div>
      </div>
      <WorkflowStepRail
        steps={[
          { id: "understand", label: "Understand", status: "complete" },
          { id: "act", label: "Act", status: "active" },
          { id: "approve", label: "Approve", status: "pending" }
        ]}
      />
      <OmniPromptComposer onSubmit={onSubmit} />
    </FeaturePanel>
  )
);
AssistantCommandCenter.displayName = "AssistantCommandCenter";

export interface HumanApprovalPipelineProps extends HTMLAttributes<HTMLDivElement> {
  onApprove?: () => void;
  onReject?: () => void;
}
export const HumanApprovalPipeline = forwardRef<HTMLDivElement, HumanApprovalPipelineProps>(
  ({ className, onApprove, onReject, ...props }, ref) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-signature-flow", className)}
      eyebrow="Control boundary"
      title="Human approval pipeline"
      status={<StatusIndicator status="blocked" label="Awaiting approval" />}
      {...props}
    >
      <WorkflowStepRail
        steps={[
          { id: "draft", label: "Agent draft", status: "complete" },
          { id: "review", label: "Human review", status: "active" },
          { id: "send", label: "External action", status: "blocked" }
        ]}
      />
      <div className="amro-approval-summary">
        <ShieldCheck aria-hidden="true" />
        <span>
          <strong>No action has been sent</strong>
          <small>Review the proposed change before it reaches a customer.</small>
        </span>
      </div>
      <div className="amro-action-row">
        <Button variant="secondary" onClick={onReject}>
          Request changes
        </Button>
        <Button onClick={onApprove}>Approve and continue</Button>
      </div>
    </FeaturePanel>
  )
);
HumanApprovalPipeline.displayName = "HumanApprovalPipeline";

export type EditorialQualityGateProps = PublicationGateProps;
export const EditorialQualityGate = PublicationGate;

export interface ConversationalLessonPlayerProps extends HTMLAttributes<HTMLDivElement> {
  lessonTitle?: string;
  playing?: boolean;
  onPlayingChange?: (playing: boolean) => void;
}
export const ConversationalLessonPlayer = forwardRef<
  HTMLDivElement,
  ConversationalLessonPlayerProps
>(
  (
    {
      className,
      lessonTitle = "Designing reliable AI workflows",
      onPlayingChange,
      playing,
      ...props
    },
    ref
  ) => {
    const [internal, setInternal] = useState(false);
    const active = playing ?? internal;
    const toggle = () => {
      setInternal(!active);
      onPlayingChange?.(!active);
    };
    return (
      <FeaturePanel
        ref={ref}
        className={cn("amro-signature-flow", className)}
        eyebrow="AmroAcademy"
        title={lessonTitle}
        status={
          <StatusIndicator
            label={active ? "Playing" : "Paused"}
            status={active ? "active" : "idle"}
          />
        }
        {...props}
      >
        <div className="amro-lesson-player">
          <AssistantPresenceOrb status={active ? "speaking" : "idle"} size="lg">
            <Mic aria-hidden="true" />
          </AssistantPresenceOrb>
          <Button aria-label={active ? "Pause lesson" : "Play lesson"} onClick={toggle}>
            {active ? <Pause aria-hidden="true" /> : <Play aria-hidden="true" />}{" "}
            {active ? "Pause" : "Play lesson"}
          </Button>
        </div>
        <blockquote>
          <mark>Human approval boundaries</mark> keep consequential actions observable and
          reversible.
        </blockquote>
        <OmniPromptComposer onSubmit={() => {}} placeholder="Ask your tutor about this lesson…" />
      </FeaturePanel>
    );
  }
);
ConversationalLessonPlayer.displayName = "ConversationalLessonPlayer";

export interface DurableGenerationCardProps extends HTMLAttributes<HTMLDivElement> {
  assetName?: string;
  version?: number;
  onDownload?: () => void;
  onRegenerate?: () => void;
}
export const DurableGenerationCard = forwardRef<HTMLDivElement, DurableGenerationCardProps>(
  (
    { assetName = "Campaign hero", className, onDownload, onRegenerate, version = 3, ...props },
    ref
  ) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-signature-flow", className)}
      eyebrow="AmroVisionAI"
      title={assetName}
      status={<StatusIndicator status="complete" label={`Saved · v${version}`} />}
      {...props}
    >
      <div
        className="amro-generated-asset"
        role="img"
        aria-label={`${assetName} generated preview`}
      >
        <span>Generated asset</span>
      </div>
      <div className="amro-action-row">
        <Button variant="secondary" onClick={onRegenerate}>
          Create variation
        </Button>
        <Button onClick={onDownload}>Download</Button>
      </div>
    </FeaturePanel>
  )
);
DurableGenerationCard.displayName = "DurableGenerationCard";

export interface ConversationToMeetingBridgeProps extends HTMLAttributes<HTMLDivElement> {
  summary?: ReactNode;
  onBook?: () => void;
}
export const ConversationToMeetingBridge = forwardRef<
  HTMLDivElement,
  ConversationToMeetingBridgeProps
>(
  (
    {
      className,
      onBook,
      summary = "Carry the goal, participants, and conversation context into a meeting.",
      ...props
    },
    ref
  ) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-signature-flow", className)}
      eyebrow="AmroAgents → AmroMeet"
      title="Continue in a meeting"
      status={<StatusIndicator status="online" label="Times available" />}
      {...props}
    >
      <p>{summary}</p>
      <BookingCalendar />
      <Button onClick={onBook}>Book selected time</Button>
    </FeaturePanel>
  )
);
ConversationToMeetingBridge.displayName = "ConversationToMeetingBridge";
```



## Usage

Shared product source installed automatically by AmroUI component entries.

