diff --git a/ui/AGENTS.md b/ui/AGENTS.md
index a7316a429f..ccee724d8b 100644
--- a/ui/AGENTS.md
+++ b/ui/AGENTS.md
@@ -13,7 +13,33 @@
## Critical Architecture Rules (Non-Negotiable)
-### 1. TypeScript Type Patterns (Required)
+### 1. React Imports (Required)
+
+**NEVER** import React with `import * as React`. Instead, import only what you need:
+
+**❌ DON'T:**
+
+```typescript
+import * as React from "react";
+import React, { useState } from "react";
+```
+
+**✅ DO:**
+
+```typescript
+import { useState, useEffect } from "react";
+```
+
+**When you need nothing from React (e.g., only using JSX):**
+
+```typescript
+// No React import needed - JSX works without it in modern Next.js
+export function MyComponent() {
+ return
Hello
;
+}
+```
+
+### 2. TypeScript Type Patterns (Required)
When defining union types for options, ALWAYS create a const object first, then extract the type:
diff --git a/ui/components/shadcn/README.md b/ui/components/shadcn/README.md
index af3bc0348d..127832a4f7 100644
--- a/ui/components/shadcn/README.md
+++ b/ui/components/shadcn/README.md
@@ -16,6 +16,9 @@ shadcn/
│ └── resource-stats-card/
│ ├── resource-stats-card.tsx
│ ├── resource-stats-card.example.tsx
+├── tabs/
+│ ├── tabs.tsx # Base tab components
+│ ├── generic-tabs.tsx # Generic reusable tabs with lazy loading
├── index.ts # Barrel exports
└── README.md
```
@@ -27,8 +30,83 @@ All shadcn components can be imported from `@/components/shadcn`:
```tsx
import { Card, CardHeader, CardContent } from "@/components/shadcn";
import { ResourceStatsCard } from "@/components/shadcn";
+import { GenericTabs, type TabItem } from "@/components/shadcn";
```
+### GenericTabs Component
+
+The `GenericTabs` component provides a flexible, lazy-loaded tabs interface. Content is only rendered when the tab is active, improving performance.
+
+**Basic Example:**
+
+```tsx
+import { lazy } from "react";
+import { GenericTabs, type TabItem } from "@/components/shadcn";
+
+const OverviewContent = lazy(() => import("./OverviewContent"));
+const DetailsContent = lazy(() => import("./DetailsContent"));
+
+const tabs: TabItem[] = [
+ {
+ id: "overview",
+ label: "Overview",
+ content: OverviewContent,
+ },
+ {
+ id: "details",
+ label: "Details",
+ content: DetailsContent,
+ },
+];
+
+export function MyComponent() {
+ return ;
+}
+```
+
+**With Icons and Props:**
+
+```tsx
+import { Eye, Settings } from "lucide-react";
+import { GenericTabs, type TabItem } from "@/components/shadcn";
+
+const tabs: TabItem[] = [
+ {
+ id: "view",
+ label: "View",
+ icon: ,
+ content: ViewContent,
+ contentProps: { data: myData },
+ },
+ {
+ id: "config",
+ label: "Config",
+ icon: ,
+ content: ConfigContent,
+ },
+];
+
+export function MyComponent() {
+ return (
+ console.log("Active tab:", tabId)}
+ />
+ );
+}
+```
+
+**Props:**
+
+- `tabs` - Array of `TabItem` objects
+- `defaultTabId` - Initial active tab ID (defaults to first tab)
+- `className` - Wrapper class
+- `listClassName` - TabsList class
+- `triggerClassName` - TabsTrigger class
+- `contentClassName` - TabsContent class
+- `onTabChange` - Callback fired when tab changes
+
## Adding New shadcn Components
When adding new shadcn components using the CLI:
diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts
index d96af75fe4..94ed272661 100644
--- a/ui/components/shadcn/index.ts
+++ b/ui/components/shadcn/index.ts
@@ -6,3 +6,5 @@ export * from "./card/resource-stats-card/resource-stats-card-content";
export * from "./card/resource-stats-card/resource-stats-card-divider";
export * from "./card/resource-stats-card/resource-stats-card-header";
export * from "./select/select";
+export * from "./tabs/generic-tabs";
+export * from "./tabs/tabs";
diff --git a/ui/components/shadcn/tabs/generic-tabs.tsx b/ui/components/shadcn/tabs/generic-tabs.tsx
new file mode 100644
index 0000000000..f87b4e3513
--- /dev/null
+++ b/ui/components/shadcn/tabs/generic-tabs.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import type { ComponentType, ReactNode } from "react";
+import { Suspense, useState } from "react";
+
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
+
+export interface TabItem {
+ id: string;
+ label: string;
+ icon?: ReactNode;
+ content: ComponentType<{ isActive: boolean }>;
+ contentProps?: Record;
+}
+
+interface GenericTabsProps {
+ tabs: TabItem[];
+ defaultTabId?: string;
+ className?: string;
+ listClassName?: string;
+ triggerClassName?: string;
+ contentClassName?: string;
+ onTabChange?: (tabId: string) => void;
+}
+
+/**
+ * A generic tabs component that accepts an array of tab objects with lazy-loaded content.
+ *
+ * @example
+ * const tabs: TabItem[] = [
+ * {
+ * id: "tab-1",
+ * label: "Tab 1",
+ * content: lazy(() => import("./Tab1Content")),
+ * contentProps: { key: "value" }
+ * },
+ * {
+ * id: "tab-2",
+ * label: "Tab 2",
+ * content: lazy(() => import("./Tab2Content"))
+ * }
+ * ];
+ *
+ * console.log(id)} />
+ */
+export function GenericTabs({
+ tabs,
+ defaultTabId,
+ className,
+ listClassName,
+ triggerClassName,
+ contentClassName,
+ onTabChange,
+}: GenericTabsProps) {
+ const [activeTab, setActiveTab] = useState(
+ defaultTabId || tabs[0]?.id || "",
+ );
+
+ const handleTabChange = (tabId: string) => {
+ setActiveTab(tabId);
+ onTabChange?.(tabId);
+ };
+
+ if (!tabs || tabs.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {tabs.map((tab) => (
+
+ {tab.icon && {tab.icon}}
+ {tab.label}
+
+ ))}
+
+
+ {tabs.map((tab) => {
+ const ContentComponent = tab.content;
+ const isActive = activeTab === tab.id;
+
+ return (
+
+ {isActive && (
+ Loading...}>
+
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/ui/components/shadcn/tabs/tabs.constants.ts b/ui/components/shadcn/tabs/tabs.constants.ts
new file mode 100644
index 0000000000..62f7e9c144
--- /dev/null
+++ b/ui/components/shadcn/tabs/tabs.constants.ts
@@ -0,0 +1,43 @@
+/**
+ * Trigger component style parts using semantic class names
+ */
+const TRIGGER_STYLES = {
+ base: "relative inline-flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50",
+ border: "border-r border-[#E9E9F0] last:border-r-0 dark:border-[#171D30]",
+ text: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
+ active:
+ "data-[state=active]:text-slate-900 dark:data-[state=active]:text-white",
+ underline:
+ "after:absolute after:bottom-0 after:left-1/2 after:h-0.5 after:w-0 after:-translate-x-1/2 after:bg-[#20B853] after:transition-all data-[state=active]:after:w-[calc(100%-theme(spacing.5))]",
+ focus:
+ "focus-visible:ring-2 focus-visible:ring-[#20B853] focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:focus-visible:ring-offset-slate-950",
+ icon: "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+} as const;
+
+/**
+ * Content component styles
+ */
+export const CONTENT_STYLES =
+ "mt-2 focus-visible:rounded-md focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50" as const;
+
+/**
+ * Build trigger className by combining style parts
+ */
+export function buildTriggerClassName(): string {
+ return [
+ TRIGGER_STYLES.base,
+ TRIGGER_STYLES.border,
+ TRIGGER_STYLES.text,
+ TRIGGER_STYLES.active,
+ TRIGGER_STYLES.underline,
+ TRIGGER_STYLES.focus,
+ TRIGGER_STYLES.icon,
+ ].join(" ");
+}
+
+/**
+ * Build list className
+ */
+export function buildListClassName(): string {
+ return "inline-flex w-full items-center border-[#E9E9F0] dark:border-[#171D30]";
+}
diff --git a/ui/components/shadcn/tabs/tabs.tsx b/ui/components/shadcn/tabs/tabs.tsx
new file mode 100644
index 0000000000..8208a7d292
--- /dev/null
+++ b/ui/components/shadcn/tabs/tabs.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import type { ComponentProps } from "react";
+
+import { cn } from "@/lib/utils";
+
+import {
+ buildListClassName,
+ buildTriggerClassName,
+ CONTENT_STYLES,
+} from "./tabs.constants";
+
+function Tabs({
+ className,
+ ...props
+}: ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsList({
+ className,
+ ...props
+}: ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsContent({
+ className,
+ ...props
+}: ComponentProps) {
+ return (
+
+ );
+}
+
+export { Tabs, TabsContent, TabsList, TabsTrigger };