# AmroUI learning

Installable AmroUI product component source.

## Installation

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

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

## Source

### source/components/learning.tsx

```tsx
import {
  Award,
  BookOpen,
  Brain,
  Building2,
  CalendarDays,
  Car,
  Check,
  ChevronRight,
  CircleHelp,
  Clock3,
  Download,
  Flame,
  Footprints,
  GraduationCap,
  Headphones,
  Languages,
  Lightbulb,
  LockKeyhole,
  MessageCircle,
  Mic,
  Pause,
  Play,
  RotateCcw,
  Share2,
  ShieldCheck,
  Sparkles,
  Target,
  Trash2,
  UserCog,
  Users,
  Volume2,
  X
} from "lucide-react";
import { useState, type CSSProperties, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "../lib/cn.js";
import { Avatar } from "./avatar.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 { DateInput, Input, RangeInput, Select } from "./input.js";

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

export type VoiceTutorState = "idle" | "listening" | "thinking" | "speaking" | "paused" | "error";
export interface VoiceTutorOrbProps extends HTMLAttributes<HTMLDivElement> {
  state?: VoiceTutorState;
  tutorName?: ReactNode;
  level?: number;
  label?: ReactNode;
}
export function VoiceTutorOrb({
  className,
  label,
  level = 0.64,
  state = "listening",
  tutorName = "Amro Tutor",
  ...props
}: VoiceTutorOrbProps) {
  return (
    <div
      className={cn("amro-voice-tutor-orb", `amro-voice-tutor-orb--${state}`, className)}
      role="status"
      aria-label={`${getAccessibleText(tutorName, "Tutor")} is ${state}`}
      style={{ "--amro-wave-level": Math.max(0, Math.min(level, 1)) } as CSSProperties}
      {...props}
    >
      <div>
        <span aria-hidden="true" />
        <span aria-hidden="true" />
        <span aria-hidden="true" />
        <Mic aria-hidden="true" />
      </div>
      <strong>
        {label ??
          (state === "listening"
            ? "Listening…"
            : state === "thinking"
              ? "Thinking…"
              : state === "speaking"
                ? "Explaining…"
                : state)}
      </strong>
      <small>{tutorName}</small>
    </div>
  );
}

export interface VoiceLessonPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  section?: ReactNode;
  duration?: number;
  currentTime?: number;
  playing?: boolean;
  speed?: number;
  waveform?: readonly number[];
  transcriptVisible?: boolean;
  onPlayingChange?: (playing: boolean) => void;
  onSeek?: (seconds: number) => void;
  onSpeedChange?: (speed: number) => void;
  onTranscriptToggle?: () => void;
}
const lessonWave = [
  24, 42, 68, 36, 76, 54, 82, 46, 64, 38, 72, 58, 88, 52, 66, 40, 76, 48, 62, 34, 56, 30, 48, 26
];
function formatTime(seconds: number) {
  const minutes = Math.floor(seconds / 60);
  return `${minutes}:${Math.floor(seconds % 60)
    .toString()
    .padStart(2, "0")}`;
}
export function VoiceLessonPlayer({
  className,
  currentTime = 224,
  duration = 780,
  onPlayingChange,
  onSeek,
  onSpeedChange,
  onTranscriptToggle,
  playing = true,
  section = "The human approval boundary",
  speed = 1,
  title = "Governed AI agents",
  transcriptVisible = true,
  waveform = lessonWave,
  ...props
}: VoiceLessonPlayerProps) {
  return (
    <div className={cn("amro-voice-lesson", className)} {...props}>
      <header>
        <span>
          <span className="amro-eyebrow">Now learning</span>
          <strong>{title}</strong>
          <small>{section}</small>
        </span>
        <Badge tone="info">
          <Headphones aria-hidden="true" />
          Audio lesson
        </Badge>
      </header>
      <div className="amro-voice-lesson__transport">
        <button
          aria-label={playing ? "Pause lesson" : "Play lesson"}
          onClick={() => onPlayingChange?.(!playing)}
          type="button"
        >
          {playing ? <Pause aria-hidden="true" /> : <Play aria-hidden="true" />}
        </button>
        <div className="amro-voice-lesson__wave" aria-hidden="true">
          {waveform.map((height, index) => (
            <i
              data-played={index / waveform.length <= currentTime / duration}
              key={index}
              style={{ height: `${height}%` }}
            />
          ))}
        </div>
        <span>
          {formatTime(currentTime)} / {formatTime(duration)}
        </span>
      </div>
      <RangeInput
        aria-label="Lesson position"
        max={duration}
        min="0"
        onChange={(event) => onSeek?.(Number(event.target.value))}
        value={currentTime}
      />
      <footer>
        <button aria-pressed={transcriptVisible} onClick={onTranscriptToggle} type="button">
          <BookOpen aria-hidden="true" />
          Transcript
        </button>
        <label>
          <Volume2 aria-hidden="true" />
          <span className="amro-sr-only">Playback speed</span>
          <Select onChange={(event) => onSpeedChange?.(Number(event.target.value))} value={speed}>
            {[0.75, 1, 1.25, 1.5, 2].map((value) => (
              <option key={value} value={value}>
                {value}×
              </option>
            ))}
          </Select>
        </label>
      </footer>
    </div>
  );
}

export interface TranscriptSegment {
  id: string;
  text: string;
  start: number;
  end: number;
  speaker?: ReactNode;
}
export interface SyncedTranscriptProps extends HTMLAttributes<HTMLDivElement> {
  segments?: readonly TranscriptSegment[];
  currentTime?: number;
  mode?: "sentence" | "word";
  onSeek?: (seconds: number) => void;
}
const transcriptSegments: readonly TranscriptSegment[] = [
  {
    id: "one",
    text: "A governed AI agent can complete useful work without hiding how it reached an outcome.",
    start: 0,
    end: 8,
    speaker: "Tutor"
  },
  {
    id: "two",
    text: "The key design choice is where the agent must stop and ask a person to decide.",
    start: 8,
    end: 17,
    speaker: "Tutor"
  },
  {
    id: "three",
    text: "That pause is the human approval boundary.",
    start: 17,
    end: 22,
    speaker: "Tutor"
  }
];
export function SyncedTranscript({
  className,
  currentTime = 10,
  mode = "sentence",
  onSeek,
  segments = transcriptSegments,
  ...props
}: SyncedTranscriptProps) {
  return (
    <div className={cn("amro-synced-transcript", className)} data-mode={mode} {...props}>
      <header>
        <span>
          <strong>Transcript</strong>
          <small>{mode} highlighting</small>
        </span>
        <Badge tone="neutral">Synced</Badge>
      </header>
      <ol>
        {segments.map((segment) => {
          const active = currentTime >= segment.start && currentTime < segment.end;
          return (
            <li data-active={active} key={segment.id}>
              <button
                aria-current={active ? "true" : undefined}
                onClick={() => onSeek?.(segment.start)}
                type="button"
              >
                <time>{formatTime(segment.start)}</time>
                <span>
                  <small>{segment.speaker}</small>
                  <span>
                    {mode === "word" && active
                      ? segment.text.split(" ").map((word, index) => (
                          <mark
                            data-active={
                              index ===
                              Math.floor(
                                ((currentTime - segment.start) /
                                  Math.max(1, segment.end - segment.start)) *
                                  segment.text.split(" ").length
                              )
                            }
                            key={`${word}-${index}`}
                          >
                            {word}{" "}
                          </mark>
                        ))
                      : segment.text}
                  </span>
                </span>
              </button>
            </li>
          );
        })}
      </ol>
    </div>
  );
}

export interface AskTutorButtonProps extends ButtonProps {
  listening?: boolean;
  playbackActive?: boolean;
}
export function AskTutorButton({
  children,
  className,
  listening = false,
  playbackActive = true,
  ...props
}: AskTutorButtonProps) {
  return (
    <Button
      className={cn("amro-ask-tutor", listening && "amro-ask-tutor--listening", className)}
      {...props}
    >
      {listening ? (
        <>
          <span className="amro-ask-tutor__pulse" aria-hidden="true" />
          <Mic aria-hidden="true" />
          Listening—ask your question
        </>
      ) : (
        <>
          <MessageCircle aria-hidden="true" />
          {children ?? (playbackActive ? "Pause and ask tutor" : "Ask tutor")}
        </>
      )}
    </Button>
  );
}

export interface TutorTurn {
  id: string;
  role: "tutor" | "learner";
  content: ReactNode;
  timestamp?: ReactNode;
  lessonContext?: ReactNode;
}
export interface TutorConversationPanelProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "onSubmit"
> {
  turns?: readonly TutorTurn[];
  tutorName?: string;
  onSubmit?: (question: string) => void;
  onClose?: () => void;
}
const tutorTurns: readonly TutorTurn[] = [
  {
    id: "t1",
    role: "tutor",
    content: "A control boundary is the point where an agent pauses before an external action.",
    lessonContext: "Section 3 · Governance"
  },
  { id: "q1", role: "learner", content: "Does every email need approval?" },
  {
    id: "t2",
    role: "tutor",
    content:
      "Not necessarily. A team can pre-approve a narrow rule set, but anything outside it should pause."
  }
];
export function TutorConversationPanel({
  className,
  onClose,
  onSubmit,
  turns = tutorTurns,
  tutorName = "Amro Tutor",
  ...props
}: TutorConversationPanelProps) {
  const [draft, setDraft] = useState("");
  return (
    <div className={cn("amro-tutor-conversation", className)} {...props}>
      <header>
        <Avatar name={tutorName} />
        <span>
          <strong>{tutorName}</strong>
          <small>Grounded in this lesson</small>
        </span>
        <StatusIndicator label="Voice ready" status="online" />
        {onClose && (
          <button aria-label="Close tutor conversation" onClick={onClose} type="button">
            <X aria-hidden="true" />
          </button>
        )}
      </header>
      <div role="log" aria-label="Tutor conversation">
        {turns.map((turn) => (
          <article data-role={turn.role} key={turn.id}>
            <small>
              {turn.role === "tutor" ? tutorName : "You"}
              {turn.lessonContext
                ? ` · ${getAccessibleText(turn.lessonContext, "lesson context")}`
                : ""}
            </small>
            <p>{turn.content}</p>
          </article>
        ))}
      </div>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (draft.trim()) {
            onSubmit?.(draft);
            setDraft("");
          }
        }}
      >
        <input
          aria-label="Ask tutor"
          onChange={(event) => setDraft(event.target.value)}
          placeholder="Ask about this concept…"
          value={draft}
        />
        <button aria-label="Ask by voice" type="button">
          <Mic aria-hidden="true" />
        </button>
        <Button disabled={!draft.trim()} size="sm" type="submit">
          Ask
        </Button>
      </form>
    </div>
  );
}

export interface TutorResponseCardProps extends HTMLAttributes<HTMLDivElement> {
  explanation?: ReactNode;
  example?: ReactNode;
  analogy?: ReactNode;
  sourceSection?: ReactNode;
  onExplainDifferently?: () => void;
  onGoDeeper?: () => void;
}
export function TutorResponseCard({
  analogy = "Think of it like a checkout: the cart can be prepared automatically, but payment still waits for you.",
  className,
  example = "Drafting an email is safe; sending it to 500 people crosses the control boundary.",
  explanation = "A control boundary separates preparation from a consequential action.",
  onExplainDifferently,
  onGoDeeper,
  sourceSection = "Governance · 03:44",
  ...props
}: TutorResponseCardProps) {
  const [view, setView] = useState<"explanation" | "example" | "analogy">("explanation");
  const content = view === "example" ? example : view === "analogy" ? analogy : explanation;
  return (
    <FeaturePanel
      className={cn("amro-tutor-response", className)}
      eyebrow="Tutor response"
      title={content}
      description={sourceSection}
      status={<Badge tone="success">Lesson grounded</Badge>}
      {...props}
    >
      <div role="tablist">
        {(["explanation", "example", "analogy"] as const).map((item) => (
          <button
            aria-selected={view === item}
            key={item}
            onClick={() => setView(item)}
            role="tab"
            type="button"
          >
            {item}
          </button>
        ))}
      </div>
      <footer>
        {onExplainDifferently && (
          <Button onClick={onExplainDifferently} variant="secondary">
            <RotateCcw aria-hidden="true" />
            Explain differently
          </Button>
        )}
        {onGoDeeper && (
          <Button onClick={onGoDeeper}>
            Go deeper
            <ChevronRight aria-hidden="true" />
          </Button>
        )}
      </footer>
    </FeaturePanel>
  );
}

export type UnderstandingResponse = "got-it" | "not-yet" | "deeper";
export interface UnderstandingCheckProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  prompt?: ReactNode;
  value?: UnderstandingResponse;
  onChange?: (response: UnderstandingResponse) => void;
}
export function UnderstandingCheck({
  className,
  onChange,
  prompt = "How does that explanation feel?",
  value: controlled,
  ...props
}: UnderstandingCheckProps) {
  const [internal, setInternal] = useState<UnderstandingResponse>();
  const value = controlled ?? internal;
  const options: readonly { id: UnderstandingResponse; label: string; detail: string }[] = [
    { id: "got-it", label: "Got it", detail: "Continue the lesson" },
    { id: "not-yet", label: "Not yet", detail: "Try a simpler explanation" },
    { id: "deeper", label: "Go deeper", detail: "Explore the nuance" }
  ];
  return (
    <div className={cn("amro-understanding-check", className)} {...props}>
      <strong>{prompt}</strong>
      <div>
        {options.map((option) => (
          <button
            aria-pressed={value === option.id}
            key={option.id}
            onClick={() => {
              setInternal(option.id);
              onChange?.(option.id);
            }}
            type="button"
          >
            <span>
              {option.id === "got-it" ? (
                <Check aria-hidden="true" />
              ) : option.id === "not-yet" ? (
                <CircleHelp aria-hidden="true" />
              ) : (
                <Brain aria-hidden="true" />
              )}
            </span>
            <span>
              <strong>{option.label}</strong>
              <small>{option.detail}</small>
            </span>
          </button>
        ))}
      </div>
    </div>
  );
}

export interface KnowledgeChoice {
  id: string;
  label: ReactNode;
  correct?: boolean;
}
export interface KnowledgeCheckCardProps extends HTMLAttributes<HTMLDivElement> {
  question?: ReactNode;
  choices?: readonly KnowledgeChoice[];
  hint?: ReactNode;
  explanation?: ReactNode;
  onAnswer?: (choice: KnowledgeChoice, correct: boolean) => void;
}
const knowledgeChoices: readonly KnowledgeChoice[] = [
  { id: "a", label: "Whenever the agent starts researching" },
  { id: "b", label: "Before a consequential external action", correct: true },
  { id: "c", label: "Only after the action has completed" }
];
export function KnowledgeCheckCard({
  choices = knowledgeChoices,
  className,
  explanation = "The boundary belongs immediately before the consequential action, while the person can still change the outcome.",
  hint = "Think about when a decision is still reversible.",
  onAnswer,
  question = "Where should a human approval boundary appear?",
  ...props
}: KnowledgeCheckCardProps) {
  const [selected, setSelected] = useState<KnowledgeChoice>();
  const [showHint, setShowHint] = useState(false);
  const answered = selected !== undefined;
  return (
    <FeaturePanel
      className={cn("amro-knowledge-check", className)}
      eyebrow="Knowledge check"
      title={question}
      status={<Badge tone="neutral">1 concept</Badge>}
      {...props}
    >
      <div role="radiogroup" aria-label="Answers">
        {choices.map((choice) => (
          <button
            aria-checked={selected?.id === choice.id}
            data-result={
              selected?.id === choice.id ? (choice.correct ? "correct" : "incorrect") : undefined
            }
            disabled={answered}
            key={choice.id}
            onClick={() => {
              setSelected(choice);
              onAnswer?.(choice, Boolean(choice.correct));
            }}
            role="radio"
            type="button"
          >
            <span>{choice.id.toUpperCase()}</span>
            <span>{choice.label}</span>
            {selected?.id === choice.id &&
              (choice.correct ? <Check aria-hidden="true" /> : <X aria-hidden="true" />)}
          </button>
        ))}
      </div>
      {!answered && (
        <Button onClick={() => setShowHint((value) => !value)} variant="tertiary">
          <Lightbulb aria-hidden="true" />
          {showHint ? hint : "Show hint"}
        </Button>
      )}
      {answered && (
        <div className="amro-knowledge-check__explanation">
          <strong>{selected.correct ? "Correct" : "Not quite"}</strong>
          <p>{explanation}</p>
        </div>
      )}
    </FeaturePanel>
  );
}

export type ConfidenceLevel = 1 | 2 | 3 | 4 | 5;
export interface ConfidenceSelectorProps extends Omit<
  HTMLAttributes<HTMLFieldSetElement>,
  "onChange"
> {
  value?: ConfidenceLevel;
  onChange?: (level: ConfidenceLevel) => void;
  prompt?: ReactNode;
}
export function ConfidenceSelector({
  className,
  onChange,
  prompt = "How confident are you with this concept?",
  value: controlled,
  ...props
}: ConfidenceSelectorProps) {
  const [internal, setInternal] = useState<ConfidenceLevel>();
  const value = controlled ?? internal;
  const labels = [
    "Need help",
    "Uncertain",
    "Getting there",
    "Confident",
    "Could teach it"
  ] as const;
  return (
    <fieldset className={cn("amro-confidence-selector", className)} {...props}>
      <legend>{prompt}</legend>
      <div>
        {labels.map((label, index) => {
          const level = (index + 1) as ConfidenceLevel;
          return (
            <label data-selected={value === level} key={label}>
              <input
                checked={value === level}
                name="confidence"
                onChange={() => {
                  setInternal(level);
                  onChange?.(level);
                }}
                type="radio"
              />
              <span>{level}</span>
              <small>{label}</small>
            </label>
          );
        })}
      </div>
    </fieldset>
  );
}

export interface LearningCompanionCardProps extends HTMLAttributes<HTMLDivElement> {
  tutor?: string;
  subject?: ReactNode;
  language?: ReactNode;
  sections?: number;
  completedSections?: number;
  durationMinutes?: number;
  onContinue?: () => void;
}
export function LearningCompanionCard({
  className,
  completedSections = 4,
  durationMinutes = 48,
  language = "English",
  onContinue,
  sections = 8,
  subject = "Governed AI agents",
  tutor = "Amro Tutor",
  ...props
}: LearningCompanionCardProps) {
  const progress = Math.round((completedSections / Math.max(1, sections)) * 100);
  return (
    <Card className={cn("amro-learning-companion", className)} {...props}>
      <header>
        <Avatar name={tutor} />
        <span>
          <span className="amro-eyebrow">Your learning companion</span>
          <strong>{tutor}</strong>
          <small>{subject}</small>
        </span>
        <StatusIndicator label="Tutor ready" status="online" />
      </header>
      <div>
        <span>
          <Languages aria-hidden="true" />
          {language}
        </span>
        <span>
          <BookOpen aria-hidden="true" />
          {completedSections}/{sections} sections
        </span>
        <span>
          <Clock3 aria-hidden="true" />
          {durationMinutes} min
        </span>
      </div>
      <progress max="100" value={progress}>
        {progress}%
      </progress>
      <footer>
        <span>
          <strong>{progress}%</strong>
          <small>Lesson progress</small>
        </span>
        {onContinue && (
          <Button onClick={onContinue}>
            Continue learning
            <Play aria-hidden="true" />
          </Button>
        )}
      </footer>
    </Card>
  );
}

export interface ProgrammeSession {
  id: string;
  title: ReactNode;
  duration?: ReactNode;
  complete?: boolean;
  locked?: boolean;
}
export interface InstructorProgrammeCardProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "title"
> {
  title?: ReactNode;
  instructor?: ReactNode;
  description?: ReactNode;
  sessions?: readonly ProgrammeSession[];
  prerequisites?: readonly ReactNode[];
  enrolled?: boolean;
  onEnroll?: () => void;
}
const programmeSessions: readonly ProgrammeSession[] = [
  { id: "one", title: "Agent foundations", duration: "35 min", complete: true },
  { id: "two", title: "Control boundaries", duration: "42 min" },
  { id: "three", title: "Outcome measurement", duration: "38 min", locked: true }
];
export function InstructorProgrammeCard({
  className,
  description = "Build practical skill in designing governed, outcome-driven AI workflows.",
  enrolled = false,
  instructor = "Dr. Maya Chen",
  onEnroll,
  prerequisites = ["Basic AI literacy"],
  sessions = programmeSessions,
  title = "Governed AI Operations",
  ...props
}: InstructorProgrammeCardProps) {
  return (
    <FeaturePanel
      className={cn("amro-programme-card", className)}
      eyebrow="Instructor programme"
      title={title}
      description={description}
      status={
        <Badge tone={enrolled ? "success" : "neutral"}>
          {enrolled ? "Enrolled" : `${sessions.length} sessions`}
        </Badge>
      }
      {...props}
    >
      <div className="amro-programme-card__instructor">
        <Avatar name={getAccessibleText(instructor, "Instructor")} />
        <span>
          <small>Instructor</small>
          <strong>{instructor}</strong>
        </span>
      </div>
      <ol>
        {sessions.map((session, index) => (
          <li key={session.id}>
            <span>
              {session.complete ? (
                <Check aria-hidden="true" />
              ) : session.locked ? (
                <LockKeyhole aria-hidden="true" />
              ) : (
                index + 1
              )}
            </span>
            <strong>{session.title}</strong>
            <small>{session.duration}</small>
          </li>
        ))}
      </ol>
      <footer>
        <span>
          <small>Prerequisites</small>
          {prerequisites.map((item, index) => (
            <Badge key={index} tone="neutral">
              {item}
            </Badge>
          ))}
        </span>
        {!enrolled && onEnroll && <Button onClick={onEnroll}>Enrol in programme</Button>}
      </footer>
    </FeaturePanel>
  );
}

export interface CourseBrief {
  topic: string;
  audience: string;
  outcome: string;
  sections: number;
  language: string;
  audio: boolean;
  quizzes: boolean;
}
export interface TopicToCourseComposerProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onSubmit" | "defaultValue"
> {
  defaultValue?: Partial<CourseBrief>;
  onSubmit?: (brief: CourseBrief) => void;
}
export function TopicToCourseComposer({
  className,
  defaultValue,
  onSubmit,
  ...props
}: TopicToCourseComposerProps) {
  const [brief, setBrief] = useState<CourseBrief>({
    topic: defaultValue?.topic ?? "Governed AI agents",
    audience: defaultValue?.audience ?? "Product and operations leaders",
    outcome: defaultValue?.outcome ?? "Design a safe autonomous workflow",
    sections: defaultValue?.sections ?? 6,
    language: defaultValue?.language ?? "English",
    audio: defaultValue?.audio ?? true,
    quizzes: defaultValue?.quizzes ?? true
  });
  const update = <K extends keyof CourseBrief>(key: K, value: CourseBrief[K]) =>
    setBrief((current) => ({ ...current, [key]: value }));
  return (
    <form
      className={cn("amro-course-composer", className)}
      onSubmit={(event) => {
        event.preventDefault();
        onSubmit?.(brief);
      }}
      {...props}
    >
      <FeaturePanel
        eyebrow="Topic to course"
        title="Create a structured learning experience"
        description="Generate sections, tutor audio, translation, and knowledge checks."
      >
        <div>
          <Input
            label="Topic"
            value={brief.topic}
            onChange={(event) => update("topic", event.target.value)}
          />
          <Input
            label="Learner audience"
            value={brief.audience}
            onChange={(event) => update("audience", event.target.value)}
          />
          <Input
            label="Desired outcome"
            value={brief.outcome}
            onChange={(event) => update("outcome", event.target.value)}
          />
          <Input
            label="Sections"
            min="2"
            max="20"
            type="number"
            value={brief.sections}
            onChange={(event) => update("sections", Number(event.target.value))}
          />
          <Input
            label="Language"
            value={brief.language}
            onChange={(event) => update("language", event.target.value)}
          />
        </div>
        <footer>
          <label>
            <input
              checked={brief.audio}
              onChange={(event) => update("audio", event.target.checked)}
              type="checkbox"
            />
            Tutor audio
          </label>
          <label>
            <input
              checked={brief.quizzes}
              onChange={(event) => update("quizzes", event.target.checked)}
              type="checkbox"
            />
            Knowledge checks
          </label>
          <Button disabled={!brief.topic.trim() || !brief.outcome.trim()} type="submit">
            Generate course
            <Sparkles aria-hidden="true" />
          </Button>
        </footer>
      </FeaturePanel>
    </form>
  );
}

export interface MyJourneyResumeCardProps extends HTMLAttributes<HTMLDivElement> {
  course?: ReactNode;
  section?: ReactNode;
  positionSeconds?: number;
  durationSeconds?: number;
  progress?: number;
  nextAction?: ReactNode;
  lastStudied?: ReactNode;
  onResume?: () => void;
}
export function MyJourneyResumeCard({
  className,
  course = "Governed AI Operations",
  durationSeconds = 780,
  lastStudied = "Yesterday",
  nextAction = "Finish the control-boundary check",
  onResume,
  positionSeconds = 224,
  progress = 56,
  section = "The human approval boundary",
  ...props
}: MyJourneyResumeCardProps) {
  return (
    <Card className={cn("amro-journey-resume", className)} {...props}>
      <header>
        <span>
          <span className="amro-eyebrow">Continue your journey</span>
          <strong>{course}</strong>
          <small>{section}</small>
        </span>
        <Badge tone="neutral">{lastStudied}</Badge>
      </header>
      <div>
        <span className="amro-journey-resume__thumb">
          <Play aria-hidden="true" />
        </span>
        <span>
          <strong>
            {formatTime(positionSeconds)} of {formatTime(durationSeconds)}
          </strong>
          <small>Exact audio position saved</small>
          <progress max="100" value={progress}>
            {progress}%
          </progress>
        </span>
      </div>
      <footer>
        <span>
          <small>Next learning action</small>
          <strong>{nextAction}</strong>
        </span>
        {onResume && (
          <Button onClick={onResume}>
            Resume lesson
            <Play aria-hidden="true" />
          </Button>
        )}
      </footer>
    </Card>
  );
}

export interface LearningMapSection {
  id: string;
  title: ReactNode;
  concepts: number;
  completedConcepts: number;
  checks: number;
  completedChecks: number;
  status?: "complete" | "active" | "locked";
}
export interface LearningSessionMapProps extends Omit<
  HTMLAttributes<HTMLOListElement>,
  "onSelect"
> {
  sections?: readonly LearningMapSection[];
  onSelect?: (section: LearningMapSection) => void;
}
const learningMap: readonly LearningMapSection[] = [
  {
    id: "one",
    title: "Agent foundations",
    concepts: 4,
    completedConcepts: 4,
    checks: 2,
    completedChecks: 2,
    status: "complete"
  },
  {
    id: "two",
    title: "Control boundaries",
    concepts: 5,
    completedConcepts: 3,
    checks: 3,
    completedChecks: 1,
    status: "active"
  },
  {
    id: "three",
    title: "Outcome measurement",
    concepts: 4,
    completedConcepts: 0,
    checks: 2,
    completedChecks: 0,
    status: "locked"
  }
];
export function LearningSessionMap({
  className,
  onSelect,
  sections = learningMap,
  ...props
}: LearningSessionMapProps) {
  return (
    <ol className={cn("amro-learning-map", className)} {...props}>
      {sections.map((section, index) => (
        <li data-status={section.status ?? "active"} key={section.id}>
          <button
            disabled={section.status === "locked"}
            onClick={() => onSelect?.(section)}
            type="button"
          >
            <span>
              {section.status === "complete" ? (
                <Check aria-hidden="true" />
              ) : section.status === "locked" ? (
                <LockKeyhole aria-hidden="true" />
              ) : (
                index + 1
              )}
            </span>
            <span>
              <strong>{section.title}</strong>
              <small>
                {section.completedConcepts}/{section.concepts} concepts · {section.completedChecks}/
                {section.checks} checks
              </small>
              <progress
                max={section.concepts + section.checks}
                value={section.completedConcepts + section.completedChecks}
              />
            </span>
            <ChevronRight aria-hidden="true" />
          </button>
        </li>
      ))}
    </ol>
  );
}

export interface MasteryProgressRingProps extends HTMLAttributes<HTMLDivElement> {
  completion?: number;
  mastery?: number;
  label?: ReactNode;
  detail?: ReactNode;
}
export function MasteryProgressRing({
  className,
  completion = 72,
  detail = "8 of 11 concepts demonstrated",
  label = "Course mastery",
  mastery = 61,
  ...props
}: MasteryProgressRingProps) {
  return (
    <div className={cn("amro-mastery-ring", className)} {...props}>
      <div
        style={
          {
            "--amro-completion": `${completion * 3.6}deg`,
            "--amro-mastery": `${mastery * 3.6}deg`
          } as CSSProperties
        }
      >
        <span>
          <strong>{mastery}%</strong>
          <small>mastery</small>
        </span>
      </div>
      <span>
        <strong>{label}</strong>
        <small>{detail}</small>
        <span>
          <i data-kind="completion" />
          {completion}% completed
        </span>
        <span>
          <i data-kind="mastery" />
          {mastery}% demonstrated
        </span>
      </span>
    </div>
  );
}

export interface MasteryAxis {
  id: string;
  label: ReactNode;
  score: number;
  target?: number;
}
export interface SkillMasteryRadarProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "title" | "onSelect"
> {
  axes?: readonly MasteryAxis[];
  title?: ReactNode;
  onSelect?: (axis: MasteryAxis) => void;
}
const masteryAxes: readonly MasteryAxis[] = [
  { id: "concepts", label: "Concepts", score: 82, target: 80 },
  { id: "application", label: "Application", score: 68, target: 75 },
  { id: "judgment", label: "Judgment", score: 74, target: 80 },
  { id: "recall", label: "Recall", score: 91, target: 75 },
  { id: "explanation", label: "Explanation", score: 63, target: 70 }
];
export function SkillMasteryRadar({
  axes = masteryAxes,
  className,
  onSelect,
  title = "Skill mastery",
  ...props
}: SkillMasteryRadarProps) {
  const points = axes
    .map((axis, index) => {
      const angle = (Math.PI * 2 * index) / axes.length - Math.PI / 2;
      const radius = axis.score * 0.82;
      return `${100 + Math.cos(angle) * radius},${100 + Math.sin(angle) * radius}`;
    })
    .join(" ");
  return (
    <div className={cn("amro-skill-radar", className)} {...props}>
      <header>
        <span>
          <strong>{title}</strong>
          <small>Demonstrated understanding by dimension</small>
        </span>
        <Badge tone="info">
          {Math.round(axes.reduce((sum, item) => sum + item.score, 0) / axes.length)}% overall
        </Badge>
      </header>
      <div>
        <svg aria-label="Mastery radar chart" role="img" viewBox="0 0 200 200">
          <polygon className="amro-skill-radar__grid" points="100,12 184,73 152,170 48,170 16,73" />
          <polygon className="amro-skill-radar__shape" points={points} />
        </svg>
        <ul>
          {axes.map((axis) => (
            <li key={axis.id}>
              <button onClick={() => onSelect?.(axis)} type="button">
                <span>{axis.label}</span>
                <strong>{axis.score}%</strong>
                <small>
                  {axis.score >= (axis.target ?? 75)
                    ? "Strength"
                    : `${(axis.target ?? 75) - axis.score}% gap`}
                </small>
              </button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export type AdaptiveBranch = "simpler" | "revision" | "advanced";
export interface AdaptivePathCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  reason?: ReactNode;
  recommended?: AdaptiveBranch;
  options?: readonly {
    id: AdaptiveBranch;
    title: ReactNode;
    detail: ReactNode;
    duration?: ReactNode;
  }[];
  onSelect?: (branch: AdaptiveBranch) => void;
}
const adaptiveOptions = [
  {
    id: "simpler" as const,
    title: "Simpler explanation",
    detail: "Rebuild the concept with a concrete analogy",
    duration: "4 min"
  },
  {
    id: "revision" as const,
    title: "Quick revision",
    detail: "Revisit the two missed ideas",
    duration: "7 min"
  },
  {
    id: "advanced" as const,
    title: "Advanced branch",
    detail: "Apply the concept to a harder scenario",
    duration: "10 min"
  }
];
export function AdaptivePathCard({
  className,
  onSelect,
  options = adaptiveOptions,
  reason = "Your answers show strong recall but lower confidence applying this concept.",
  recommended = "revision",
  ...props
}: AdaptivePathCardProps) {
  return (
    <FeaturePanel
      className={cn("amro-adaptive-path", className)}
      eyebrow="Adaptive next step"
      title="Choose your best path"
      description={reason}
      status={
        <Badge tone="info">
          <Sparkles aria-hidden="true" />
          Personalised
        </Badge>
      }
      {...props}
    >
      <div>
        {options.map((option) => (
          <button
            data-recommended={option.id === recommended}
            key={option.id}
            onClick={() => onSelect?.(option.id)}
            type="button"
          >
            <span>
              {option.id === "simpler" ? (
                <Lightbulb aria-hidden="true" />
              ) : option.id === "revision" ? (
                <RotateCcw aria-hidden="true" />
              ) : (
                <Brain aria-hidden="true" />
              )}
            </span>
            <span>
              <strong>{option.title}</strong>
              <small>{option.detail}</small>
            </span>
            <Badge tone={option.id === recommended ? "success" : "neutral"}>
              {option.id === recommended ? "Recommended" : option.duration}
            </Badge>
          </button>
        ))}
      </div>
    </FeaturePanel>
  );
}

export interface LearningStreakTileProps extends HTMLAttributes<HTMLDivElement> {
  days?: number;
  activeDays?: readonly number[];
  weeklyGoal?: number;
  sessionsThisWeek?: number;
  message?: ReactNode;
}
export function LearningStreakTile({
  activeDays = [1, 2, 4, 5],
  className,
  days = 6,
  message = "A short session today keeps your momentum.",
  sessionsThisWeek = 4,
  weeklyGoal = 5,
  ...props
}: LearningStreakTileProps) {
  return (
    <Card className={cn("amro-learning-streak", className)} {...props}>
      <header>
        <span>
          <Flame aria-hidden="true" />
        </span>
        <span>
          <strong>{days} day streak</strong>
          <small>{message}</small>
        </span>
        <Badge tone="neutral">
          {sessionsThisWeek}/{weeklyGoal} this week
        </Badge>
      </header>
      <div aria-label="Learning activity this week">
        {["M", "T", "W", "T", "F", "S", "S"].map((day, index) => (
          <span data-active={activeDays.includes(index + 1)} key={`${day}-${index}`}>
            <i>{activeDays.includes(index + 1) ? <Check aria-hidden="true" /> : null}</i>
            <small>{day}</small>
          </span>
        ))}
      </div>
    </Card>
  );
}

export interface BadgeEarnedToastProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  points?: number;
  icon?: ReactNode;
  onView?: () => void;
  onDismiss?: () => void;
}
export function BadgeEarnedToast({
  className,
  description = "You demonstrated the control-boundary concept in two scenarios.",
  icon = <Award aria-hidden="true" />,
  onDismiss,
  onView,
  points = 120,
  title = "Governance guide",
  ...props
}: BadgeEarnedToastProps) {
  return (
    <div className={cn("amro-badge-toast", className)} role="status" {...props}>
      <span>{icon}</span>
      <span>
        <span className="amro-eyebrow">Badge earned · +{points} XP</span>
        <strong>{title}</strong>
        <small>{description}</small>
      </span>
      {onView && (
        <Button onClick={onView} size="sm" variant="secondary">
          View badge
        </Button>
      )}
      {onDismiss && (
        <button aria-label="Dismiss badge" onClick={onDismiss} type="button">
          <X aria-hidden="true" />
        </button>
      )}
    </div>
  );
}

export interface CertificateCardProps extends HTMLAttributes<HTMLDivElement> {
  course?: ReactNode;
  learner?: ReactNode;
  issuedAt?: ReactNode;
  credentialId?: string;
  verified?: boolean;
  onVerify?: () => void;
  onShare?: () => void;
  onDownload?: () => void;
}
export function CertificateCard({
  className,
  course = "Governed AI Operations",
  credentialId = "AMRO-GOV-2026-1842",
  issuedAt = "5 August 2026",
  learner = "Maya Chen",
  onDownload,
  onShare,
  onVerify,
  verified = true,
  ...props
}: CertificateCardProps) {
  return (
    <Card className={cn("amro-certificate-card", className)} {...props}>
      <div className="amro-certificate-card__preview">
        <span>
          <Award aria-hidden="true" />
        </span>
        <span className="amro-eyebrow">Certificate of mastery</span>
        <h3>{course}</h3>
        <p>
          Awarded to <strong>{learner}</strong>
        </p>
        <small>
          Issued {issuedAt} · {credentialId}
        </small>
      </div>
      <footer>
        <StatusIndicator
          label={verified ? "Verified credential" : "Verification pending"}
          status={verified ? "success" : "pending"}
        />
        <div>
          {onVerify && (
            <Button onClick={onVerify} variant="tertiary">
              Verify
            </Button>
          )}
          {onShare && (
            <Button onClick={onShare} variant="secondary">
              <Share2 aria-hidden="true" />
              Share
            </Button>
          )}
          {onDownload && (
            <Button onClick={onDownload}>
              <Download aria-hidden="true" />
              Download
            </Button>
          )}
        </div>
      </footer>
    </Card>
  );
}

export interface LearningLanguages {
  content: string;
  transcript: string;
  voice: string;
}
export interface LanguageSwitcherProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  value?: LearningLanguages;
  languages?: readonly string[];
  onChange?: (value: LearningLanguages) => void;
}
export function LanguageSwitcher({
  className,
  languages = ["English", "Finnish", "Hindi", "Spanish", "German"],
  onChange,
  value = { content: "English", transcript: "English", voice: "English" },
  ...props
}: LanguageSwitcherProps) {
  const [selection, setSelection] = useState(value);
  const update = (key: keyof LearningLanguages, next: string) => {
    const updated = { ...selection, [key]: next };
    setSelection(updated);
    onChange?.(updated);
  };
  return (
    <FeaturePanel
      className={cn("amro-language-switcher", className)}
      eyebrow="Lesson language"
      title="Control each learning layer"
      description="Content, transcript, and tutor voice can use different languages."
      status={<Languages aria-hidden="true" />}
      {...props}
    >
      <div>
        {(["content", "transcript", "voice"] as const).map((key) => (
          <label key={key}>
            <span>
              <strong>
                {key === "content"
                  ? "Lesson content"
                  : key === "transcript"
                    ? "Transcript"
                    : "Tutor voice"}
              </strong>
              <small>
                {key === "voice" ? "Audio regenerates when changed" : "Available immediately"}
              </small>
            </span>
            <Select onChange={(event) => update(key, event.target.value)} value={selection[key]}>
              {languages.map((language) => (
                <option key={language}>{language}</option>
              ))}
            </Select>
          </label>
        ))}
      </div>
    </FeaturePanel>
  );
}

export interface HandsFreeModeProps extends HTMLAttributes<HTMLDivElement> {
  playing?: boolean;
  listening?: boolean;
  section?: ReactNode;
  elapsed?: ReactNode;
  onPlayChange?: (playing: boolean) => void;
  onAsk?: () => void;
  onPrevious?: () => void;
  onNext?: () => void;
}
export function HandsFreeMode({
  className,
  elapsed = "03:44 / 12:58",
  listening = false,
  onAsk,
  onNext,
  onPlayChange,
  onPrevious,
  playing = true,
  section = "The human approval boundary",
  ...props
}: HandsFreeModeProps) {
  return (
    <div className={cn("amro-hands-free", className)} {...props}>
      <header>
        <span>
          <span className="amro-eyebrow">Hands-free lesson</span>
          <strong>{section}</strong>
          <small>{elapsed}</small>
        </span>
        <StatusIndicator
          label={listening ? "Listening" : playing ? "Playing" : "Paused"}
          status={listening ? "active" : playing ? "online" : "idle"}
        />
      </header>
      <div>
        <button aria-label="Previous section" onClick={onPrevious} type="button">
          <RotateCcw aria-hidden="true" />
          <small>Previous</small>
        </button>
        <button
          aria-label={playing ? "Pause lesson" : "Play lesson"}
          onClick={() => onPlayChange?.(!playing)}
          type="button"
        >
          {playing ? <Pause aria-hidden="true" /> : <Play aria-hidden="true" />}
          <small>{playing ? "Pause" : "Play"}</small>
        </button>
        <button aria-label="Ask tutor" data-active={listening} onClick={onAsk} type="button">
          <Mic aria-hidden="true" />
          <small>Ask tutor</small>
        </button>
        <button aria-label="Next section" onClick={onNext} type="button">
          <ChevronRight aria-hidden="true" />
          <small>Next</small>
        </button>
      </div>
    </div>
  );
}

export type LearningContext = "walk" | "drive" | "talk" | "focus";
export interface LearningContextModeProps extends Omit<
  HTMLAttributes<HTMLFieldSetElement>,
  "onChange"
> {
  value?: LearningContext;
  onChange?: (mode: LearningContext) => void;
}
const contextModes: readonly {
  id: LearningContext;
  label: string;
  detail: string;
  icon: ReactNode;
}[] = [
  {
    id: "walk",
    label: "Walk",
    detail: "Audio-first, large controls",
    icon: <Footprints aria-hidden="true" />
  },
  {
    id: "drive",
    label: "Drive",
    detail: "Voice-only interaction",
    icon: <Car aria-hidden="true" />
  },
  { id: "talk", label: "Talk", detail: "Tutor conversation", icon: <Mic aria-hidden="true" /> },
  {
    id: "focus",
    label: "Focus",
    detail: "Transcript and checks",
    icon: <Target aria-hidden="true" />
  }
];
export function LearningContextMode({
  className,
  onChange,
  value: controlled,
  ...props
}: LearningContextModeProps) {
  const [internal, setInternal] = useState<LearningContext>("focus");
  const value = controlled ?? internal;
  return (
    <fieldset className={cn("amro-learning-context", className)} {...props}>
      <legend>How are you learning right now?</legend>
      <div>
        {contextModes.map((mode) => (
          <label data-selected={value === mode.id} key={mode.id}>
            <input
              checked={value === mode.id}
              name="learning-context"
              onChange={() => {
                setInternal(mode.id);
                onChange?.(mode.id);
              }}
              type="radio"
            />
            <span>{mode.icon}</span>
            <span>
              <strong>{mode.label}</strong>
              <small>{mode.detail}</small>
            </span>
          </label>
        ))}
      </div>
    </fieldset>
  );
}

export interface OfflineLessonCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  sizeMb?: number;
  status?: "available" | "downloading" | "downloaded" | "syncing" | "error";
  progress?: number;
  lastSynced?: ReactNode;
  onDownload?: () => void;
  onRemove?: () => void;
}
export function OfflineLessonCard({
  className,
  lastSynced = "Progress synced 2 min ago",
  onDownload,
  onRemove,
  progress = 68,
  sizeMb = 42,
  status = "available",
  title = "Governed AI Operations",
  ...props
}: OfflineLessonCardProps) {
  return (
    <Card className={cn("amro-offline-lesson", className)} {...props}>
      <header>
        <span>
          <Download aria-hidden="true" />
        </span>
        <span>
          <strong>{title}</strong>
          <small>{sizeMb} MB · audio, transcript, and checks</small>
        </span>
        <StatusIndicator
          label={status}
          status={
            status === "downloaded"
              ? "success"
              : status === "downloading" || status === "syncing"
                ? "loading"
                : status === "available"
                  ? "idle"
                  : status
          }
        />
      </header>
      {status === "downloading" && (
        <div>
          <progress max="100" value={progress}>
            {progress}%
          </progress>
          <strong>{progress}%</strong>
        </div>
      )}
      <footer>
        <span>
          <ShieldCheck aria-hidden="true" />
          {lastSynced}
        </span>
        {status === "downloaded" && onRemove ? (
          <Button onClick={onRemove} variant="tertiary">
            Remove download
          </Button>
        ) : (
          onDownload && (
            <Button onClick={onDownload} variant="secondary">
              <Download aria-hidden="true" />
              Download for offline
            </Button>
          )
        )}
      </footer>
    </Card>
  );
}

export interface TutorMemory {
  id: string;
  label: ReactNode;
  detail?: ReactNode;
  category: "goal" | "preference" | "progress";
  removable?: boolean;
}
export interface TutorMemoryPanelProps extends HTMLAttributes<HTMLDivElement> {
  memories?: readonly TutorMemory[];
  onRemove?: (memory: TutorMemory) => void;
  onClear?: () => void;
}
const tutorMemories: readonly TutorMemory[] = [
  { id: "goal", label: "Design a governed AI workflow", detail: "Learning goal", category: "goal" },
  {
    id: "analogy",
    label: "Prefers operational examples",
    detail: "Explanation preference",
    category: "preference",
    removable: true
  },
  {
    id: "gap",
    label: "Revisit approval exceptions",
    detail: "Current knowledge gap",
    category: "progress",
    removable: true
  }
];
export function TutorMemoryPanel({
  className,
  memories = tutorMemories,
  onClear,
  onRemove,
  ...props
}: TutorMemoryPanelProps) {
  return (
    <FeaturePanel
      className={cn("amro-tutor-memory", className)}
      eyebrow="Tutor memory"
      title="What your tutor remembers"
      description="Memory improves continuity and stays under your control."
      status={<Badge tone="neutral">{memories.length} items</Badge>}
      {...props}
    >
      {memories.length ? (
        <ul>
          {memories.map((memory) => (
            <li key={memory.id}>
              <span>
                {memory.category === "goal" ? (
                  <Target aria-hidden="true" />
                ) : memory.category === "preference" ? (
                  <Lightbulb aria-hidden="true" />
                ) : (
                  <Brain aria-hidden="true" />
                )}
              </span>
              <span>
                <strong>{memory.label}</strong>
                <small>{memory.detail}</small>
              </span>
              {memory.removable && onRemove && (
                <button
                  aria-label={`Forget ${getAccessibleText(memory.label, "memory")}`}
                  onClick={() => onRemove(memory)}
                  type="button"
                >
                  <Trash2 aria-hidden="true" />
                </button>
              )}
            </li>
          ))}
        </ul>
      ) : (
        <EmptyNotice
          title="No tutor memory"
          description="Your tutor will adapt only during the current session."
        />
      )}
      {onClear && memories.length > 0 && (
        <Button onClick={onClear} variant="tertiary">
          Clear removable memory
        </Button>
      )}
    </FeaturePanel>
  );
}

export interface LearningGoal {
  outcome: string;
  deadline: string;
  weeklyMinutes: number;
}
export interface LearningGoalCardProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onChange" | "onSubmit"
> {
  value?: LearningGoal;
  onChange?: (goal: LearningGoal) => void;
  onSubmit?: (goal: LearningGoal) => void;
}
export function LearningGoalCard({
  className,
  onChange,
  onSubmit,
  value = {
    outcome: "Design a governed AI workflow for my team",
    deadline: "2026-09-30",
    weeklyMinutes: 90
  },
  ...props
}: LearningGoalCardProps) {
  const [goal, setGoal] = useState(value);
  const update = <K extends keyof LearningGoal>(key: K, next: LearningGoal[K]) => {
    const updated = { ...goal, [key]: next };
    setGoal(updated);
    onChange?.(updated);
  };
  return (
    <form
      className={cn("amro-learning-goal", className)}
      onSubmit={(event) => {
        event.preventDefault();
        onSubmit?.(goal);
      }}
      {...props}
    >
      <header>
        <Target aria-hidden="true" />
        <span>
          <strong>Your learning goal</strong>
          <small>Used to personalise pace and practice</small>
        </span>
      </header>
      <Input
        label="Desired outcome"
        value={goal.outcome}
        onChange={(event) => update("outcome", event.target.value)}
      />
      <div>
        <label className="amro-field">
          <span className="amro-field__label">Target date</span>
          <DateInput
            value={goal.deadline}
            onChange={(event) => update("deadline", event.target.value)}
          />
        </label>
        <Input
          label="Minutes per week"
          min="15"
          step="15"
          type="number"
          value={goal.weeklyMinutes}
          onChange={(event) => update("weeklyMinutes", Number(event.target.value))}
        />
      </div>
      <footer>
        <span>
          <CalendarDays aria-hidden="true" />
          About {Math.ceil(goal.weeklyMinutes / 30)} short sessions per week
        </span>
        <Button disabled={!goal.outcome.trim()} type="submit">
          Save goal
        </Button>
      </footer>
    </form>
  );
}

export interface OrganisationLearner {
  id: string;
  name: string;
  email: string;
  role: ReactNode;
  assignment?: ReactNode;
  activation: "invited" | "active" | "inactive";
  progress?: number;
}
export interface OrganisationAllocationTableProps extends HTMLAttributes<HTMLDivElement> {
  learners?: readonly OrganisationLearner[];
  onAssign?: (learner: OrganisationLearner) => void;
  onInvite?: () => void;
}
const organisationLearners: readonly OrganisationLearner[] = [
  {
    id: "maya",
    name: "Maya Chen",
    email: "maya@example.com",
    role: "Operations",
    assignment: "Governed AI Operations",
    activation: "active",
    progress: 72
  },
  {
    id: "alex",
    name: "Alex Morgan",
    email: "alex@example.com",
    role: "Product",
    assignment: "AI Product Foundations",
    activation: "active",
    progress: 46
  },
  {
    id: "sam",
    name: "Sam Wilson",
    email: "sam@example.com",
    role: "Support",
    activation: "invited",
    progress: 0
  }
];
export function OrganisationAllocationTable({
  className,
  learners = organisationLearners,
  onAssign,
  onInvite,
  ...props
}: OrganisationAllocationTableProps) {
  return (
    <div className={cn("amro-allocation-table", className)} {...props}>
      <header>
        <span>
          <strong>Learning allocation</strong>
          <small>
            {learners.filter((learner) => learner.activation === "active").length} active of{" "}
            {learners.length}
          </small>
        </span>
        {onInvite && (
          <Button onClick={onInvite} size="sm">
            Invite learner
          </Button>
        )}
      </header>
      <table>
        <thead>
          <tr>
            <th>Learner</th>
            <th>Role</th>
            <th>Programme or tutor</th>
            <th>Activation</th>
            <th>Progress</th>
            <th>
              <span className="amro-sr-only">Actions</span>
            </th>
          </tr>
        </thead>
        <tbody>
          {learners.map((learner) => (
            <tr key={learner.id}>
              <td>
                <strong>{learner.name}</strong>
                <small>{learner.email}</small>
              </td>
              <td>{learner.role}</td>
              <td>{learner.assignment ?? "Unassigned"}</td>
              <td>
                <StatusIndicator
                  label={learner.activation}
                  status={
                    learner.activation === "active"
                      ? "online"
                      : learner.activation === "invited"
                        ? "pending"
                        : "offline"
                  }
                />
              </td>
              <td>
                <progress max="100" value={learner.progress ?? 0} /> {learner.progress ?? 0}%
              </td>
              <td>
                {onAssign && (
                  <Button onClick={() => onAssign(learner)} size="sm" variant="tertiary">
                    Assign
                  </Button>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export interface HeatmapCell {
  learnerId: string;
  topicId: string;
  engagement: number;
  mastery: number;
}
export interface TeamLearningHeatmapProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  learners?: readonly { id: string; name: ReactNode }[];
  topics?: readonly { id: string; label: ReactNode }[];
  cells?: readonly HeatmapCell[];
  onSelect?: (cell: HeatmapCell) => void;
}
const heatLearners = [
  { id: "maya", name: "Maya" },
  { id: "alex", name: "Alex" },
  { id: "sam", name: "Sam" }
];
const heatTopics = [
  { id: "foundations", label: "Foundations" },
  { id: "governance", label: "Governance" },
  { id: "outcomes", label: "Outcomes" },
  { id: "practice", label: "Practice" }
];
const heatCells: readonly HeatmapCell[] = heatLearners.flatMap((learner, row) =>
  heatTopics.map((topic, column) => ({
    learnerId: learner.id,
    topicId: topic.id,
    engagement: 55 + (((row + column) * 9) % 43),
    mastery: 48 + (((row * 2 + column) * 11) % 49)
  }))
);
export function TeamLearningHeatmap({
  cells = heatCells,
  className,
  learners = heatLearners,
  onSelect,
  topics = heatTopics,
  ...props
}: TeamLearningHeatmapProps) {
  return (
    <div className={cn("amro-learning-heatmap", className)} {...props}>
      <header>
        <span>
          <strong>Team learning overview</strong>
          <small>Course-level engagement and mastery—not activity surveillance</small>
        </span>
        <Badge tone="success">
          <ShieldCheck aria-hidden="true" />
          Privacy-safe
        </Badge>
      </header>
      <div
        className="amro-learning-heatmap__grid"
        style={{
          gridTemplateColumns: `minmax(100px, 1fr) repeat(${topics.length}, minmax(80px, 1fr))`
        }}
      >
        <span />
        {topics.map((topic) => (
          <strong key={topic.id}>{topic.label}</strong>
        ))}
        {learners.flatMap((learner) => [
          <strong key={`${learner.id}-label`}>{learner.name}</strong>,
          ...topics.map((topic) => {
            const cell = cells.find(
              (item) => item.learnerId === learner.id && item.topicId === topic.id
            ) ?? { learnerId: learner.id, topicId: topic.id, engagement: 0, mastery: 0 };
            return (
              <button
                aria-label={`${getAccessibleText(learner.name, "Learner")}, ${getAccessibleText(topic.label, "topic")}: ${cell.mastery}% mastery`}
                key={`${learner.id}-${topic.id}`}
                onClick={() => onSelect?.(cell)}
                style={{ "--amro-heat": cell.mastery / 100 } as CSSProperties}
                type="button"
              >
                <strong>{cell.mastery}%</strong>
                <small>{cell.engagement}% engaged</small>
              </button>
            );
          })
        ])}
      </div>
    </div>
  );
}

export interface CreditPoolMeterProps extends HTMLAttributes<HTMLDivElement> {
  personalUsed?: number;
  personalLimit?: number;
  organisationUsed?: number;
  organisationLimit?: number;
  resetLabel?: ReactNode;
}
export function CreditPoolMeter({
  className,
  organisationLimit = 5000,
  organisationUsed = 3180,
  personalLimit = 1000,
  personalUsed = 420,
  resetLabel = "Resets 1 September",
  ...props
}: CreditPoolMeterProps) {
  const totalUsed = personalUsed + organisationUsed;
  const total = personalLimit + organisationLimit;
  return (
    <FeaturePanel
      className={cn("amro-credit-pool", className)}
      eyebrow="Learning credits"
      title={`${totalUsed.toLocaleString()} of ${total.toLocaleString()} used`}
      description={resetLabel}
      status={<Badge tone="neutral">{Math.round((totalUsed / total) * 100)}%</Badge>}
      {...props}
    >
      <div>
        <section>
          <span>
            <UserCog aria-hidden="true" />
            <strong>Personal</strong>
            <small>
              {personalUsed.toLocaleString()} / {personalLimit.toLocaleString()}
            </small>
          </span>
          <progress max={personalLimit} value={personalUsed} />
        </section>
        <section>
          <span>
            <Building2 aria-hidden="true" />
            <strong>Organisation</strong>
            <small>
              {organisationUsed.toLocaleString()} / {organisationLimit.toLocaleString()}
            </small>
          </span>
          <progress max={organisationLimit} value={organisationUsed} />
        </section>
      </div>
    </FeaturePanel>
  );
}

export type LearningRole = "learner" | "organisation-admin" | "instructor" | "platform-admin";
export interface RoleBadgeProps extends HTMLAttributes<HTMLSpanElement> {
  role?: LearningRole;
}
const roleLabels: Record<LearningRole, string> = {
  learner: "Learner",
  "organisation-admin": "Organisation admin",
  instructor: "Instructor",
  "platform-admin": "Platform admin"
};
export function RoleBadge({ className, role = "learner", ...props }: RoleBadgeProps) {
  const icon =
    role === "learner" ? (
      <GraduationCap aria-hidden="true" />
    ) : role === "instructor" ? (
      <BookOpen aria-hidden="true" />
    ) : role === "organisation-admin" ? (
      <Users aria-hidden="true" />
    ) : (
      <ShieldCheck aria-hidden="true" />
    );
  return (
    <span className={cn("amro-role-badge", `amro-role-badge--${role}`, className)} {...props}>
      {icon}
      {roleLabels[role]}
    </span>
  );
}
```



## Usage

Shared product source installed automatically by AmroUI component entries.

