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
Overview
Analytics
Settings
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
layout.tsx
page.tsx
package.json
README.md
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
utils.ts
constants.ts
hooks.ts
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 ({ className, variant, size, ...props }, ref) => {160 return (161 <button162 className={cn(buttonVariants({ variant, size, className }))}163 ref={ref}164 {...props}165 />166 )167 }168 )169 Button.displayName = "Button"170171 export { Button, buttonVariants }`,172 "src/components/ui/card.tsx": `import * as React from "react"173 import { cn } from "@/lib/utils"174175 const Card = React.forwardRef<176 HTMLDivElement,177 React.HTMLAttributes<HTMLDivElement>178 >(({ className, ...props }, ref) => (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 Card.displayName = "Card"189190 const CardHeader = React.forwardRef<191 HTMLDivElement,192 React.HTMLAttributes<HTMLDivElement>193 >(({ className, ...props }, ref) => (194 <div195 ref={ref}196 className={cn("flex flex-col space-y-1.5 p-6", className)}197 {...props}198 />199 ))200 CardHeader.displayName = "CardHeader"201202 const CardTitle = React.forwardRef<203 HTMLParagraphElement,204 React.HTMLAttributes<HTMLHeadingElement>205 >(({ className, ...props }, ref) => (206 <h3207 ref={ref}208 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}209 {...props}210 />211 ))212 CardTitle.displayName = "CardTitle"213214 export { Card, CardHeader, CardTitle }`,215 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component216 // This is a simplified preview217218 import { cva } from "class-variance-authority"219220 export const fileTreeVariants = cva(221 "font-mono text-sm select-none",222 {223 variants: {224 variant: {225 default: "bg-background text-foreground",226 ghost: "bg-transparent",227 bordered: "bg-background border border-border rounded-lg p-2",228 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",229 },230 },231 }232 )`,233 "src/components/header.tsx": `import Link from "next/link"234 import { Button } from "@/components/ui/button"235236 export function Header() {237 return (238 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">239 <div className="container flex h-14 items-center">240 <Link href="/" className="mr-6 flex items-center space-x-2">241 <span className="font-bold">My App</span>242 </Link>243 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">244 <Link href="/about">About</Link>245 <Link href="/docs">Docs</Link>246 </nav>247 <Button size="sm">Sign In</Button>248 </div>249 </header>250 )251 }`,252 "src/components/footer.tsx": `export function Footer() {253 return (254 <footer className="border-t py-6 md:py-0">255 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">256 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">257 Built with Next.js and Tailwind CSS.258 </p>259 </div>260 </footer>261 )262 }`,263 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"264 import { twMerge } from "tailwind-merge"265266 export function cn(...inputs: ClassValue[]) {267 return twMerge(clsx(inputs))268 }`,269 "src/lib/constants.ts": `export const APP_NAME = "My Application"270 export const APP_VERSION = "1.0.0"271 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"272273 export const ROUTES = {274 home: "/",275 about: "/about",276 dashboard: "/dashboard",277 settings: "/settings",278 } as const`,279 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"280281 interface User {282 id: string283 email: string284 name: string285 }286287 export function useAuth() {288 const [user, setUser] = useState<User | null>(null)289 const [loading, setLoading] = useState(true)290291 useEffect(() => {292 // Check for existing session293 const checkAuth = async () => {294 try {295 const response = await fetch("/api/auth/me")296 if (response.ok) {297 const userData = await response.json()298 setUser(userData)299 }300 } catch (error) {301 console.error("Auth check failed:", error)302 } finally {303 setLoading(false)304 }305 }306 checkAuth()307 }, [])308309 const signOut = useCallback(async () => {310 await fetch("/api/auth/signout", { method: "POST" })311 setUser(null)312 }, [])313314 return { user, loading, signOut }315 }`,316 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"317318 type Theme = "light" | "dark" | "system"319320 export function useTheme() {321 const [theme, setTheme] = useState<Theme>("system")322323 useEffect(() => {324 const stored = localStorage.getItem("theme") as Theme | null325 if (stored) {326 setTheme(stored)327 }328 }, [])329330 useEffect(() => {331 const root = document.documentElement332 root.classList.remove("light", "dark")333334 if (theme === "system") {335 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches336 ? "dark"337 : "light"338 root.classList.add(systemTheme)339 } else {340 root.classList.add(theme)341 }342343 localStorage.setItem("theme", theme)344 }, [theme])345346 return { theme, setTheme }347 }`,348 "package.json": `{349 "name": "my-nextjs-app",350 "version": "0.1.0",351 "private": true,352 "scripts": {353 "dev": "next dev",354 "build": "next build",355 "start": "next start",356 "lint": "next lint"357 },358 "dependencies": {359 "next": "14.2.0",360 "react": "^18",361 "react-dom": "^18",362 "class-variance-authority": "^0.7.0",363 "clsx": "^2.1.0",364 "tailwind-merge": "^2.2.0",365 "lucide-react": "^0.344.0"366 },367 "devDependencies": {368 "typescript": "^5",369 "@types/node": "^20",370 "@types/react": "^18",371 "@types/react-dom": "^18",372 "tailwindcss": "^3.4.1",373 "postcss": "^8"374 }375 }`,376 "tsconfig.json": `{377 "compilerOptions": {378 "lib": ["dom", "dom.iterable", "esnext"],379 "allowJs": true,380 "skipLibCheck": true,381 "strict": true,382 "noEmit": true,383 "esModuleInterop": true,384 "module": "esnext",385 "moduleResolution": "bundler",386 "resolveJsonModule": true,387 "isolatedModules": true,388 "jsx": "preserve",389 "incremental": true,390 "plugins": [{ "name": "next" }],391 "paths": {392 "@/*": ["./*"]393 }394 },395 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],396 "exclude": ["node_modules"]397 }`,398 "next.config.mjs": `/** @type {import('next').NextConfig} */399 const nextConfig = {400 reactStrictMode: true,401 images: {402 domains: [],403 },404 }405406 export default nextConfig`,407 "tailwind.config.ts": `import type { Config } from "tailwindcss"408409 const config: Config = {410 darkMode: ["class"],411 content: [412 "./pages/**/*.{js,ts,jsx,tsx,mdx}",413 "./components/**/*.{js,ts,jsx,tsx,mdx}",414 "./app/**/*.{js,ts,jsx,tsx,mdx}",415 ],416 theme: {417 extend: {418 colors: {419 border: "hsl(var(--border))",420 background: "hsl(var(--background))",421 foreground: "hsl(var(--foreground))",422 },423 },424 },425 plugins: [],426 }427 export default config`,428 ".env.local": `# Environment Variables429 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"430 NEXT_PUBLIC_API_URL="http://localhost:3000/api"431 AUTH_SECRET="your-secret-key-here"`,432 ".gitignore": `# Dependencies433 node_modules434 .pnp435 .pnp.js436437 # Testing438 coverage439440 # Next.js441 .next/442 out/443444 # Production445 build446447 # Misc448 .DS_Store449 *.pem450451 # Debug452 npm-debug.log*453454 # Local env files455 .env*.local456457 # Vercel458 .vercel459460 # TypeScript461 *.tsbuildinfo462 next-env.d.ts`,463 "README.md": `# My Next.js App464465 A modern web application built with Next.js 14, React, and Tailwind CSS.466467 ## Getting Started468469 First, install dependencies:470471 \`\`\`bash472 npm install473 # or474 yarn install475 # or476 pnpm install477 \`\`\`478479 Then, run the development server:480481 \`\`\`bash482 npm run dev483 \`\`\`484485 Open [http://localhost:3000](http://localhost:3000) to see the result.486487 ## Features488489 - Next.js 14 App Router490 - TypeScript491 - Tailwind CSS492 - Component library with CVA variants493 `,494};495496const sampleData: FileTreeNode[] = [497 {498 name: "src",499 type: "folder",500 children: [501 {502 name: "app",503 type: "folder",504 children: [505 { name: "layout.tsx", type: "file" },506 { name: "page.tsx", type: "file" },507 { name: "globals.css", type: "file" },508 {509 name: "api",510 type: "folder",511 children: [512 {513 name: "auth",514 type: "folder",515 children: [{ name: "route.ts", type: "file" }],516 },517 {518 name: "users",519 type: "folder",520 children: [{ name: "route.ts", type: "file" }],521 },522 ],523 },524 ],525 },526 {527 name: "components",528 type: "folder",529 children: [530 {531 name: "ui",532 type: "folder",533 children: [534 { name: "button.tsx", type: "file" },535 { name: "card.tsx", type: "file" },536 { name: "file-tree.tsx", type: "file" },537 ],538 },539 { name: "header.tsx", type: "file" },540 { name: "footer.tsx", type: "file" },541 ],542 },543 {544 name: "lib",545 type: "folder",546 children: [547 { name: "utils.ts", type: "file" },548 { name: "constants.ts", type: "file" },549 ],550 },551 {552 name: "hooks",553 type: "folder",554 children: [555 { name: "use-auth.ts", type: "file" },556 { name: "use-theme.ts", type: "file" },557 ],558 },559 ],560 },561 {562 name: "public",563 type: "folder",564 children: [565 { name: "favicon.ico", type: "file" },566 { name: "logo.svg", type: "file" },567 {568 name: "images",569 type: "folder",570 children: [571 { name: "hero.png", type: "file" },572 { name: "avatar.jpg", type: "file" },573 ],574 },575 ],576 },577 { name: "package.json", type: "file" },578 { name: "tsconfig.json", type: "file" },579 { name: "next.config.mjs", type: "file" },580 { name: "tailwind.config.ts", type: "file" },581 { name: ".env.local", type: "file" },582 { name: ".gitignore", type: "file" },583 { name: "README.md", type: "file" },584];585586const imagePreviewUrls: Record<string, string> = {587 "public/images/hero.png":588 "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1200&auto=format&fit=crop",589 "public/images/avatar.jpg":590 "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=400&auto=format&fit=crop",591 "public/logo.svg": "/file.svg",592 "public/favicon.ico": "https://picsum.photos/seed/favicon/64/64",593};594595type OpenFileTab = FileContent & { id: string };596597function getTabFileIcon(filename: string) {598 const iconClass = "shrink-0";599 const ext = filename.split(".").pop()?.toLowerCase();600601 const iconMap: Record<string, ReactNode> = {602 js: <FileCode className={cn(iconClass, "text-yellow-500")} size={14} />,603 jsx: <FileCode className={cn(iconClass, "text-yellow-500")} size={14} />,604 ts: <FileCode className={cn(iconClass, "text-blue-500")} size={14} />,605 tsx: <FileCode className={cn(iconClass, "text-blue-500")} size={14} />,606 json: <FileJson className={cn(iconClass, "text-yellow-600")} size={14} />,607 md: (608 <FileText className={cn(iconClass, "text-muted-foreground")} size={14} />609 ),610 css: <FileCode className={cn(iconClass, "text-blue-400")} size={14} />,611 svg: <ImageIcon className={cn(iconClass, "text-orange-400")} size={14} />,612 png: <ImageIcon className={cn(iconClass, "text-green-500")} size={14} />,613 jpg: <ImageIcon className={cn(iconClass, "text-green-500")} size={14} />,614 };615616 if (filename === "package.json") {617 return <Package className={cn(iconClass, "text-green-600")} size={14} />;618 }619 if (filename.startsWith(".env")) {620 return <Cog className={cn(iconClass, "text-yellow-600")} size={14} />;621 }622 if (filename.includes("config")) {623 return <Cog className={cn(iconClass, "text-muted-foreground")} size={14} />;624 }625626 return (627 iconMap[ext || ""] || (628 <File className={cn(iconClass, "text-muted-foreground")} size={14} />629 )630 );631}632633function buildFileContent(node: FileTreeNode, path: string): FileContent {634 const content = fileContents[path];635 const previewUrl = imagePreviewUrls[path];636637 if (content) {638 return { path, name: node.name, content, previewUrl };639 }640641 return {642 path,643 name: node.name,644 content: previewUrl645 ? ""646 : `// Content for ${node.name} not available in demo`,647 previewUrl,648 };649}650651type FlatFileEntry = {652 path: string;653 name: string;654 node: FileTreeNode;655};656657function flattenFilePaths(nodes: FileTreeNode[], prefix = ""): FlatFileEntry[] {658 const entries: FlatFileEntry[] = [];659660 for (const node of nodes) {661 const path = prefix ? `${prefix}/${node.name}` : node.name;662 if (node.type === "file") {663 entries.push({ path, name: node.name, node });664 } else if (node.children?.length) {665 entries.push(...flattenFilePaths(node.children, path));666 }667 }668669 return entries;670}671672const mockExtensions = [673 {674 id: "eslint",675 name: "ESLint",676 publisher: "Microsoft",677 description: "Integrates ESLint into your workspace.",678 },679 {680 id: "prettier",681 name: "Prettier",682 publisher: "Prettier",683 description: "Code formatter using Prettier.",684 },685 {686 id: "tailwind",687 name: "Tailwind CSS IntelliSense",688 publisher: "Tailwind Labs",689 description: "Intelligent Tailwind class completion and linting.",690 },691 {692 id: "gitlens",693 name: "GitLens",694 publisher: "GitKraken",695 description: "Supercharge Git capabilities built into your editor.",696 },697];698699export function FileViewerWithEditorSidebar() {700 const { activeId: sidebarView, setActiveId: setSidebarView } =701 useEditorSidebar("explorer");702 const { tabs, activeId, setActiveId, openTab, closeTab, focusTab } =703 useEditorTabs<OpenFileTab>();704 const [searchQuery, setSearchQuery] = useState("");705 const [explorerPanelOpen, setExplorerPanelOpen] = useState(true);706707 const activeFile = useMemo(708 () => tabs.find((tab) => tab.id === activeId) ?? null,709 [tabs, activeId],710 );711712 const allFiles = useMemo(() => flattenFilePaths(sampleData), []);713714 const searchResults = useMemo(() => {715 const query = searchQuery.trim().toLowerCase();716 if (!query) return allFiles.slice(0, 8);717 return allFiles.filter(718 (file) =>719 file.path.toLowerCase().includes(query) ||720 file.name.toLowerCase().includes(query),721 );722 }, [allFiles, searchQuery]);723724 const handleSelect = (node: FileTreeNode, path: string) => {725 if (node.type !== "file") return;726727 const file = buildFileContent(node, path);728 const tab: OpenFileTab = { ...file, id: path };729730 if (tabs.some((item) => item.id === path)) {731 focusTab(path);732 return;733 }734735 openTab(tab);736 };737738 const handleSearchSelect = (entry: FlatFileEntry) => {739 handleSelect(entry.node, entry.path);740 setSidebarView("explorer");741 };742743 return (744 <div className="w-full">745 <FileExplorer variant="elevated" rounded="lg" className="h-[600px]">746 <EditorSidebar747 value={sidebarView}748 onValueChange={setSidebarView}749 variant="ghost"750 className="h-full"751 >752 <EditorSidebarRail className="h-full min-h-0 self-stretch">753 <div className="flex min-h-0 flex-1 flex-col gap-1">754 <EditorSidebarTrigger755 value="explorer"756 icon={<Files />}757 label="Explorer"758 />759 <EditorSidebarTrigger760 value="search"761 icon={<Search />}762 label="Search"763 />764 <EditorSidebarTrigger765 value="extensions"766 icon={<Blocks />}767 label="Extensions"768 badge769 />770 </div>771 <Button772 type="button"773 variant="ghost"774 size="icon"775 className="mt-1 size-9 shrink-0 text-muted-foreground"776 onClick={() => setExplorerPanelOpen((open) => !open)}777 aria-label={778 explorerPanelOpen779 ? "Collapse explorer panel"780 : "Expand explorer panel"781 }782 aria-expanded={explorerPanelOpen}783 title={explorerPanelOpen ? "Collapse panel" : "Expand panel"}784 >785 {explorerPanelOpen ? (786 <PanelLeftClose className="size-4" />787 ) : (788 <PanelLeft className="size-4" />789 )}790 </Button>791 </EditorSidebarRail>792 <div793 className={cn(794 "flex h-full min-h-0 shrink-0 flex-col overflow-hidden transition-[width] duration-200 ease-linear",795 explorerPanelOpen ? "w-[260px]" : "w-0",796 )}797 aria-hidden={!explorerPanelOpen}798 >799 <EditorSidebarContent800 width="260px"801 className="h-full min-h-0 min-w-[260px] flex-none"802 >803 <EditorSidebarPanel value="explorer" title="Explorer">804 <FileTree805 data={sampleData}806 variant="ghost"807 size="default"808 onSelect={handleSelect}809 selectedPath={activeId}810 />811 </EditorSidebarPanel>812 <EditorSidebarPanel value="search" title="Search">813 <div className="flex flex-col gap-3">814 <input815 type="search"816 value={searchQuery}817 onChange={(event) => setSearchQuery(event.target.value)}818 placeholder="Search files..."819 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"820 />821 <ul className="flex flex-col gap-0.5">822 {searchResults.length === 0 ? (823 <li className="px-1 py-2 text-xs text-muted-foreground">824 No files match your search.825 </li>826 ) : (827 searchResults.map((file) => (828 <li key={file.path}>829 <button830 type="button"831 onClick={() => handleSearchSelect(file)}832 className="flex w-full items-center gap-2 rounded-sm px-1.5 py-1 text-left text-xs hover:bg-accent"833 >834 {getTabFileIcon(file.name)}835 <span className="truncate font-mono">836 {file.path}837 </span>838 </button>839 </li>840 ))841 )}842 </ul>843 </div>844 </EditorSidebarPanel>845 <EditorSidebarPanel value="extensions" title="Extensions">846 <ul className="flex flex-col gap-2">847 {mockExtensions.map((extension) => (848 <li849 key={extension.id}850 className="rounded-md border border-border p-2.5"851 >852 <p className="text-sm font-medium">{extension.name}</p>853 <p className="text-xs text-muted-foreground">854 {extension.publisher}855 </p>856 <p className="mt-1 text-xs text-muted-foreground">857 {extension.description}858 </p>859 </li>860 ))}861 </ul>862 </EditorSidebarPanel>863 </EditorSidebarContent>864 </div>865 </EditorSidebar>866 <FileExplorerContent className="flex flex-col">867 <EditorTabs868 value={activeId}869 onValueChange={setActiveId}870 variant="ghost"871 className="h-full"872 >873 {tabs.length > 0 ? (874 <EditorTabsList>875 {tabs.map((tab) => (876 <EditorTab877 key={tab.id}878 value={tab.id}879 icon={getTabFileIcon(tab.name)}880 onClose={closeTab}881 >882 {tab.name}883 </EditorTab>884 ))}885 </EditorTabsList>886 ) : null}887 <EditorTabsPanel>888 <FileViewer889 file={activeFile}890 variant="ghost"891 size="default"892 rounded="none"893 maxHeight="100%"894 showHeader={false}895 className="h-full"896 />897 </EditorTabsPanel>898 </EditorTabs>899 </FileExplorerContent>900 </FileExplorer>901 </div>902 );903}
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 |