+ {/* Example selector (if multiple) */}
+ {examples.length > 1 && (
+
+ {examples.map((example, index) => (
+
+ ))}
+
+ )}
+
+ {/* Visualization */}
+
+
+ );
+}
diff --git a/frontend/src/components/visualization/step-controller.tsx b/frontend/src/components/visualization/step-controller.tsx
new file mode 100644
index 0000000..8905572
--- /dev/null
+++ b/frontend/src/components/visualization/step-controller.tsx
@@ -0,0 +1,174 @@
+"use client";
+
+import { useVisualization } from "./visualization-context";
+import type { SpeedPreset } from "@/types/visualization";
+
+const speedLabels: Record = {
+ slow: "0.5x",
+ normal: "1x",
+ fast: "2x",
+};
+
+const speedOrder: SpeedPreset[] = ["slow", "normal", "fast"];
+
+export function StepController() {
+ const {
+ stepIndex,
+ totalSteps,
+ isPlaying,
+ speed,
+ nextStep,
+ prevStep,
+ togglePlayback,
+ setSpeed,
+ canGoNext,
+ canGoPrev,
+ } = useVisualization();
+
+ const cycleSpeed = () => {
+ const currentIndex = speedOrder.indexOf(speed);
+ const nextIndex = (currentIndex + 1) % speedOrder.length;
+ setSpeed(speedOrder[nextIndex]);
+ };
+
+ return (
+
+ {/* Step counter */}
+
+
+ Step {stepIndex + 1} of {totalSteps}
+
+
+
+ {/* Navigation controls */}
+
+ {/* Previous */}
+
+
+ {/* Play/Pause */}
+
+
+ {/* Next */}
+
+
+
+ {/* Speed control */}
+
+
+ {/* Progress bar */}
+
+
+ {/* Keyboard hints */}
+
+
+ ←
+
+
+ →
+
+ step
+
+ Space
+
+ play
+
+
+ );
+}
diff --git a/frontend/src/components/visualization/step-description.tsx b/frontend/src/components/visualization/step-description.tsx
new file mode 100644
index 0000000..84f11e7
--- /dev/null
+++ b/frontend/src/components/visualization/step-description.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useVisualization } from "./visualization-context";
+
+export function StepDescription() {
+ const { currentStep } = useVisualization();
+
+ if (!currentStep) {
+ return null;
+ }
+
+ return (
+
+
{currentStep.description}
+
+ );
+}
diff --git a/frontend/src/components/visualization/structure-renderer.tsx b/frontend/src/components/visualization/structure-renderer.tsx
new file mode 100644
index 0000000..52e4c1b
--- /dev/null
+++ b/frontend/src/components/visualization/structure-renderer.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useVisualization } from "./visualization-context";
+import { ArrayVisualizer } from "./visualizers/array-visualizer";
+import type { DataStructureState } from "@/types/visualization";
+
+interface StructureVisualizerProps {
+ name: string;
+ data: DataStructureState;
+}
+
+function StructureVisualizer({ name, data }: StructureVisualizerProps) {
+ switch (data.type) {
+ case "array":
+ return ;
+ case "linkedlist":
+ // Phase 3: LinkedListVisualizer
+ return (
+
+ LinkedList visualization coming soon
+
+ );
+ case "stack":
+ // Phase 3: StackVisualizer
+ return (
+
+ Stack visualization coming soon
+
+ );
+ case "tree":
+ // Phase 3: TreeVisualizer
+ return (
+
+ Tree visualization coming soon
+
+ );
+ case "graph":
+ // Phase 5: GraphVisualizer
+ return (
+
+ Graph visualization coming soon
+
+ );
+ case "matrix":
+ // Phase 5: MatrixVisualizer
+ return (
+
+ Matrix visualization coming soon
+
+ );
+ default:
+ return (
+
+ Unknown data structure type
+
+ );
+ }
+}
+
+export function StructureRenderer() {
+ const { currentStep } = useVisualization();
+
+ if (!currentStep?.structures) {
+ return (
+
+ No data structures to display
+
+ );
+ }
+
+ const structureEntries = Object.entries(currentStep.structures);
+
+ if (structureEntries.length === 0) {
+ return (
+
+ No data structures to display
+
+ );
+ }
+
+ return (
+
+ {structureEntries.map(([name, data]) => (
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/visualization/variables-pane.tsx b/frontend/src/components/visualization/variables-pane.tsx
new file mode 100644
index 0000000..bb33644
--- /dev/null
+++ b/frontend/src/components/visualization/variables-pane.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useVisualization } from "./visualization-context";
+
+function formatValue(value: unknown): string {
+ if (value === null) return "null";
+ if (value === undefined) return "undefined";
+ if (typeof value === "string") return `"${value}"`;
+ if (typeof value === "boolean") return value ? "true" : "false";
+ if (Array.isArray(value)) return `[${value.map(formatValue).join(", ")}]`;
+ if (typeof value === "object") return JSON.stringify(value);
+ return String(value);
+}
+
+export function VariablesPane() {
+ const { currentStep } = useVisualization();
+
+ if (!currentStep?.variables || Object.keys(currentStep.variables).length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ Variables
+
+
+ {Object.entries(currentStep.variables).map(([name, value]) => (
+
+
+ {name}
+
+ =
+ {formatValue(value)}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/components/visualization/visualization-container.tsx b/frontend/src/components/visualization/visualization-container.tsx
new file mode 100644
index 0000000..5e00034
--- /dev/null
+++ b/frontend/src/components/visualization/visualization-container.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { VisualizationProvider } from "./visualization-context";
+import { StepController } from "./step-controller";
+import { StepDescription } from "./step-description";
+import { VariablesPane } from "./variables-pane";
+import { StructureRenderer } from "./structure-renderer";
+import type { VisualizationData } from "@/types/visualization";
+
+interface VisualizationContainerProps {
+ data: VisualizationData;
+ className?: string;
+}
+
+export function VisualizationContainer({
+ data,
+ className = "",
+}: VisualizationContainerProps) {
+ if (!data.steps || data.steps.length === 0) {
+ return (
+
+ No visualization steps available
+
+ );
+ }
+
+ return (
+
+
+ {/* Title */}
+
+
{data.title}
+
+
+ {/* Step description */}
+
+
+ {/* Data structure visualization */}
+
+
+
+
+ {/* Variables */}
+
+
+ {/* Controls */}
+
+
+
+ );
+}
diff --git a/frontend/src/components/visualization/visualization-context.tsx b/frontend/src/components/visualization/visualization-context.tsx
new file mode 100644
index 0000000..181555d
--- /dev/null
+++ b/frontend/src/components/visualization/visualization-context.tsx
@@ -0,0 +1,222 @@
+"use client";
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from "react";
+import type {
+ SpeedPreset,
+ VisualizationData,
+ VisualizationStep,
+} from "@/types/visualization";
+
+const speedValues: Record = {
+ slow: 2000,
+ normal: 1000,
+ fast: 500,
+};
+
+interface VisualizationContextValue {
+ // Data
+ data: VisualizationData | null;
+ currentStep: VisualizationStep | null;
+ stepIndex: number;
+ totalSteps: number;
+
+ // Playback state
+ isPlaying: boolean;
+ speed: SpeedPreset;
+
+ // Navigation actions
+ goToStep: (index: number) => void;
+ nextStep: () => void;
+ prevStep: () => void;
+ play: () => void;
+ pause: () => void;
+ togglePlayback: () => void;
+ setSpeed: (speed: SpeedPreset) => void;
+
+ // State queries
+ canGoNext: boolean;
+ canGoPrev: boolean;
+}
+
+const VisualizationContext = createContext(
+ null
+);
+
+interface VisualizationProviderProps {
+ data: VisualizationData;
+ initialStep?: number;
+ autoPlay?: boolean;
+ children: ReactNode;
+}
+
+export function VisualizationProvider({
+ data,
+ initialStep = 0,
+ autoPlay = false,
+ children,
+}: VisualizationProviderProps) {
+ const [stepIndex, setStepIndex] = useState(initialStep);
+ const [isPlaying, setIsPlaying] = useState(autoPlay);
+ const [speed, setSpeed] = useState("normal");
+ const intervalRef = useRef | null>(null);
+
+ const totalSteps = data.steps.length;
+ const currentStep = data.steps[stepIndex] ?? null;
+ const canGoNext = stepIndex < totalSteps - 1;
+ const canGoPrev = stepIndex > 0;
+
+ const goToStep = useCallback(
+ (index: number) => {
+ const clampedIndex = Math.max(0, Math.min(index, totalSteps - 1));
+ setStepIndex(clampedIndex);
+ },
+ [totalSteps]
+ );
+
+ const nextStep = useCallback(() => {
+ if (canGoNext) {
+ setStepIndex((prev) => prev + 1);
+ } else {
+ setIsPlaying(false);
+ }
+ }, [canGoNext]);
+
+ const prevStep = useCallback(() => {
+ if (canGoPrev) {
+ setStepIndex((prev) => prev - 1);
+ }
+ }, [canGoPrev]);
+
+ const play = useCallback(() => {
+ if (stepIndex >= totalSteps - 1) {
+ setStepIndex(0);
+ }
+ setIsPlaying(true);
+ }, [stepIndex, totalSteps]);
+
+ const pause = useCallback(() => {
+ setIsPlaying(false);
+ }, []);
+
+ const togglePlayback = useCallback(() => {
+ if (isPlaying) {
+ pause();
+ } else {
+ play();
+ }
+ }, [isPlaying, play, pause]);
+
+ // Auto-advance when playing
+ useEffect(() => {
+ if (isPlaying) {
+ intervalRef.current = setInterval(() => {
+ setStepIndex((prev) => {
+ if (prev >= totalSteps - 1) {
+ setIsPlaying(false);
+ return prev;
+ }
+ return prev + 1;
+ });
+ }, speedValues[speed]);
+ }
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ };
+ }, [isPlaying, speed, totalSteps]);
+
+ // Keyboard navigation
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Don't capture if user is typing in an input
+ if (
+ event.target instanceof HTMLInputElement ||
+ event.target instanceof HTMLTextAreaElement
+ ) {
+ return;
+ }
+
+ switch (event.key) {
+ case "ArrowLeft":
+ event.preventDefault();
+ prevStep();
+ break;
+ case "ArrowRight":
+ event.preventDefault();
+ nextStep();
+ break;
+ case " ":
+ event.preventDefault();
+ togglePlayback();
+ break;
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [prevStep, nextStep, togglePlayback]);
+
+ const value = useMemo(
+ () => ({
+ data,
+ currentStep,
+ stepIndex,
+ totalSteps,
+ isPlaying,
+ speed,
+ goToStep,
+ nextStep,
+ prevStep,
+ play,
+ pause,
+ togglePlayback,
+ setSpeed,
+ canGoNext,
+ canGoPrev,
+ }),
+ [
+ data,
+ currentStep,
+ stepIndex,
+ totalSteps,
+ isPlaying,
+ speed,
+ goToStep,
+ nextStep,
+ prevStep,
+ play,
+ pause,
+ togglePlayback,
+ canGoNext,
+ canGoPrev,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useVisualization(): VisualizationContextValue {
+ const context = useContext(VisualizationContext);
+ if (!context) {
+ throw new Error(
+ "useVisualization must be used within a VisualizationProvider"
+ );
+ }
+ return context;
+}
diff --git a/frontend/src/components/visualization/visualizers/array-visualizer.tsx b/frontend/src/components/visualization/visualizers/array-visualizer.tsx
new file mode 100644
index 0000000..45e38ec
--- /dev/null
+++ b/frontend/src/components/visualization/visualizers/array-visualizer.tsx
@@ -0,0 +1,263 @@
+"use client";
+
+import { useMemo } from "react";
+import type { ArrayState, ElementState } from "@/types/visualization";
+
+interface ArrayVisualizerProps {
+ data: ArrayState;
+ name?: string;
+}
+
+const CELL_WIDTH = 56;
+const CELL_HEIGHT = 56;
+const CELL_GAP = 4;
+const INDEX_AREA_HEIGHT = 20;
+const POINTER_HEIGHT = 24;
+const ANNOTATION_HEIGHT = 20;
+const SVG_PADDING = 16;
+
+function getStateColor(state: ElementState): string {
+ switch (state) {
+ case "active":
+ return "var(--viz-active)";
+ case "comparing":
+ return "var(--viz-comparing)";
+ case "found":
+ return "var(--viz-found)";
+ case "visited":
+ return "var(--viz-visited)";
+ case "swapping":
+ return "var(--viz-swapping)";
+ default:
+ return "var(--viz-default)";
+ }
+}
+
+function getTextColor(state: ElementState): string {
+ switch (state) {
+ case "active":
+ case "found":
+ case "swapping":
+ return "white";
+ case "comparing":
+ return "var(--foreground)";
+ case "visited":
+ return "var(--muted-foreground)";
+ default:
+ return "var(--foreground)";
+ }
+}
+
+export function ArrayVisualizer({ data, name }: ArrayVisualizerProps) {
+ const { values, pointers = {} } = data;
+
+ const dimensions = useMemo(() => {
+ const hasPointers = Object.keys(pointers).length > 0;
+ const hasAnnotations = values.some(
+ (v) => v.annotations && v.annotations.length > 0
+ );
+
+ const contentWidth =
+ values.length * CELL_WIDTH + (values.length - 1) * CELL_GAP;
+ const width = contentWidth + SVG_PADDING * 2;
+
+ let height = CELL_HEIGHT + INDEX_AREA_HEIGHT + SVG_PADDING * 2;
+ if (hasPointers) height += POINTER_HEIGHT + 16;
+ if (hasAnnotations) height += ANNOTATION_HEIGHT;
+
+ return {
+ width,
+ height,
+ contentWidth,
+ hasPointers,
+ hasAnnotations,
+ startX: SVG_PADDING,
+ startY: hasAnnotations
+ ? SVG_PADDING + ANNOTATION_HEIGHT
+ : SVG_PADDING + 8,
+ };
+ }, [values, pointers]);
+
+ // Group pointers by position for stacking
+ const pointersByPosition = useMemo(() => {
+ const grouped: Record = {};
+ for (const [label, index] of Object.entries(pointers)) {
+ if (!grouped[index]) {
+ grouped[index] = [];
+ }
+ grouped[index].push(label);
+ }
+ return grouped;
+ }, [pointers]);
+
+ return (
+
+ {name && (
+
+ {name}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/visualization/visualizers/index.ts b/frontend/src/components/visualization/visualizers/index.ts
new file mode 100644
index 0000000..2c5c8df
--- /dev/null
+++ b/frontend/src/components/visualization/visualizers/index.ts
@@ -0,0 +1 @@
+export { ArrayVisualizer } from "./array-visualizer";
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index afe3402..56a5a20 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -57,6 +57,22 @@ export interface LearningProgression {
challenge: LearningQuestion[];
}
+export interface VisualizationExampleStep {
+ id: string;
+ description: string;
+ structures: Record;
+ variables?: Record;
+ codeHighlight?: { startLine: number; endLine: number };
+}
+
+export interface VisualizationExample {
+ id: string;
+ title: string;
+ input?: Record;
+ code: string;
+ steps: VisualizationExampleStep[];
+}
+
export interface PatternTutorial extends Pattern {
metaphor: string | null;
core_concept: string | null;
@@ -69,6 +85,7 @@ export interface PatternTutorial extends Pattern {
prerequisite_patterns: RelatedPattern[] | null;
difficulty_level: number | null;
learning_progression: LearningProgression | null;
+ visualization_examples: VisualizationExample[] | null;
}
export interface QuestionListItem {
diff --git a/frontend/src/types/visualization.ts b/frontend/src/types/visualization.ts
new file mode 100644
index 0000000..9c892e6
--- /dev/null
+++ b/frontend/src/types/visualization.ts
@@ -0,0 +1,160 @@
+// Visualization System Types
+
+export type ElementState =
+ | "default"
+ | "active"
+ | "comparing"
+ | "found"
+ | "visited"
+ | "swapping";
+
+// Array Data Structure
+export interface ArrayElement {
+ value: unknown;
+ state: ElementState;
+ annotations?: string[];
+}
+
+export interface ArrayState {
+ type: "array";
+ values: ArrayElement[];
+ pointers?: Record;
+}
+
+// Linked List Data Structure
+export interface LinkedListNode {
+ value: unknown;
+ state: ElementState;
+ annotations?: string[];
+}
+
+export interface LinkedListState {
+ type: "linkedlist";
+ nodes: LinkedListNode[];
+ pointers?: Record;
+}
+
+// Stack Data Structure
+export interface StackState {
+ type: "stack";
+ values: Array<{
+ value: unknown;
+ state: ElementState;
+ annotations?: string[];
+ }>;
+}
+
+// Tree Data Structure
+export interface TreeNode {
+ value: unknown;
+ state: ElementState;
+ annotations?: string[];
+ left?: TreeNode | null;
+ right?: TreeNode | null;
+}
+
+export interface TreeState {
+ type: "tree";
+ root: TreeNode | null;
+ pointers?: Record;
+}
+
+// Graph Data Structure
+export interface GraphNode {
+ id: string;
+ value: unknown;
+ state: ElementState;
+ annotations?: string[];
+}
+
+export interface GraphEdge {
+ from: string;
+ to: string;
+ weight?: number;
+ state: ElementState;
+}
+
+export interface GraphState {
+ type: "graph";
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+ directed?: boolean;
+}
+
+// Matrix Data Structure
+export interface MatrixCell {
+ value: unknown;
+ state: ElementState;
+ annotations?: string[];
+}
+
+export interface MatrixState {
+ type: "matrix";
+ rows: MatrixCell[][];
+ pointers?: Record;
+}
+
+// Union type for all data structures
+export type DataStructureState =
+ | ArrayState
+ | LinkedListState
+ | StackState
+ | TreeState
+ | GraphState
+ | MatrixState;
+
+// Code highlighting range
+export interface CodeHighlight {
+ startLine: number;
+ endLine: number;
+}
+
+// Transition configuration
+export interface TransitionConfig {
+ duration?: number;
+ emphasis?: string;
+}
+
+// Core visualization step
+export interface VisualizationStep {
+ id: string;
+ description: string;
+ structures: Record;
+ variables?: Record;
+ codeHighlight?: CodeHighlight;
+ transition?: TransitionConfig;
+}
+
+// Complete visualization data
+export interface VisualizationData {
+ id: string;
+ title: string;
+ code: string;
+ steps: VisualizationStep[];
+ totalSteps: number;
+}
+
+// Visualization example from API (stored in pattern YAML)
+export interface VisualizationExample {
+ id: string;
+ title: string;
+ input?: Record;
+ code: string;
+ steps: VisualizationStep[];
+}
+
+// Playback state for context
+export interface PlaybackState {
+ currentStep: number;
+ isPlaying: boolean;
+ speed: number;
+}
+
+// Speed presets (ms per step)
+export const SPEED_PRESETS = {
+ slow: 2000,
+ normal: 1000,
+ fast: 500,
+} as const;
+
+export type SpeedPreset = keyof typeof SPEED_PRESETS;