diff --git a/apps/web/src/components/home/ProblemSection.tsx b/apps/web/src/components/home/ProblemSection.tsx
new file mode 100644
index 0000000..a15fff2
--- /dev/null
+++ b/apps/web/src/components/home/ProblemSection.tsx
@@ -0,0 +1,72 @@
+/**
+ * ProblemSection Component
+ *
+ * Shows the fragmented, static world before tpmjs.
+ * Cards are rotated randomly to create visual chaos.
+ */
+
+'use client';
+
+import { Card, CardContent } from '@tpmjs/ui/Card/Card';
+import { Container } from '@tpmjs/ui/Container/Container';
+import { DitherSectionHeader } from '@tpmjs/ui/DitherText/DitherSectionHeader';
+import { Icon } from '@tpmjs/ui/Icon/Icon';
+import { problemPoints } from '../../data/homePageData';
+
+export function ProblemSection(): React.ReactElement {
+ return (
+
+ {/* Subtle grid background */}
+
+
+
+ THE PROBLEM
+
+
+ {problemPoints.map((problem, index) => {
+ // Random rotation for chaos
+ const rotation = (index % 2 === 0 ? 1 : -1) * (Math.random() * 5 + 2);
+
+ return (
+
+
+
+
+
+
+ {problem.title}
+
+
+ {problem.description}
+
+
+
+ {/* Dashed line to show disconnection */}
+ {index < problemPoints.length - 1 && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ Agents stuck with static, manually-configured toolsets
+
+
+
+ );
+}
diff --git a/apps/web/src/components/home/VisionSection.tsx b/apps/web/src/components/home/VisionSection.tsx
new file mode 100644
index 0000000..3f91f7b
--- /dev/null
+++ b/apps/web/src/components/home/VisionSection.tsx
@@ -0,0 +1,71 @@
+/**
+ * VisionSection Component
+ *
+ * Shows the dynamic, connected ecosystem enabled by tpmjs.
+ * Features animated flow diagram showing semantic discovery.
+ */
+
+'use client';
+
+import { Container } from '@tpmjs/ui/Container/Container';
+import { DitherSectionHeader } from '@tpmjs/ui/DitherText/DitherSectionHeader';
+import { FlowDiagram } from '@tpmjs/ui/FlowDiagram/FlowDiagram';
+
+export function VisionSection(): React.ReactElement {
+ return (
+
+ {/* Blueprint grid with subtle pulse */}
+
+
+
+ THE VISION
+
+
+
+
+
+
+
Dynamic
+
+ Agents discover tools on-demand through semantic search
+
+
+
+
+
+ Scalable
+
+
+ Thousands of tools available without manual integration
+
+
+
+
+
+ Evolving
+
+
+ New capabilities emerge as the community publishes tools
+
+
+
+
+
+
+ The registry that gives agents superpowers
+
+
+
+ );
+}
diff --git a/apps/web/src/data/homePageData.ts b/apps/web/src/data/homePageData.ts
index 2ddf6bf..b741ced 100644
--- a/apps/web/src/data/homePageData.ts
+++ b/apps/web/src/data/homePageData.ts
@@ -220,3 +220,71 @@ export const statistics: Statistic[] = [
subtext: '95th percentile latency',
},
];
+
+export interface ProblemPoint {
+ title: string;
+ description: string;
+ icon: IconName;
+}
+
+export const problemPoints: ProblemPoint[] = [
+ {
+ title: 'Manual Configuration',
+ description:
+ 'Agents require hard-coded tool imports and manual config files for every capability',
+ icon: 'copy',
+ },
+ {
+ title: 'No Discovery',
+ description: 'Tools must be known ahead of time—no way to find new capabilities on-demand',
+ icon: 'check',
+ },
+ {
+ title: 'Version Hell',
+ description: 'Breaking changes force manual updates across every agent using a tool',
+ icon: 'chevronDown',
+ },
+ {
+ title: 'Limited Capabilities',
+ description: 'Static toolsets restrict agents to predetermined functions—no room to grow',
+ icon: 'externalLink',
+ },
+];
+
+export interface DeveloperStory {
+ code: string;
+ quote: string;
+ author: string;
+ company: string;
+}
+
+export const developerStories: DeveloperStory[] = [
+ {
+ code: `const agent = new Agent({
+ tools: await tpmjs.search({
+ query: "email, slack, calendar"
+ })
+})`,
+ quote: 'Reduced config from 500 lines to 3',
+ author: 'Sarah Chen',
+ company: 'Support.ai',
+ },
+ {
+ code: `// Agents find tools semantically
+const tools = await tpmjs.search({
+ query: "parse PDF invoices",
+ limit: 5
+})`,
+ quote: 'Our agents adapt to new doc formats instantly',
+ author: 'Marcus Rodriguez',
+ company: 'DocFlow',
+ },
+ {
+ code: `// No more version conflicts
+agent.loadTool("sql-query@latest")
+// Always get the newest, tested version`,
+ quote: 'Eliminated 80% of our integration bugs',
+ author: 'Priya Patel',
+ company: 'DataSync Pro',
+ },
+];
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 9c963c9..abec288 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -106,6 +106,34 @@
"./system/hooks/useParallax": {
"types": "./dist/system/hooks/useParallax.d.ts",
"default": "./dist/system/hooks/useParallax.js"
+ },
+ "./system/hooks/useReducedMotion": {
+ "types": "./dist/system/hooks/useReducedMotion.d.ts",
+ "default": "./dist/system/hooks/useReducedMotion.js"
+ },
+ "./system/hooks/useDitherAnimation": {
+ "types": "./dist/system/hooks/useDitherAnimation.d.ts",
+ "default": "./dist/system/hooks/useDitherAnimation.js"
+ },
+ "./DitherCanvas/DitherCanvas": {
+ "types": "./dist/DitherCanvas/DitherCanvas.d.ts",
+ "default": "./dist/DitherCanvas/DitherCanvas.js"
+ },
+ "./DitherText/DitherHeadline": {
+ "types": "./dist/DitherText/DitherHeadline.d.ts",
+ "default": "./dist/DitherText/DitherHeadline.js"
+ },
+ "./DitherText/DitherSectionHeader": {
+ "types": "./dist/DitherText/DitherSectionHeader.d.ts",
+ "default": "./dist/DitherText/DitherSectionHeader.js"
+ },
+ "./FlowDiagram/FlowDiagram": {
+ "types": "./dist/FlowDiagram/FlowDiagram.d.ts",
+ "default": "./dist/FlowDiagram/FlowDiagram.js"
+ },
+ "./ActivityStream/ActivityStream": {
+ "types": "./dist/ActivityStream/ActivityStream.d.ts",
+ "default": "./dist/ActivityStream/ActivityStream.js"
}
},
"files": ["dist"],
diff --git a/packages/ui/src/ActivityStream/ActivityStream.tsx b/packages/ui/src/ActivityStream/ActivityStream.tsx
new file mode 100644
index 0000000..4773d13
--- /dev/null
+++ b/packages/ui/src/ActivityStream/ActivityStream.tsx
@@ -0,0 +1,126 @@
+/**
+ * ActivityStream Component
+ *
+ * Live ticker showing recent tool activity (invocations, publications, updates).
+ * Uses mock data to simulate a live ecosystem.
+ */
+
+'use client';
+
+import { useEffect, useState } from 'react';
+import type { ActivityItem, ActivityStreamProps } from './types';
+
+// Mock data generator
+const mockTools = [
+ 'web-scraper',
+ 'sql-query',
+ 'email-sender',
+ 'github-manager',
+ 'image-processor',
+ 'pdf-generator',
+ 'slack-bot',
+ 'calendar-sync',
+ 'data-validator',
+ 'file-converter',
+];
+
+const activityTypes: ActivityItem['type'][] = ['invoked', 'published', 'updated'];
+
+function generateMockActivity(): ActivityItem {
+ const type = activityTypes[Math.floor(Math.random() * activityTypes.length)] || 'invoked';
+ const tool = mockTools[Math.floor(Math.random() * mockTools.length)] || 'unknown-tool';
+ const seconds = Math.floor(Math.random() * 30) + 1;
+
+ return {
+ type,
+ tool,
+ time: `${seconds}s ago`,
+ };
+}
+
+function getActivityIcon(type: ActivityItem['type']): string {
+ switch (type) {
+ case 'invoked':
+ return '▸';
+ case 'published':
+ return '+';
+ case 'updated':
+ return '↻';
+ default:
+ return '•';
+ }
+}
+
+function getActivityColor(type: ActivityItem['type']): string {
+ switch (type) {
+ case 'invoked':
+ return 'text-brutalist-accent';
+ case 'published':
+ return 'text-green-500';
+ case 'updated':
+ return 'text-yellow-500';
+ default:
+ return 'text-foreground-secondary';
+ }
+}
+
+export function ActivityStream({
+ activities: initialActivities,
+ updateInterval = 6000,
+ maxItems = 5,
+ className = '',
+}: ActivityStreamProps): React.ReactElement {
+ // Initialize with provided activities or generate mock data
+ const [activities, setActivities] = useState
(
+ initialActivities || Array.from({ length: maxItems }, generateMockActivity)
+ );
+
+ // Auto-update with new activities
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const newActivity = generateMockActivity();
+
+ setActivities((prev) => {
+ const updated = [newActivity, ...prev];
+ return updated.slice(0, maxItems);
+ });
+ }, updateInterval);
+
+ return () => clearInterval(interval);
+ }, [updateInterval, maxItems]);
+
+ return (
+
+
+ Recent Activity
+
+
+
+ {activities.map((activity, index) => (
+
+
+ {getActivityIcon(activity.type)}
+
+
+
+ {activity.tool}
+ {activity.type}
+
+
+ {activity.time}
+
+ ))}
+
+
+ );
+}
diff --git a/packages/ui/src/ActivityStream/types.ts b/packages/ui/src/ActivityStream/types.ts
new file mode 100644
index 0000000..ae7d647
--- /dev/null
+++ b/packages/ui/src/ActivityStream/types.ts
@@ -0,0 +1,28 @@
+/**
+ * Type definitions for ActivityStream component
+ */
+
+export interface ActivityItem {
+ /** Activity type */
+ type: 'invoked' | 'published' | 'updated';
+
+ /** Tool name */
+ tool: string;
+
+ /** Time ago string (e.g., "2s ago") */
+ time: string;
+}
+
+export interface ActivityStreamProps {
+ /** Initial activity items */
+ activities?: ActivityItem[];
+
+ /** Update interval in milliseconds (default: 6000) */
+ updateInterval?: number;
+
+ /** Maximum number of items to display (default: 5) */
+ maxItems?: number;
+
+ /** Additional CSS classes */
+ className?: string;
+}
diff --git a/packages/ui/src/DitherCanvas/DitherCanvas.tsx b/packages/ui/src/DitherCanvas/DitherCanvas.tsx
new file mode 100644
index 0000000..2716f73
--- /dev/null
+++ b/packages/ui/src/DitherCanvas/DitherCanvas.tsx
@@ -0,0 +1,119 @@
+/**
+ * DitherCanvas Component
+ *
+ * Renders text with Bayer dithering effect using HTML5 Canvas.
+ * Supports reveal, pulse, and static modes with full accessibility.
+ */
+
+'use client';
+
+import { useEffect, useMemo, useRef } from 'react';
+import { getDitherEngine } from '../system/canvas/DitherEngine';
+import { useDitherAnimation } from '../system/hooks/useDitherAnimation';
+import { useReducedMotion } from '../system/hooks/useReducedMotion';
+import type { DitherCanvasProps } from './types';
+
+export function DitherCanvas({
+ text,
+ mode,
+ speed = 2000,
+ threshold = 128,
+ font = 'bold 120px Space Grotesk',
+ color = 'currentColor',
+ className = '',
+ delay = 0,
+ onComplete,
+ lowDetail = false,
+}: DitherCanvasProps): React.ReactElement {
+ const canvasRef = useRef(null);
+ const prefersReducedMotion = useReducedMotion();
+
+ // Adjust mode for accessibility
+ const effectiveMode = prefersReducedMotion ? 'static' : mode;
+
+ // Get dither engine
+ const engine = useMemo(() => getDitherEngine(lowDetail), [lowDetail]);
+
+ // Get canvas scale for retina displays
+ const scale = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
+
+ // Generate animation frames
+ const frames = useMemo(() => {
+ // Skip frame generation during SSR
+ if (typeof window === 'undefined') {
+ return [];
+ }
+
+ try {
+ const options = { font, threshold, color, scale, lowDetail };
+
+ if (effectiveMode === 'reveal') {
+ return engine.generateRevealFrames(text, options, 60);
+ }
+
+ if (effectiveMode === 'pulse') {
+ return engine.generatePulseFrames(text, options, 30);
+ }
+
+ // Static mode - single frame
+ return [engine.ditherText(text, options)];
+ } catch (error) {
+ console.error('Failed to generate dither frames:', error);
+ return [];
+ }
+ }, [text, font, threshold, color, effectiveMode, engine, scale, lowDetail]);
+
+ // Animate frames
+ const { currentFrame } = useDitherAnimation({
+ frames,
+ mode: effectiveMode,
+ speed,
+ delay,
+ onComplete,
+ });
+
+ // Render current frame to canvas
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas || !currentFrame) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ // Set canvas dimensions
+ canvas.width = currentFrame.width;
+ canvas.height = currentFrame.height;
+
+ // Render frame
+ ctx.putImageData(currentFrame, 0, 0);
+ }, [currentFrame]);
+
+ // Get canvas dimensions for styling
+ const dimensions = useMemo(() => {
+ if (frames.length === 0 || !frames[0]) return { width: 0, height: 0 };
+ return {
+ width: frames[0].width / scale,
+ height: frames[0].height / scale,
+ };
+ }, [frames, scale]);
+
+ return (
+
+ {/* Canvas element */}
+
+
+ {/* Hidden text for screen readers */}
+ {text}
+
+ );
+}
diff --git a/packages/ui/src/DitherCanvas/types.ts b/packages/ui/src/DitherCanvas/types.ts
new file mode 100644
index 0000000..d68429a
--- /dev/null
+++ b/packages/ui/src/DitherCanvas/types.ts
@@ -0,0 +1,35 @@
+/**
+ * Type definitions for DitherCanvas components
+ */
+
+export interface DitherCanvasProps {
+ /** Text to render and dither */
+ text: string;
+
+ /** Animation mode */
+ mode: 'reveal' | 'pulse' | 'static';
+
+ /** Animation speed in milliseconds (default: 2000) */
+ speed?: number;
+
+ /** Dithering threshold 0-255 (default: 128) */
+ threshold?: number;
+
+ /** Font string (e.g., "bold 120px Space Grotesk") */
+ font?: string;
+
+ /** Text color (default: currentColor) */
+ color?: string;
+
+ /** Additional CSS classes */
+ className?: string;
+
+ /** Delay before animation starts (ms, default: 0) */
+ delay?: number;
+
+ /** Callback when animation completes (reveal mode only) */
+ onComplete?: () => void;
+
+ /** Use low-detail dithering for performance */
+ lowDetail?: boolean;
+}
diff --git a/packages/ui/src/DitherText/DitherHeadline.tsx b/packages/ui/src/DitherText/DitherHeadline.tsx
new file mode 100644
index 0000000..23d74ac
--- /dev/null
+++ b/packages/ui/src/DitherText/DitherHeadline.tsx
@@ -0,0 +1,48 @@
+/**
+ * DitherHeadline Component
+ *
+ * Large hero headline with dramatic dithered reveal animation.
+ * Supports multi-line text with staggered line reveals.
+ */
+
+'use client';
+
+import { DitherCanvas } from '../DitherCanvas/DitherCanvas';
+import type { DitherHeadlineProps } from './types';
+
+export function DitherHeadline({
+ children,
+ delay = 500,
+ stagger = 150,
+ speed = 1500,
+ fontSize = '120px',
+ fontWeight = '800',
+ className = '',
+ onComplete,
+}: DitherHeadlineProps): React.ReactElement {
+ // Split text into lines
+ const lines = children.split(/\n|
/);
+
+ // Track completion
+ const handleLineComplete = () => {
+ onComplete?.();
+ };
+
+ return (
+
+ {lines.map((line, index) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/ui/src/DitherText/DitherSectionHeader.tsx b/packages/ui/src/DitherText/DitherSectionHeader.tsx
new file mode 100644
index 0000000..83519da
--- /dev/null
+++ b/packages/ui/src/DitherText/DitherSectionHeader.tsx
@@ -0,0 +1,32 @@
+/**
+ * DitherSectionHeader Component
+ *
+ * Section header with subtle pulsing dither effect.
+ * Continuously animates for a "living" feel without being distracting.
+ */
+
+'use client';
+
+import { DitherCanvas } from '../DitherCanvas/DitherCanvas';
+import type { DitherSectionHeaderProps } from './types';
+
+export function DitherSectionHeader({
+ children,
+ pulseSpeed = 3000,
+ fontSize = '56px',
+ fontWeight = '700',
+ className = '',
+}: DitherSectionHeaderProps): React.ReactElement {
+ return (
+
+
+
+ );
+}
diff --git a/packages/ui/src/DitherText/types.ts b/packages/ui/src/DitherText/types.ts
new file mode 100644
index 0000000..d778200
--- /dev/null
+++ b/packages/ui/src/DitherText/types.ts
@@ -0,0 +1,46 @@
+/**
+ * Type definitions for DitherText components
+ */
+
+export interface DitherHeadlineProps {
+ /** Text content (can include line breaks) */
+ children: string;
+
+ /** Delay before animation starts (ms) */
+ delay?: number;
+
+ /** Delay between each line stagger (ms) */
+ stagger?: number;
+
+ /** Animation speed (ms) */
+ speed?: number;
+
+ /** Font size (CSS value, e.g., "120px") */
+ fontSize?: string;
+
+ /** Font weight (CSS value, e.g., "800") */
+ fontWeight?: string;
+
+ /** Additional CSS classes */
+ className?: string;
+
+ /** Callback when all lines complete */
+ onComplete?: () => void;
+}
+
+export interface DitherSectionHeaderProps {
+ /** Header text */
+ children: string;
+
+ /** Pulse speed (ms for full cycle) */
+ pulseSpeed?: number;
+
+ /** Font size (CSS value, e.g., "56px") */
+ fontSize?: string;
+
+ /** Font weight (CSS value, e.g., "700") */
+ fontWeight?: string;
+
+ /** Additional CSS classes */
+ className?: string;
+}
diff --git a/packages/ui/src/FlowDiagram/FlowDiagram.tsx b/packages/ui/src/FlowDiagram/FlowDiagram.tsx
new file mode 100644
index 0000000..8f48ed5
--- /dev/null
+++ b/packages/ui/src/FlowDiagram/FlowDiagram.tsx
@@ -0,0 +1,318 @@
+/**
+ * FlowDiagram Component
+ *
+ * Animated SVG diagram showing the flow from AI Agent → Semantic Query → Registry → Tools.
+ * Uses stroke-dashoffset animation for path reveals and expanding circles for radiating effect.
+ */
+
+'use client';
+
+import type { FlowDiagramProps } from './types';
+
+export function FlowDiagram({
+ className = '',
+ speed = 5000,
+ autoPlay = true,
+}: FlowDiagramProps): React.ReactElement {
+ const animationDuration = `${speed / 1000}s`;
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/ui/src/FlowDiagram/types.ts b/packages/ui/src/FlowDiagram/types.ts
new file mode 100644
index 0000000..5bc4f85
--- /dev/null
+++ b/packages/ui/src/FlowDiagram/types.ts
@@ -0,0 +1,14 @@
+/**
+ * Type definitions for FlowDiagram component
+ */
+
+export interface FlowDiagramProps {
+ /** Additional CSS classes */
+ className?: string;
+
+ /** Animation speed in milliseconds (default: 5000) */
+ speed?: number;
+
+ /** Whether to start animation immediately (default: true) */
+ autoPlay?: boolean;
+}
diff --git a/packages/ui/src/system/canvas/DitherEngine.ts b/packages/ui/src/system/canvas/DitherEngine.ts
new file mode 100644
index 0000000..979adb8
--- /dev/null
+++ b/packages/ui/src/system/canvas/DitherEngine.ts
@@ -0,0 +1,192 @@
+/**
+ * DitherEngine - Core Bayer Dithering Algorithm
+ *
+ * Provides high-performance canvas-based text dithering using
+ * ordered Bayer matrix dithering. Supports pre-computed animation frames
+ * for reveal and pulse effects.
+ */
+
+import { getBayerMatrix, getBayerMatrixSize } from './bayerMatrix';
+
+export interface DitherOptions {
+ /** Font string (e.g., "bold 120px Space Grotesk") */
+ font: string;
+ /** Dithering threshold (0-255, lower = more black pixels) */
+ threshold: number;
+ /** Text color (default: currentColor) */
+ color?: string;
+ /** Canvas scaling for high-DPI displays */
+ scale?: number;
+ /** Use low-detail matrix for performance */
+ lowDetail?: boolean;
+}
+
+export class DitherEngine {
+ private bayerMatrix: number[][];
+ private matrixSize: number;
+
+ constructor(lowDetail = false) {
+ this.bayerMatrix = getBayerMatrix(lowDetail);
+ this.matrixSize = getBayerMatrixSize(this.bayerMatrix);
+ }
+
+ /**
+ * Generate dithered text as ImageData
+ *
+ * @param text - Text to dither
+ * @param options - Dithering options
+ * @param width - Canvas width (optional, auto-calculated if not provided)
+ * @param height - Canvas height (optional, auto-calculated if not provided)
+ * @returns ImageData with dithered text
+ */
+ ditherText(text: string, options: DitherOptions, width?: number, height?: number): ImageData {
+ // SSR safety: return empty ImageData if not in browser
+ if (typeof document === 'undefined') {
+ // Return 1x1 transparent ImageData for SSR
+ return new ImageData(1, 1);
+ }
+
+ const { font, threshold, color = '#000000', scale = 1 } = options;
+
+ // Create temporary canvas to measure text
+ const tempCanvas = document.createElement('canvas');
+ const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
+ if (!tempCtx) throw new Error('Failed to get 2D context');
+
+ tempCtx.font = font;
+ const metrics = tempCtx.measureText(text);
+
+ // Calculate dimensions
+ const w =
+ width ||
+ Math.ceil(metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight + 20) * scale;
+ const h =
+ height ||
+ Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + 20) * scale;
+
+ // Set canvas size
+ tempCanvas.width = w;
+ tempCanvas.height = h;
+
+ // Draw text
+ tempCtx.scale(scale, scale);
+ tempCtx.font = font;
+ tempCtx.fillStyle = color;
+ tempCtx.textBaseline = 'top';
+ tempCtx.fillText(text, 10, 10);
+
+ // Get image data
+ const imageData = tempCtx.getImageData(0, 0, w, h);
+ const data = imageData.data;
+
+ // Apply Bayer dithering
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const i = (y * w + x) * 4;
+
+ // Convert to grayscale
+ const r = data[i] ?? 0;
+ const g = data[i + 1] ?? 0;
+ const b = data[i + 2] ?? 0;
+ const a = data[i + 3] ?? 0;
+
+ // Skip fully transparent pixels
+ if (a === 0) continue;
+
+ const grayscale = r * 0.299 + g * 0.587 + b * 0.114;
+
+ // Get Bayer matrix value
+ const bayerValue = this.bayerMatrix[y % this.matrixSize]?.[x % this.matrixSize] ?? 0;
+
+ // Apply threshold with Bayer value
+ const output = grayscale > threshold + bayerValue ? 255 : 0;
+
+ data[i] = output;
+ data[i + 1] = output;
+ data[i + 2] = output;
+ data[i + 3] = output === 255 ? 0 : 255; // Invert alpha for black pixels
+ }
+ }
+
+ return imageData;
+ }
+
+ /**
+ * Generate reveal animation frames
+ *
+ * Animates threshold from 255 (hidden) to 0 (fully visible)
+ *
+ * @param text - Text to animate
+ * @param options - Dithering options (threshold will be overridden)
+ * @param steps - Number of frames (default: 60 = 1s at 60fps)
+ * @returns Array of ImageData frames
+ */
+ generateRevealFrames(text: string, options: DitherOptions, steps = 60): ImageData[] {
+ const frames: ImageData[] = [];
+
+ for (let i = 0; i < steps; i++) {
+ // Ease-out curve for smooth reveal
+ const progress = this.easeOutCubic(i / (steps - 1));
+ const threshold = Math.floor(255 - progress * 255);
+
+ const frameData = this.ditherText(text, {
+ ...options,
+ threshold,
+ });
+
+ frames.push(frameData);
+ }
+
+ return frames;
+ }
+
+ /**
+ * Generate pulse animation frames
+ *
+ * Oscillates threshold for a breathing/pulsing effect
+ *
+ * @param text - Text to animate
+ * @param options - Dithering options
+ * @param phases - Number of frames in full cycle (default: 30)
+ * @returns Array of ImageData frames
+ */
+ generatePulseFrames(text: string, options: DitherOptions, phases = 30): ImageData[] {
+ const frames: ImageData[] = [];
+ const baseThreshold = options.threshold || 128;
+ const amplitude = 40; // Pulse intensity
+
+ for (let i = 0; i < phases; i++) {
+ // Sine wave oscillation
+ const progress = Math.sin((i / phases) * Math.PI * 2);
+ const threshold = Math.floor(baseThreshold + progress * amplitude);
+
+ const frameData = this.ditherText(text, {
+ ...options,
+ threshold: Math.max(0, Math.min(255, threshold)),
+ });
+
+ frames.push(frameData);
+ }
+
+ return frames;
+ }
+
+ /**
+ * Ease-out cubic curve for smooth animations
+ */
+ private easeOutCubic(t: number): number {
+ return 1 - (1 - t) ** 3;
+ }
+}
+
+/**
+ * Singleton instance for reuse across components
+ */
+let ditherEngineInstance: DitherEngine | null = null;
+
+export function getDitherEngine(lowDetail = false): DitherEngine {
+ if (!ditherEngineInstance) {
+ ditherEngineInstance = new DitherEngine(lowDetail);
+ }
+ return ditherEngineInstance;
+}
diff --git a/packages/ui/src/system/canvas/bayerMatrix.ts b/packages/ui/src/system/canvas/bayerMatrix.ts
new file mode 100644
index 0000000..f319d60
--- /dev/null
+++ b/packages/ui/src/system/canvas/bayerMatrix.ts
@@ -0,0 +1,61 @@
+/**
+ * Bayer Matrix for Ordered Dithering
+ *
+ * 8×8 Bayer matrix used for ordered dithering algorithms.
+ * Values are normalized to 0-255 range for easy threshold comparison.
+ *
+ * This creates the characteristic halftone/dot-matrix pattern
+ * while avoiding the randomness of error-diffusion dithering.
+ */
+
+/**
+ * Standard 8×8 Bayer matrix (normalized to 0-255)
+ *
+ * Formula: bayerMatrix[y][x] = (originalValue / 64) * 255
+ * where original values range from 0-63
+ */
+export const BAYER_MATRIX_8X8: number[][] = [
+ [0, 32, 8, 40, 2, 34, 10, 42],
+ [48, 16, 56, 24, 50, 18, 58, 26],
+ [12, 44, 4, 36, 14, 46, 6, 38],
+ [60, 28, 52, 20, 62, 30, 54, 22],
+ [3, 35, 11, 43, 1, 33, 9, 41],
+ [51, 19, 59, 27, 49, 17, 57, 25],
+ [15, 47, 7, 39, 13, 45, 5, 37],
+ [63, 31, 55, 23, 61, 29, 53, 21],
+].map((row) => row.map((val) => Math.floor((val / 64) * 255)));
+
+/**
+ * 4×4 Bayer matrix for mobile/performance optimization
+ *
+ * Smaller matrix = faster computation, less detailed dithering
+ * Use this on mobile devices or for subtle effects
+ */
+export const BAYER_MATRIX_4X4: number[][] = [
+ [0, 8, 2, 10],
+ [12, 4, 14, 6],
+ [3, 11, 1, 9],
+ [15, 7, 13, 5],
+].map((row) => row.map((val) => Math.floor((val / 16) * 255)));
+
+/**
+ * Get the appropriate Bayer matrix based on device capabilities
+ *
+ * @param preferLowDetail - Force 4×4 matrix for performance
+ * @returns Bayer matrix (either 8×8 or 4×4)
+ */
+export function getBayerMatrix(preferLowDetail = false): number[][] {
+ // Use smaller matrix on mobile or when explicitly requested
+ if (preferLowDetail || (typeof window !== 'undefined' && window.innerWidth < 640)) {
+ return BAYER_MATRIX_4X4;
+ }
+
+ return BAYER_MATRIX_8X8;
+}
+
+/**
+ * Get matrix size (4 or 8)
+ */
+export function getBayerMatrixSize(matrix: number[][]): number {
+ return matrix.length;
+}
diff --git a/packages/ui/src/system/hooks/useDitherAnimation.ts b/packages/ui/src/system/hooks/useDitherAnimation.ts
new file mode 100644
index 0000000..ccf16c8
--- /dev/null
+++ b/packages/ui/src/system/hooks/useDitherAnimation.ts
@@ -0,0 +1,126 @@
+/**
+ * useDitherAnimation Hook
+ *
+ * Manages RAF-based frame playback for dither animations.
+ * Handles reveal (one-shot) and pulse (looping) animation modes.
+ */
+
+import { useEffect, useRef, useState } from 'react';
+
+export interface DitherAnimationOptions {
+ /** Pre-computed animation frames */
+ frames: ImageData[];
+ /** Animation mode */
+ mode: 'reveal' | 'pulse' | 'static';
+ /** Animation duration in milliseconds */
+ speed?: number;
+ /** Delay before starting animation (ms) */
+ delay?: number;
+ /** Called when animation completes (reveal mode only) */
+ onComplete?: () => void;
+ /** Pause animation (for intersection observer) */
+ paused?: boolean;
+}
+
+export interface DitherAnimationState {
+ /** Current frame ImageData */
+ currentFrame: ImageData | null;
+ /** Whether animation is complete */
+ isComplete: boolean;
+ /** Current frame index */
+ frameIndex: number;
+}
+
+/**
+ * Hook for managing dither animation playback
+ */
+export function useDitherAnimation(options: DitherAnimationOptions): DitherAnimationState {
+ const { frames, mode, speed = 2000, delay = 0, onComplete, paused = false } = options;
+
+ const [currentFrame, setCurrentFrame] = useState(
+ mode === 'static' ? frames[frames.length - 1] || null : null
+ );
+ const [isComplete, setIsComplete] = useState(mode === 'static');
+ const [frameIndex, setFrameIndex] = useState(0);
+
+ const rafRef = useRef(null);
+
+ useEffect(() => {
+ // Don't animate if static mode or paused
+ if (mode === 'static' || paused || frames.length === 0) {
+ return;
+ }
+
+ let animationStartTime: number | null = null;
+
+ const animate = (timestamp: number) => {
+ // Initialize start time
+ if (animationStartTime === null) {
+ animationStartTime = timestamp + delay;
+ rafRef.current = requestAnimationFrame(animate);
+ return;
+ }
+
+ // Wait for delay
+ if (timestamp < animationStartTime) {
+ rafRef.current = requestAnimationFrame(animate);
+ return;
+ }
+
+ // Calculate progress
+ const elapsed = timestamp - animationStartTime;
+ const progress = Math.min(elapsed / speed, 1);
+
+ if (mode === 'reveal') {
+ // Reveal: play once from start to end
+ const index = Math.floor(progress * (frames.length - 1));
+ const frame = frames[index];
+ if (frame) {
+ setCurrentFrame(frame);
+ setFrameIndex(index);
+ }
+
+ if (progress >= 1) {
+ // Animation complete
+ const lastFrame = frames[frames.length - 1];
+ if (lastFrame) {
+ setCurrentFrame(lastFrame);
+ }
+ setFrameIndex(frames.length - 1);
+ setIsComplete(true);
+ onComplete?.();
+ return;
+ }
+ } else if (mode === 'pulse') {
+ // Pulse: loop continuously
+ const loopProgress = (elapsed % speed) / speed;
+ const index = Math.floor(loopProgress * frames.length) % frames.length;
+ const frame = frames[index];
+ if (frame) {
+ setCurrentFrame(frame);
+ setFrameIndex(index);
+ }
+ }
+
+ // Continue animation
+ rafRef.current = requestAnimationFrame(animate);
+ };
+
+ // Start animation
+ rafRef.current = requestAnimationFrame(animate);
+
+ // Cleanup
+ return () => {
+ if (rafRef.current !== null) {
+ cancelAnimationFrame(rafRef.current);
+ rafRef.current = null;
+ }
+ };
+ }, [frames, mode, speed, delay, onComplete, paused]);
+
+ return {
+ currentFrame,
+ isComplete,
+ frameIndex,
+ };
+}
diff --git a/packages/ui/src/system/hooks/useReducedMotion.ts b/packages/ui/src/system/hooks/useReducedMotion.ts
new file mode 100644
index 0000000..0166477
--- /dev/null
+++ b/packages/ui/src/system/hooks/useReducedMotion.ts
@@ -0,0 +1,46 @@
+/**
+ * useReducedMotion Hook
+ *
+ * Detects if the user has requested reduced motion via
+ * the `prefers-reduced-motion` media query.
+ *
+ * Used to make animations accessible by skipping or simplifying
+ * them for users who are sensitive to motion.
+ */
+
+import { useEffect, useState } from 'react';
+
+/**
+ * Check if user prefers reduced motion
+ *
+ * @returns true if user prefers reduced motion
+ */
+export function useReducedMotion(): boolean {
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
+
+ useEffect(() => {
+ // Check if we're in a browser environment
+ if (typeof window === 'undefined') return;
+
+ // Create media query
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+
+ // Set initial value
+ setPrefersReducedMotion(mediaQuery.matches);
+
+ // Update on change
+ const handleChange = (event: MediaQueryListEvent) => {
+ setPrefersReducedMotion(event.matches);
+ };
+
+ // Listen for changes
+ mediaQuery.addEventListener('change', handleChange);
+
+ // Cleanup
+ return () => {
+ mediaQuery.removeEventListener('change', handleChange);
+ };
+ }, []);
+
+ return prefersReducedMotion;
+}
diff --git a/packages/ui/tsup.config.ts b/packages/ui/tsup.config.ts
index bf94e2c..2d31e5a 100644
--- a/packages/ui/tsup.config.ts
+++ b/packages/ui/tsup.config.ts
@@ -33,10 +33,16 @@ const entries = allFiles.filter((file) => {
// Manually add RadioGroup which doesn't match the folder/file naming convention
entries.push('src/Radio/RadioGroup.tsx');
+// Manually add DitherText components
+entries.push('src/DitherText/DitherHeadline.tsx');
+entries.push('src/DitherText/DitherSectionHeader.tsx');
+
// Manually add system hooks
entries.push('src/system/hooks/useScrollReveal.ts');
entries.push('src/system/hooks/useCountUp.ts');
entries.push('src/system/hooks/useParallax.ts');
+entries.push('src/system/hooks/useReducedMotion.ts');
+entries.push('src/system/hooks/useDitherAnimation.ts');
export default defineConfig({
entry: entries,