diff --git a/docs/classic-inbox.md b/docs/classic-inbox.md
new file mode 100644
index 0000000..ebaa2b8
--- /dev/null
+++ b/docs/classic-inbox.md
@@ -0,0 +1,47 @@
+# Classic Inbox UI
+
+This package implements the Classic Inbox surface for the paid warpSpeed OPEN
+bounty. It is designed as a reusable React Native/TypeScript module that can be
+mounted by the host app and exercised in Storybook.
+
+## Included Acceptance Surface
+
+- Top navigation row with account selector, Flow, Dashboard, Classic, and Compose actions
+- Category buttons for Inbox, Sent, Drafts, All Mail, Primary, Promotions, and Updates
+- Search and filter controls
+- Email list rendering with read and unread states
+- Selected message state
+- Empty state
+- Smooth scroll header behavior via animated scroll offset
+- Storybook fixture with representative accounts, categories, and messages
+- Static verification script for bounty-critical props and UI paths
+
+## Integration
+
+```tsx
+import { ClassicInbox } from "../packages/classic-inbox/src";
+
+
+```
+
+The component is intentionally controlled by the host app. It does not own
+network fetching, authentication, or persistence. That keeps the integration
+portable across warpSpeed apps while preserving a complete UI contract.
+
+## Validation
+
+Run:
+
+```bash
+npm test
+```
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d71989a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "warpspeed-integrations",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "node scripts/verify-classic-inbox.mjs"
+ },
+ "devDependencies": {
+ "typescript": "^5.5.0"
+ }
+}
diff --git a/packages/classic-inbox/src/ClassicInbox.stories.tsx b/packages/classic-inbox/src/ClassicInbox.stories.tsx
new file mode 100644
index 0000000..cdfb7a5
--- /dev/null
+++ b/packages/classic-inbox/src/ClassicInbox.stories.tsx
@@ -0,0 +1,96 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import React, { useState } from "react";
+import { ClassicInbox } from "./ClassicInbox";
+import type { ClassicInboxCategory, ClassicInboxMessage, InboxAccount } from "./types";
+
+const accounts: InboxAccount[] = [
+ { id: "personal", label: "Personal", address: "nate@example.com" },
+ { id: "work", label: "Work", address: "nate@warpspeed.dev" },
+];
+
+const categories: ClassicInboxCategory[] = [
+ { id: "inbox", label: "Inbox", icon: "inbox", count: 24 },
+ { id: "sent", label: "Sent", icon: "send" },
+ { id: "drafts", label: "Drafts", icon: "draft", count: 3 },
+ { id: "all", label: "All Mail", icon: "all" },
+ { id: "primary", label: "Primary", icon: "primary", count: 12 },
+ { id: "promotions", label: "Promotions", icon: "tag", count: 5 },
+ { id: "updates", label: "Updates", icon: "bell", count: 7 },
+];
+
+const messages: ClassicInboxMessage[] = [
+ {
+ id: "m-1",
+ accountId: "personal",
+ categoryId: "primary",
+ senderName: "warpSpeed OPEN",
+ senderEmail: "bounties@warpspeedopen.org",
+ subject: "Classic Inbox bounty update",
+ preview: "Your implementation checklist has been reviewed and is ready for final polish.",
+ timestampLabel: "9:41 AM",
+ unread: true,
+ starred: true,
+ hasAttachment: false,
+ },
+ {
+ id: "m-2",
+ accountId: "personal",
+ categoryId: "updates",
+ senderName: "Design System",
+ senderEmail: "tokens@warpspeed.dev",
+ subject: "Icon set refresh",
+ preview: "The latest category and navigation icons are available in the design system package.",
+ timestampLabel: "Yesterday",
+ unread: false,
+ starred: false,
+ hasAttachment: true,
+ },
+ {
+ id: "m-3",
+ accountId: "work",
+ categoryId: "promotions",
+ senderName: "Growth",
+ senderEmail: "growth@warpspeed.dev",
+ subject: "Campaign draft ready",
+ preview: "Promotional campaign copy is waiting for your final approval.",
+ timestampLabel: "Mon",
+ unread: true,
+ starred: false,
+ hasAttachment: false,
+ },
+];
+
+const meta: Meta = {
+ title: "Bounties/ClassicInbox",
+ component: ClassicInbox,
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Interactive: Story = {
+ render: () => {
+ const [activeAccountId, setActiveAccountId] = useState("personal");
+ const [activeCategoryId, setActiveCategoryId] = useState("inbox");
+ const [selectedMessageId, setSelectedMessageId] = useState();
+
+ return (
+ undefined}
+ onFlowPress={() => undefined}
+ onDashboardPress={() => undefined}
+ onClassicPress={() => undefined}
+ onMessagePress={(message) => setSelectedMessageId(message.id)}
+ />
+ );
+ },
+};
diff --git a/packages/classic-inbox/src/ClassicInbox.tsx b/packages/classic-inbox/src/ClassicInbox.tsx
new file mode 100644
index 0000000..b12b21a
--- /dev/null
+++ b/packages/classic-inbox/src/ClassicInbox.tsx
@@ -0,0 +1,444 @@
+import React, { useMemo, useRef, useState } from "react";
+import {
+ Animated,
+ FlatList,
+ Pressable,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import type {
+ ClassicInboxCategory,
+ ClassicInboxMessage,
+ ClassicInboxProps,
+ InboxAccount,
+} from "./types";
+
+const HEADER_HIDE_DISTANCE = 88;
+
+const DEFAULT_FILTERS = [
+ { id: "all", label: "All" },
+ { id: "unread", label: "Unread" },
+ { id: "starred", label: "Starred" },
+ { id: "attachments", label: "Files" },
+] as const;
+
+type FilterId = (typeof DEFAULT_FILTERS)[number]["id"];
+
+export function ClassicInbox({
+ accounts,
+ categories,
+ messages,
+ activeAccountId,
+ activeCategoryId,
+ selectedMessageId,
+ onAccountChange,
+ onCategoryChange,
+ onCompose,
+ onFlowPress,
+ onDashboardPress,
+ onClassicPress,
+ onMessagePress,
+}: ClassicInboxProps) {
+ const [query, setQuery] = useState("");
+ const [filter, setFilter] = useState("all");
+ const scrollY = useRef(new Animated.Value(0)).current;
+
+ const activeAccount = useMemo(
+ () => accounts.find((account) => account.id === activeAccountId) ?? accounts[0],
+ [accounts, activeAccountId],
+ );
+
+ const filteredMessages = useMemo(() => {
+ const normalizedQuery = query.trim().toLowerCase();
+ return messages.filter((message) => {
+ if (message.accountId !== activeAccountId) {
+ return false;
+ }
+ if (
+ activeCategoryId !== "inbox" &&
+ activeCategoryId !== "all" &&
+ message.categoryId !== activeCategoryId
+ ) {
+ return false;
+ }
+ if (filter === "unread" && !message.unread) {
+ return false;
+ }
+ if (filter === "starred" && !message.starred) {
+ return false;
+ }
+ if (filter === "attachments" && !message.hasAttachment) {
+ return false;
+ }
+ if (!normalizedQuery) {
+ return true;
+ }
+ const haystack = [
+ message.senderName,
+ message.senderEmail,
+ message.subject,
+ message.preview,
+ ].join(" ").toLowerCase();
+ return haystack.includes(normalizedQuery);
+ });
+ }, [activeAccountId, activeCategoryId, filter, messages, query]);
+
+ const headerTranslateY = scrollY.interpolate({
+ inputRange: [0, HEADER_HIDE_DISTANCE],
+ outputRange: [0, -HEADER_HIDE_DISTANCE],
+ extrapolate: "clamp",
+ });
+
+ return (
+
+
+
+
+
+
+
+ Filter
+
+
+
+ {DEFAULT_FILTERS.map((item) => (
+ setFilter(item.id)}
+ style={[styles.filterChip, filter === item.id && styles.filterChipActive]}
+ >
+
+ {item.label}
+
+
+ ))}
+
+
+
+ item.id}
+ contentContainerStyle={styles.listContent}
+ onScroll={Animated.event(
+ [{ nativeEvent: { contentOffset: { y: scrollY } } }],
+ { useNativeDriver: true },
+ )}
+ scrollEventThrottle={16}
+ ListEmptyComponent={}
+ renderItem={({ item }) => (
+ onMessagePress(item)}
+ />
+ )}
+ />
+
+ );
+}
+
+function TopNavigation({
+ accounts,
+ activeAccount,
+ onAccountChange,
+ onCompose,
+ onFlowPress,
+ onDashboardPress,
+ onClassicPress,
+}: {
+ accounts: InboxAccount[];
+ activeAccount?: InboxAccount;
+ onAccountChange: (accountId: string) => void;
+ onCompose: () => void;
+ onFlowPress?: () => void;
+ onDashboardPress?: () => void;
+ onClassicPress?: () => void;
+}) {
+ return (
+
+
+ Account
+
+ {accounts.map((account) => (
+ onAccountChange(account.id)}
+ style={[
+ styles.accountPill,
+ account.id === activeAccount?.id && styles.accountPillActive,
+ ]}
+ >
+
+ {account.label}
+
+
+ ))}
+
+
+ {activeAccount?.address}
+
+
+
+
+
+
+
+ Compose
+
+
+
+ );
+}
+
+function NavAction({
+ label,
+ selected,
+ onPress,
+}: {
+ label: string;
+ selected?: boolean;
+ onPress?: () => void;
+}) {
+ return (
+
+ {label}
+
+ );
+}
+
+function CategoryRail({
+ categories,
+ activeCategoryId,
+ onCategoryChange,
+}: {
+ categories: ClassicInboxCategory[];
+ activeCategoryId: string;
+ onCategoryChange: (categoryId: string) => void;
+}) {
+ return (
+
+ {categories.map((category) => {
+ const active = category.id === activeCategoryId;
+ return (
+ onCategoryChange(category.id)}
+ style={[styles.categoryButton, active && styles.categoryButtonActive]}
+ >
+
+ {iconForCategory(category.icon)}
+
+
+ {category.label}
+
+ {typeof category.count === "number" && (
+
+ {category.count}
+
+ )}
+
+ );
+ })}
+
+ );
+}
+
+function MessageRow({
+ message,
+ selected,
+ onPress,
+}: {
+ message: ClassicInboxMessage;
+ selected: boolean;
+ onPress: () => void;
+}) {
+ return (
+
+
+ {initials(message.senderName)}
+
+
+
+
+ {message.senderName}
+
+
+ {message.timestampLabel}
+
+
+
+ {message.subject}
+
+
+ {message.preview}
+
+
+
+ {message.starred && ★}
+ {message.hasAttachment && ⌕}
+ {message.unread && }
+
+
+ );
+}
+
+function EmptyInbox({ query }: { query: string }) {
+ return (
+
+ {query ? "No matching messages" : "No mail here"}
+
+ {query ? "Try another search or clear filters." : "Switch category or account to keep browsing."}
+
+
+ );
+}
+
+function initials(name: string): string {
+ return name
+ .split(/\s+/)
+ .filter(Boolean)
+ .slice(0, 2)
+ .map((part) => part[0]?.toUpperCase())
+ .join("") || "?";
+}
+
+function iconForCategory(icon: string | undefined): string {
+ switch (icon) {
+ case "send":
+ return "↗";
+ case "draft":
+ return "✎";
+ case "tag":
+ return "#";
+ case "bell":
+ return "!";
+ case "primary":
+ return "◆";
+ case "all":
+ return "◎";
+ default:
+ return "▣";
+ }
+}
+
+const styles = StyleSheet.create({
+ root: { flex: 1, backgroundColor: "#f6f8fb" },
+ header: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ zIndex: 10,
+ backgroundColor: "#ffffff",
+ borderBottomWidth: 1,
+ borderBottomColor: "#e5e9f0",
+ },
+ topNav: { paddingHorizontal: 16, paddingTop: 14, paddingBottom: 10 },
+ accountBlock: { marginBottom: 10 },
+ eyebrow: { color: "#697386", fontSize: 11, fontWeight: "700", textTransform: "uppercase" },
+ accountChoices: { flexDirection: "row", gap: 8, marginTop: 6 },
+ accountPill: { borderWidth: 1, borderColor: "#d9dee8", borderRadius: 8, paddingHorizontal: 10, paddingVertical: 6 },
+ accountPillActive: { backgroundColor: "#172033", borderColor: "#172033" },
+ accountPillText: { color: "#3b4454", fontWeight: "700" },
+ accountPillTextActive: { color: "#ffffff" },
+ accountAddress: { color: "#697386", fontSize: 12, marginTop: 5 },
+ primaryActions: { flexDirection: "row", flexWrap: "wrap", gap: 8, alignItems: "center" },
+ navAction: { borderRadius: 8, paddingHorizontal: 10, paddingVertical: 8, backgroundColor: "#eef2f7" },
+ navActionSelected: { backgroundColor: "#d8e7ff" },
+ navActionText: { color: "#3b4454", fontWeight: "700" },
+ navActionTextSelected: { color: "#1456a3" },
+ composeButton: { borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, backgroundColor: "#1456a3" },
+ composeText: { color: "#ffffff", fontWeight: "800" },
+ categoryRail: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingBottom: 10, flexWrap: "wrap" },
+ categoryButton: { flexDirection: "row", alignItems: "center", gap: 6, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 7, backgroundColor: "#f4f6fa" },
+ categoryButtonActive: { backgroundColor: "#172033" },
+ categoryIcon: { color: "#697386", fontWeight: "800" },
+ categoryText: { color: "#3b4454", fontWeight: "700" },
+ categoryTextActive: { color: "#ffffff" },
+ categoryCount: { color: "#697386", fontSize: 12, fontWeight: "700" },
+ searchRow: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingBottom: 10 },
+ searchInput: { flex: 1, borderRadius: 8, borderWidth: 1, borderColor: "#d9dee8", paddingHorizontal: 12, paddingVertical: 9, backgroundColor: "#ffffff" },
+ filterButton: { borderRadius: 8, borderWidth: 1, borderColor: "#d9dee8", paddingHorizontal: 12, justifyContent: "center" },
+ filterButtonText: { color: "#172033", fontWeight: "800" },
+ filterRail: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingBottom: 12 },
+ filterChip: { borderRadius: 8, paddingHorizontal: 10, paddingVertical: 6, backgroundColor: "#eef2f7" },
+ filterChipActive: { backgroundColor: "#1456a3" },
+ filterChipText: { color: "#3b4454", fontWeight: "700" },
+ filterChipTextActive: { color: "#ffffff" },
+ listContent: { paddingTop: 226, paddingHorizontal: 12, paddingBottom: 24 },
+ messageRow: { flexDirection: "row", gap: 12, borderRadius: 8, backgroundColor: "#ffffff", borderWidth: 1, borderColor: "#e5e9f0", padding: 12, marginBottom: 8 },
+ messageRowUnread: { borderColor: "#9cc7ff", backgroundColor: "#f8fbff" },
+ messageRowSelected: { borderColor: "#1456a3" },
+ senderAvatar: { width: 38, height: 38, borderRadius: 19, backgroundColor: "#d9dee8", alignItems: "center", justifyContent: "center" },
+ senderAvatarUnread: { backgroundColor: "#1456a3" },
+ senderAvatarText: { color: "#ffffff", fontWeight: "800" },
+ messageContent: { flex: 1, minWidth: 0 },
+ messageHeader: { flexDirection: "row", justifyContent: "space-between", gap: 12 },
+ senderName: { flex: 1, color: "#172033", fontWeight: "600" },
+ unreadText: { fontWeight: "900" },
+ timestamp: { color: "#697386", fontSize: 12 },
+ subject: { color: "#172033", marginTop: 3 },
+ preview: { color: "#697386", marginTop: 3 },
+ messageMeta: { width: 22, alignItems: "center", gap: 6 },
+ star: { color: "#d9a400" },
+ attachment: { color: "#697386" },
+ unreadDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: "#1456a3" },
+ emptyState: { alignItems: "center", paddingVertical: 48 },
+ emptyTitle: { color: "#172033", fontWeight: "800", fontSize: 16 },
+ emptyText: { color: "#697386", marginTop: 6 },
+});
diff --git a/packages/classic-inbox/src/index.ts b/packages/classic-inbox/src/index.ts
new file mode 100644
index 0000000..51151f1
--- /dev/null
+++ b/packages/classic-inbox/src/index.ts
@@ -0,0 +1,2 @@
+export { ClassicInbox } from "./ClassicInbox";
+export type { ClassicInboxCategory, ClassicInboxMessage, ClassicInboxProps, InboxAccount } from "./types";
diff --git a/packages/classic-inbox/src/types.ts b/packages/classic-inbox/src/types.ts
new file mode 100644
index 0000000..9bd8f31
--- /dev/null
+++ b/packages/classic-inbox/src/types.ts
@@ -0,0 +1,42 @@
+export interface InboxAccount {
+ id: string;
+ label: string;
+ address: string;
+}
+
+export interface ClassicInboxCategory {
+ id: string;
+ label: string;
+ icon?: "inbox" | "send" | "draft" | "all" | "primary" | "tag" | "bell";
+ count?: number;
+}
+
+export interface ClassicInboxMessage {
+ id: string;
+ accountId: string;
+ categoryId: string;
+ senderName: string;
+ senderEmail: string;
+ subject: string;
+ preview: string;
+ timestampLabel: string;
+ unread: boolean;
+ starred?: boolean;
+ hasAttachment?: boolean;
+}
+
+export interface ClassicInboxProps {
+ accounts: InboxAccount[];
+ categories: ClassicInboxCategory[];
+ messages: ClassicInboxMessage[];
+ activeAccountId: string;
+ activeCategoryId: string;
+ selectedMessageId?: string;
+ onAccountChange: (accountId: string) => void;
+ onCategoryChange: (categoryId: string) => void;
+ onCompose: () => void;
+ onFlowPress?: () => void;
+ onDashboardPress?: () => void;
+ onClassicPress?: () => void;
+ onMessagePress: (message: ClassicInboxMessage) => void;
+}
diff --git a/scripts/verify-classic-inbox.mjs b/scripts/verify-classic-inbox.mjs
new file mode 100644
index 0000000..47759b2
--- /dev/null
+++ b/scripts/verify-classic-inbox.mjs
@@ -0,0 +1,67 @@
+import { readFileSync, existsSync } from "node:fs";
+
+const requiredFiles = [
+ "packages/classic-inbox/src/ClassicInbox.tsx",
+ "packages/classic-inbox/src/ClassicInbox.stories.tsx",
+ "packages/classic-inbox/src/types.ts",
+ "packages/classic-inbox/src/index.ts",
+ "docs/classic-inbox.md",
+];
+
+for (const file of requiredFiles) {
+ if (!existsSync(file)) {
+ throw new Error(`Missing required file: ${file}`);
+ }
+}
+
+const source = readFileSync("packages/classic-inbox/src/ClassicInbox.tsx", "utf8");
+const story = readFileSync("packages/classic-inbox/src/ClassicInbox.stories.tsx", "utf8");
+
+const sourceRequirements = [
+ "ClassicInbox",
+ "TopNavigation",
+ "CategoryRail",
+ "TextInput",
+ "FlatList",
+ "Animated.event",
+ "headerTranslateY",
+ "Search mail",
+ "Filter",
+ "Compose",
+ "Flow",
+ "Dashboard",
+ "Classic",
+ "unread",
+ "starred",
+ "hasAttachment",
+ "onAccountChange",
+ "onCategoryChange",
+ "onMessagePress",
+];
+
+for (const token of sourceRequirements) {
+ if (!source.includes(token)) {
+ throw new Error(`ClassicInbox missing required token: ${token}`);
+ }
+}
+
+const storyRequirements = [
+ "Bounties/ClassicInbox",
+ "accounts",
+ "categories",
+ "messages",
+ "primary",
+ "promotions",
+ "updates",
+ "sent",
+ "drafts",
+ "all",
+];
+
+for (const token of storyRequirements) {
+ if (!story.includes(token)) {
+ throw new Error(`Story missing required token: ${token}`);
+ }
+}
+
+console.log("Classic Inbox bounty verification passed");