Editor Tabs
VS Code-style editor tab bar with horizontal scroll, active accent, closable tabs, dirty indicators, and composable primitives.
Default
Basic editor tabs with panel content
Project overview and recent activity.
default-example.tsx
1"use client";23import { useState } from "react";4import {5 EditorTab,6 EditorTabs,7 EditorTabsList,8 EditorTabsPanel,9} from "@/registry/ui";1011const defaultPanels: Record<string, string> = {12 overview: "Project overview and recent activity.",13 analytics: "Traffic, conversion, and retention metrics.",14 settings: "Workspace defaults and notification preferences.",15};1617export function EditorTabsDefaultExample() {18 const [active, setActive] = useState("overview");1920 return (21 <div className="mx-auto w-full max-w-2xl overflow-hidden rounded-lg border border-border">22 <EditorTabs value={active} onValueChange={setActive} variant="bordered">23 <EditorTabsList>24 <EditorTab value="overview">Overview</EditorTab>25 <EditorTab value="analytics">Analytics</EditorTab>26 <EditorTab value="settings">Settings</EditorTab>27 </EditorTabsList>28 <EditorTabsPanel className="p-4">29 <p className="text-sm text-muted-foreground">30 {defaultPanels[active]}31 </p>32 </EditorTabsPanel>33 </EditorTabs>34 </div>35 );36}
Dirty & pinned
Tabs with icons, dirty state, and pinned tab
Active tab: layout.tsx
dirty-example.tsx
1"use client";23import { useState } from "react";4import { FileCode, FileJson, FileText } from "lucide-react";5import {6 EditorTab,7 EditorTabs,8 EditorTabsList,9 EditorTabsPanel,10} from "@/registry/ui";1112export function EditorTabsDirtyExample() {13 const [active, setActive] = useState("layout.tsx");1415 return (16 <div className="mx-auto w-full max-w-2xl overflow-hidden rounded-lg border border-border">17 <EditorTabs value={active} onValueChange={setActive}>18 <EditorTabsList>19 <EditorTab20 value="layout.tsx"21 icon={<FileCode className="text-blue-500" />}22 dirty23 >24 layout.tsx25 </EditorTab>26 <EditorTab27 value="page.tsx"28 icon={<FileCode className="text-blue-500" />}29 >30 page.tsx31 </EditorTab>32 <EditorTab33 value="package.json"34 icon={<FileJson className="text-yellow-600" />}35 pinned36 >37 package.json38 </EditorTab>39 <EditorTab40 value="README.md"41 icon={<FileText className="text-muted-foreground" />}42 >43 README.md44 </EditorTab>45 </EditorTabsList>46 <EditorTabsPanel className="p-4">47 <p className="font-mono text-sm text-muted-foreground">48 Active tab: {active}49 </p>50 </EditorTabsPanel>51 </EditorTabs>52 </div>53 );54}
Closable
Close tabs individually with middle-click support
Active tab: utils.ts
closable-example.tsx
1"use client";23import { FileCode } from "lucide-react";4import {5 EditorTab,6 EditorTabs,7 EditorTabsList,8 EditorTabsPanel,9 useEditorTabs,10} from "@/registry/ui";1112type DemoTab = {13 id: string;14 label: string;15};1617const initialClosableTabs: DemoTab[] = [18 { id: "utils.ts", label: "utils.ts" },19 { id: "constants.ts", label: "constants.ts" },20 { id: "hooks.ts", label: "hooks.ts" },21];2223export function EditorTabsClosableExample() {24 const { tabs, activeId, setActiveId, closeTab } = useEditorTabs<DemoTab>({25 initialTabs: initialClosableTabs,26 initialActiveId: "utils.ts",27 });2829 return (30 <div className="mx-auto w-full max-w-2xl overflow-hidden rounded-lg border border-border">31 <EditorTabs value={activeId} onValueChange={setActiveId} variant="ghost">32 <EditorTabsList>33 {tabs.map((tab) => (34 <EditorTab35 key={tab.id}36 value={tab.id}37 icon={<FileCode className="text-blue-500" />}38 onClose={closeTab}39 >40 {tab.label}41 </EditorTab>42 ))}43 </EditorTabsList>44 <EditorTabsPanel className="p-4">45 {tabs.length === 0 ? (46 <p className="text-sm text-muted-foreground">47 All tabs closed. Re-open files from the tree in the workspace48 demo.49 </p>50 ) : (51 <p className="font-mono text-sm text-muted-foreground">52 Active tab: {activeId}53 </p>54 )}55 </EditorTabsPanel>56 </EditorTabs>57 </div>58 );59}
File explorer workspace
File tree, editor tabs, and file viewer combined
Select a file to view its contents
with-editor-sidebar-example.tsx
1"use client";23import {4 FileTree,5 FileTreeNode,6 FileExplorer,7 FileExplorerContent,8 FileViewer,9 FileContent,10 EditorTabs,11 EditorTabsList,12 EditorTab,13 EditorTabsPanel,14 useEditorTabs,15 EditorSidebar,16 EditorSidebarRail,17 EditorSidebarTrigger,18 EditorSidebarContent,19 EditorSidebarPanel,20 useEditorSidebar,21} from "@/registry/ui";22import { useState, useMemo, type ReactNode } from "react";23import {24 File,25 FileCode,26 FileJson,27 FileText,28 Image as ImageIcon,29 Cog,30 Package,31 Files,32 Search,33 Blocks,34 PanelLeft,35 PanelLeftClose,36} from "lucide-react";37import { Button } from "@/components/ui/button";38import { cn } from "@/lib/utils";3940const fileContents: Record<string, string> = {41 "src/app/layout.tsx": `import type { Metadata } from "next"42import { Inter } from "next/font/google"43import "./globals.css"4445const inter = Inter({ subsets: ["latin"] })4647export const metadata: Metadata = {48 title: "My App",49 description: "A Next.js application",50}5152export default function RootLayout({53 children,54}: {55 children: React.ReactNode56}) {57 return (58 <html lang="en">59 <body className={inter.className}>{children}</body>60 </html>61 )62}`,63 "src/app/page.tsx": `export default function Home() {64 return (65 <main className="flex min-h-screen flex-col items-center justify-center p-24">66 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>67 <p className="mt-4 text-lg text-muted-foreground">68 Get started by editing app/page.tsx69 </p>70 </main>71 )72 }`,73 "src/app/globals.css": `@tailwind base;74 @tailwind components;75 @tailwind utilities;7677 :root {78 --foreground-rgb: 0, 0, 0;79 --background-start-rgb: 214, 219, 220;80 --background-end-rgb: 255, 255, 255;81 }8283 @media (prefers-color-scheme: dark) {84 :root {85 --foreground-rgb: 255, 255, 255;86 --background-start-rgb: 0, 0, 0;87 --background-end-rgb: 0, 0, 0;88 }89 }`,90 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"9192 export async function POST(request: Request) {93 const body = await request.json()94 const { email, password } = body9596 // Validate credentials97 if (!email || !password) {98 return NextResponse.json(99 { error: "Missing credentials" },100 { status: 400 }101 )102 }103104 // TODO: Implement actual authentication105 return NextResponse.json({ success: true })106 }`,107 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"108109 const users = [110 { id: 1, name: "Alice", email: "alice@example.com" },111 { id: 2, name: "Bob", email: "bob@example.com" },112 ]113114 export async function GET() {115 return NextResponse.json(users)116 }117118 export async function POST(request: Request) {119 const body = await request.json()120 const newUser = { id: users.length + 1, ...body }121 users.push(newUser)122 return NextResponse.json(newUser, { status: 201 })123 }`,124 "src/components/ui/button.tsx": `import * as React from "react"125 import { cva, type VariantProps } from "class-variance-authority"126 import { cn } from "@/lib/utils"127128 const buttonVariants = cva(129 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",130 {131 variants: {132 variant: {133 default: "bg-primary text-primary-foreground hover:bg-primary/90",134 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",135 outline: "border border-input hover:bg-accent hover:text-accent-foreground",136 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",137 ghost: "hover:bg-accent hover:text-accent-foreground",138 link: "underline-offset-4 hover:underline text-primary",139 },140 size: {141 default: "h-10 px-4 py-2",142 sm: "h-9 rounded-md px-3",143 lg: "h-11 rounded-md px-8",144 icon: "h-10 w-10",145 },146 },147 defaultVariants: {148 variant: "default",149 size: "default",150 },151 }152 )153154 export interface ButtonProps155 extends React.ButtonHTMLAttributes<HTMLButtonElement>,156 VariantProps<typeof buttonVariants> {}157158 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(159 function Button({ className, variant, size, ...props }, ref) {160 return (161 <button162 className={cn(buttonVariants({ variant, size, className }))}163 ref={ref}164 {...props}165 />166 )167 }168 )169170 export { Button, buttonVariants }`,171 "src/components/ui/card.tsx": `import * as React from "react"172 import { cn } from "@/lib/utils"173174 const Card = React.forwardRef<175 HTMLDivElement,176 React.HTMLAttributes<HTMLDivElement>177 >(function Card({ className, ...props }, ref) {178 return (179 <div180 ref={ref}181 className={cn(182 "rounded-lg border bg-card text-card-foreground shadow-sm",183 className184 )}185 {...props}186 />187 )188 })189190 const CardHeader = React.forwardRef<191 HTMLDivElement,192 React.HTMLAttributes<HTMLDivElement>193 >(function CardHeader({ className, ...props }, ref) {194 return (195 <div196 ref={ref}197 className={cn("flex flex-col space-y-1.5 p-6", className)}198 {...props}199 />200 )201 })202203 const CardTitle = React.forwardRef<204 HTMLParagraphElement,205 React.HTMLAttributes<HTMLHeadingElement>206 >(function CardTitle({ className, ...props }, ref) {207 return (208 <h3209 ref={ref}210 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}211 {...props}212 />213 )214 })215216 export { Card, CardHeader, CardTitle }`,217 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component218 // This is a simplified preview219220 import { cva } from "class-variance-authority"221222 export const fileTreeVariants = cva(223 "font-mono text-sm select-none",224 {225 variants: {226 variant: {227 default: "bg-background text-foreground",228 ghost: "bg-transparent",229 bordered: "bg-background border border-border rounded-lg p-2",230 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",231 },232 },233 }234 )`,235 "src/components/header.tsx": `import Link from "next/link"236 import { Button } from "@/components/ui/button"237238 export function Header() {239 return (240 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">241 <div className="container flex h-14 items-center">242 <Link href="/" className="mr-6 flex items-center space-x-2">243 <span className="font-bold">My App</span>244 </Link>245 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">246 <Link href="/about">About</Link>247 <Link href="/docs">Docs</Link>248 </nav>249 <Button size="sm">Sign In</Button>250 </div>251 </header>252 )253 }`,254 "src/components/footer.tsx": `export function Footer() {255 return (256 <footer className="border-t py-6 md:py-0">257 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">258 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">259 Built with Next.js and Tailwind CSS.260 </p>261 </div>262 </footer>263 )264 }`,265 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"266 import { twMerge } from "tailwind-merge"267268 export function cn(...inputs: ClassValue[]) {269 return twMerge(clsx(inputs))270 }`,271 "src/lib/constants.ts": `export const APP_NAME = "My Application"272 export const APP_VERSION = "1.0.0"273 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"274275 export const ROUTES = {276 home: "/",277 about: "/about",278 dashboard: "/dashboard",279 settings: "/settings",280 } as const`,281 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"282283 interface User {284 id: string285 email: string286 name: string287 }288289 export function useAuth() {290 const [user, setUser] = useState<User | null>(null)291 const [loading, setLoading] = useState(true)292293 useEffect(() => {294 // Check for existing session295 const checkAuth = async () => {296 try {297 const response = await fetch("/api/auth/me")298 if (response.ok) {299 const userData = await response.json()300 setUser(userData)301 }302 } catch (error) {303 console.error("Auth check failed:", error)304 } finally {305 setLoading(false)306 }307 }308 checkAuth()309 }, [])310311 const signOut = useCallback(async () => {312 await fetch("/api/auth/signout", { method: "POST" })313 setUser(null)314 }, [])315316 return { user, loading, signOut }317 }`,318 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"319320 type Theme = "light" | "dark" | "system"321322 export function useTheme() {323 const [theme, setTheme] = useState<Theme>("system")324325 useEffect(() => {326 const stored = localStorage.getItem("theme") as Theme | null327 if (stored) {328 setTheme(stored)329 }330 }, [])331332 useEffect(() => {333 const root = document.documentElement334 root.classList.remove("light", "dark")335336 if (theme === "system") {337 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches338 ? "dark"339 : "light"340 root.classList.add(systemTheme)341 } else {342 root.classList.add(theme)343 }344345 localStorage.setItem("theme", theme)346 }, [theme])347348 return { theme, setTheme }349 }`,350 "package.json": `{351 "name": "my-nextjs-app",352 "version": "0.1.0",353 "private": true,354 "scripts": {355 "dev": "next dev",356 "build": "next build",357 "start": "next start",358 "lint": "next lint"359 },360 "dependencies": {361 "next": "14.2.0",362 "react": "^18",363 "react-dom": "^18",364 "class-variance-authority": "^0.7.0",365 "clsx": "^2.1.0",366 "tailwind-merge": "^2.2.0",367 "lucide-react": "^0.344.0"368 },369 "devDependencies": {370 "typescript": "^5",371 "@types/node": "^20",372 "@types/react": "^18",373 "@types/react-dom": "^18",374 "tailwindcss": "^3.4.1",375 "postcss": "^8"376 }377 }`,378 "tsconfig.json": `{379 "compilerOptions": {380 "lib": ["dom", "dom.iterable", "esnext"],381 "allowJs": true,382 "skipLibCheck": true,383 "strict": true,384 "noEmit": true,385 "esModuleInterop": true,386 "module": "esnext",387 "moduleResolution": "bundler",388 "resolveJsonModule": true,389 "isolatedModules": true,390 "jsx": "preserve",391 "incremental": true,392 "plugins": [{ "name": "next" }],393 "paths": {394 "@/*": ["./*"]395 }396 },397 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],398 "exclude": ["node_modules"]399 }`,400 "next.config.mjs": `/** @type {import('next').NextConfig} */401 const nextConfig = {402 reactStrictMode: true,403 images: {404 domains: [],405 },406 }407408 export default nextConfig`,409 "tailwind.config.ts": `import type { Config } from "tailwindcss"410411 const config: Config = {412 darkMode: ["class"],413 content: [414 "./pages/**/*.{js,ts,jsx,tsx,mdx}",415 "./components/**/*.{js,ts,jsx,tsx,mdx}",416 "./app/**/*.{js,ts,jsx,tsx,mdx}",417 ],418 theme: {419 extend: {420 colors: {421 border: "hsl(var(--border))",422 background: "hsl(var(--background))",423 foreground: "hsl(var(--foreground))",424 },425 },426 },427 plugins: [],428 }429 export default config`,430 ".env.local": `# Environment Variables431 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"432 NEXT_PUBLIC_API_URL="http://localhost:3000/api"433 AUTH_SECRET="your-secret-key-here"`,434 ".gitignore": `# Dependencies435 node_modules436 .pnp437 .pnp.js438439 # Testing440 coverage441442 # Next.js443 .next/444 out/445446 # Production447 build448449 # Misc450 .DS_Store451 *.pem452453 # Debug454 npm-debug.log*455456 # Local env files457 .env*.local458459 # Vercel460 .vercel461462 # TypeScript463 *.tsbuildinfo464 next-env.d.ts`,465 "README.md": `# My Next.js App466467 A modern web application built with Next.js 14, React, and Tailwind CSS.468469 ## Getting Started470471 First, install dependencies:472473 \`\`\`bash474 npm install475 # or476 yarn install477 # or478 pnpm install479 \`\`\`480481 Then, run the development server:482483 \`\`\`bash484 npm run dev485 \`\`\`486487 Open [http://localhost:3000](http://localhost:3000) to see the result.488489 ## Features490491 - Next.js 14 App Router492 - TypeScript493 - Tailwind CSS494 - Component library with CVA variants495 `,496};497498const sampleData: FileTreeNode[] = [499 {500 name: "src",501 type: "folder",502 children: [503 {504 name: "app",505 type: "folder",506 children: [507 { name: "layout.tsx", type: "file" },508 { name: "page.tsx", type: "file" },509 { name: "globals.css", type: "file" },510 {511 name: "api",512 type: "folder",513 children: [514 {515 name: "auth",516 type: "folder",517 children: [{ name: "route.ts", type: "file" }],518 },519 {520 name: "users",521 type: "folder",522 children: [{ name: "route.ts", type: "file" }],523 },524 ],525 },526 ],527 },528 {529 name: "components",530 type: "folder",531 children: [532 {533 name: "ui",534 type: "folder",535 children: [536 { name: "button.tsx", type: "file" },537 { name: "card.tsx", type: "file" },538 { name: "file-tree.tsx", type: "file" },539 ],540 },541 { name: "header.tsx", type: "file" },542 { name: "footer.tsx", type: "file" },543 ],544 },545 {546 name: "lib",547 type: "folder",548 children: [549 { name: "utils.ts", type: "file" },550 { name: "constants.ts", type: "file" },551 ],552 },553 {554 name: "hooks",555 type: "folder",556 children: [557 { name: "use-auth.ts", type: "file" },558 { name: "use-theme.ts", type: "file" },559 ],560 },561 ],562 },563 {564 name: "public",565 type: "folder",566 children: [567 { name: "favicon.ico", type: "file" },568 { name: "logo.svg", type: "file" },569 {570 name: "images",571 type: "folder",572 children: [573 { name: "hero.png", type: "file" },574 { name: "avatar.jpg", type: "file" },575 ],576 },577 ],578 },579 { name: "package.json", type: "file" },580 { name: "tsconfig.json", type: "file" },581 { name: "next.config.mjs", type: "file" },582 { name: "tailwind.config.ts", type: "file" },583 { name: ".env.local", type: "file" },584 { name: ".gitignore", type: "file" },585 { name: "README.md", type: "file" },586];587588const imagePreviewUrls: Record<string, string> = {589 "public/images/hero.png":590 "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1200&auto=format&fit=crop",591 "public/images/avatar.jpg":592 "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=400&auto=format&fit=crop",593 "public/logo.svg": "/file.svg",594 "public/favicon.ico": "https://picsum.photos/seed/favicon/64/64",595};596597type OpenFileTab = FileContent & { id: string };598599function getTabFileIcon(filename: string) {600 const iconClass = "shrink-0";601 const ext = filename.split(".").pop()?.toLowerCase();602603 const iconMap: Record<string, ReactNode> = {604 js: <FileCode className={cn(iconClass, "text-yellow-500")} size={14} />,605 jsx: <FileCode className={cn(iconClass, "text-yellow-500")} size={14} />,606 ts: <FileCode className={cn(iconClass, "text-blue-500")} size={14} />,607 tsx: <FileCode className={cn(iconClass, "text-blue-500")} size={14} />,608 json: <FileJson className={cn(iconClass, "text-yellow-600")} size={14} />,609 md: (610 <FileText className={cn(iconClass, "text-muted-foreground")} size={14} />611 ),612 css: <FileCode className={cn(iconClass, "text-blue-400")} size={14} />,613 svg: <ImageIcon className={cn(iconClass, "text-orange-400")} size={14} />,614 png: <ImageIcon className={cn(iconClass, "text-green-500")} size={14} />,615 jpg: <ImageIcon className={cn(iconClass, "text-green-500")} size={14} />,616 };617618 if (filename === "package.json") {619 return <Package className={cn(iconClass, "text-green-600")} size={14} />;620 }621 if (filename.startsWith(".env")) {622 return <Cog className={cn(iconClass, "text-yellow-600")} size={14} />;623 }624 if (filename.includes("config")) {625 return <Cog className={cn(iconClass, "text-muted-foreground")} size={14} />;626 }627628 return (629 iconMap[ext || ""] || (630 <File className={cn(iconClass, "text-muted-foreground")} size={14} />631 )632 );633}634635function buildFileContent(node: FileTreeNode, path: string): FileContent {636 const content = fileContents[path];637 const previewUrl = imagePreviewUrls[path];638639 if (content) {640 return { path, name: node.name, content, previewUrl };641 }642643 return {644 path,645 name: node.name,646 content: previewUrl647 ? ""648 : `// Content for ${node.name} not available in demo`,649 previewUrl,650 };651}652653type FlatFileEntry = {654 path: string;655 name: string;656 node: FileTreeNode;657};658659function flattenFilePaths(nodes: FileTreeNode[], prefix = ""): FlatFileEntry[] {660 const entries: FlatFileEntry[] = [];661662 for (const node of nodes) {663 const path = prefix ? `${prefix}/${node.name}` : node.name;664 if (node.type === "file") {665 entries.push({ path, name: node.name, node });666 } else if (node.children?.length) {667 entries.push(...flattenFilePaths(node.children, path));668 }669 }670671 return entries;672}673674const mockExtensions = [675 {676 id: "eslint",677 name: "ESLint",678 publisher: "Microsoft",679 description: "Integrates ESLint into your workspace.",680 },681 {682 id: "prettier",683 name: "Prettier",684 publisher: "Prettier",685 description: "Code formatter using Prettier.",686 },687 {688 id: "tailwind",689 name: "Tailwind CSS IntelliSense",690 publisher: "Tailwind Labs",691 description: "Intelligent Tailwind class completion and linting.",692 },693 {694 id: "gitlens",695 name: "GitLens",696 publisher: "GitKraken",697 description: "Supercharge Git capabilities built into your editor.",698 },699];700701export function FileViewerWithEditorSidebar() {702 const { activeId: sidebarView, setActiveId: setSidebarView } =703 useEditorSidebar("explorer");704 const { tabs, activeId, setActiveId, openTab, closeTab, focusTab } =705 useEditorTabs<OpenFileTab>();706 const [searchQuery, setSearchQuery] = useState("");707 const [explorerPanelOpen, setExplorerPanelOpen] = useState(true);708709 const activeFile = useMemo(710 () => tabs.find((tab) => tab.id === activeId) ?? null,711 [tabs, activeId],712 );713714 const allFiles = useMemo(() => flattenFilePaths(sampleData), []);715716 const searchResults = useMemo(() => {717 const query = searchQuery.trim().toLowerCase();718 if (!query) return allFiles.slice(0, 8);719 return allFiles.filter(720 (file) =>721 file.path.toLowerCase().includes(query) ||722 file.name.toLowerCase().includes(query),723 );724 }, [allFiles, searchQuery]);725726 const handleSelect = (node: FileTreeNode, path: string) => {727 if (node.type !== "file") return;728729 const file = buildFileContent(node, path);730 const tab: OpenFileTab = { ...file, id: path };731732 if (tabs.some((item) => item.id === path)) {733 focusTab(path);734 return;735 }736737 openTab(tab);738 };739740 const handleSearchSelect = (entry: FlatFileEntry) => {741 handleSelect(entry.node, entry.path);742 setSidebarView("explorer");743 };744745 return (746 <div className="w-full">747 <FileExplorer variant="elevated" rounded="lg" className="h-[600px]">748 <EditorSidebar749 value={sidebarView}750 onValueChange={setSidebarView}751 variant="ghost"752 className="h-full"753 >754 <EditorSidebarRail className="h-full min-h-0 self-stretch">755 <div className="flex min-h-0 flex-1 flex-col gap-1">756 <EditorSidebarTrigger757 value="explorer"758 icon={<Files />}759 label="Explorer"760 />761 <EditorSidebarTrigger762 value="search"763 icon={<Search />}764 label="Search"765 />766 <EditorSidebarTrigger767 value="extensions"768 icon={<Blocks />}769 label="Extensions"770 badge771 />772 </div>773 <Button774 type="button"775 variant="ghost"776 size="icon"777 className="mt-1 size-9 shrink-0 text-muted-foreground"778 onClick={() => setExplorerPanelOpen((open) => !open)}779 aria-label={780 explorerPanelOpen781 ? "Collapse explorer panel"782 : "Expand explorer panel"783 }784 aria-expanded={explorerPanelOpen}785 title={explorerPanelOpen ? "Collapse panel" : "Expand panel"}786 >787 {explorerPanelOpen ? (788 <PanelLeftClose className="size-4" />789 ) : (790 <PanelLeft className="size-4" />791 )}792 </Button>793 </EditorSidebarRail>794 <div795 className={cn(796 "flex h-full min-h-0 shrink-0 flex-col overflow-hidden transition-[width] duration-200 ease-linear",797 explorerPanelOpen ? "w-[260px]" : "w-0",798 )}799 aria-hidden={!explorerPanelOpen}800 >801 <EditorSidebarContent802 width="260px"803 className="h-full min-h-0 min-w-[260px] flex-none"804 >805 <EditorSidebarPanel value="explorer" title="Explorer">806 <FileTree807 data={sampleData}808 variant="ghost"809 size="default"810 onSelect={handleSelect}811 selectedPath={activeId}812 />813 </EditorSidebarPanel>814 <EditorSidebarPanel value="search" title="Search">815 <div className="flex flex-col gap-3">816 <input817 type="search"818 value={searchQuery}819 onChange={(event) => setSearchQuery(event.target.value)}820 placeholder="Search files..."821 className="h-8 w-full rounded-md border border-border bg-background px-2.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/60"822 />823 <ul className="flex flex-col gap-0.5">824 {searchResults.length === 0 ? (825 <li className="px-1 py-2 text-xs text-muted-foreground">826 No files match your search.827 </li>828 ) : (829 searchResults.map((file) => (830 <li key={file.path}>831 <button832 type="button"833 onClick={() => handleSearchSelect(file)}834 className="flex w-full items-center gap-2 rounded-sm px-1.5 py-1 text-left text-xs hover:bg-accent"835 >836 {getTabFileIcon(file.name)}837 <span className="truncate font-mono">838 {file.path}839 </span>840 </button>841 </li>842 ))843 )}844 </ul>845 </div>846 </EditorSidebarPanel>847 <EditorSidebarPanel value="extensions" title="Extensions">848 <ul className="flex flex-col gap-2">849 {mockExtensions.map((extension) => (850 <li851 key={extension.id}852 className="rounded-md border border-border p-2.5"853 >854 <p className="text-sm font-medium">{extension.name}</p>855 <p className="text-xs text-muted-foreground">856 {extension.publisher}857 </p>858 <p className="mt-1 text-xs text-muted-foreground">859 {extension.description}860 </p>861 </li>862 ))}863 </ul>864 </EditorSidebarPanel>865 </EditorSidebarContent>866 </div>867 </EditorSidebar>868 <FileExplorerContent className="flex flex-col">869 <EditorTabs870 value={activeId}871 onValueChange={setActiveId}872 variant="ghost"873 className="h-full"874 >875 {tabs.length > 0 ? (876 <EditorTabsList>877 {tabs.map((tab) => (878 <EditorTab879 key={tab.id}880 value={tab.id}881 icon={getTabFileIcon(tab.name)}882 onClose={closeTab}883 >884 {tab.name}885 </EditorTab>886 ))}887 </EditorTabsList>888 ) : null}889 <EditorTabsPanel>890 <FileViewer891 file={activeFile}892 variant="ghost"893 size="default"894 rounded="none"895 maxHeight="100%"896 showHeader={false}897 className="h-full"898 />899 </EditorTabsPanel>900 </EditorTabs>901 </FileExplorerContent>902 </FileExplorer>903 </div>904 );905}
Installation & source
Install via the shadcn CLI or copy the registry files manually.
bash
npx shadcn@latest add @tt-ui/editor-tabs
Props
| Name | Type | Default | Description |
|---|---|---|---|
| value / onValueChange | string / (value: string) => void | uncontrolled if omitted | Controlled active tab id |
| defaultValue | string | undefined | Initial active tab in uncontrolled mode |
| variant (CVA) | "default" | "ghost" | "bordered" | "default" | Visual style for the tab bar container |
| size (CVA) | "sm" | "default" | "lg" | "default" | Tab bar and trigger sizing |
| EditorTab.icon | ReactNode | undefined | Optional leading icon slot |
| EditorTab.dirty | boolean | false | Shows unsaved-changes indicator dot |
| EditorTab.pinned | boolean | false | Pinned tabs hide the close button |
| EditorTab.onClose | (value: string) => void | undefined | When provided, renders close button and enables middle-click close |
| useEditorTabs | hook | undefined | Optional helper for open/close/focus tab state |