File Viewer
File viewer with optional FileExplorer layout (tree sidebar + content). FileExplorer supports a collapsible sidebar when paired with FileTree.
Default
File tree and viewer with collapsible explorer sidebar
Select a file to view its contents
default-example.tsx
1"use client";23import {4 FileTree,5 FileTreeNode,6 FileExplorer,7 FileExplorerContent,8 FileExplorerSidebar,9 FileExplorerSidebarToggle,10 useFileExplorer,11 FileViewer,12 FileContent,13} from "@/registry/ui";14import { useState } from "react";1516function FileExplorerSidebarToggleWhen({17 when,18 className,19}: {20 when: "open" | "closed";21 className?: string;22}) {23 const { collapsible, sidebarOpen } = useFileExplorer();24 if (!collapsible) return null;25 if (when === "open" && !sidebarOpen) return null;26 if (when === "closed" && sidebarOpen) return null;27 return <FileExplorerSidebarToggle className={className} />;28}2930const fileContents: Record<string, string> = {31 "src/app/layout.tsx": `import type { Metadata } from "next"32import { Inter } from "next/font/google"33import "./globals.css"3435const inter = Inter({ subsets: ["latin"] })3637export const metadata: Metadata = {38 title: "My App",39 description: "A Next.js application",40}4142export default function RootLayout({43 children,44}: {45 children: React.ReactNode46}) {47 return (48 <html lang="en">49 <body className={inter.className}>{children}</body>50 </html>51 )52}`,53 "src/app/page.tsx": `export default function Home() {54 return (55 <main className="flex min-h-screen flex-col items-center justify-center p-24">56 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>57 <p className="mt-4 text-lg text-muted-foreground">58 Get started by editing app/page.tsx59 </p>60 </main>61 )62 }`,63 "src/app/globals.css": `@tailwind base;64 @tailwind components;65 @tailwind utilities;6667 :root {68 --foreground-rgb: 0, 0, 0;69 --background-start-rgb: 214, 219, 220;70 --background-end-rgb: 255, 255, 255;71 }7273 @media (prefers-color-scheme: dark) {74 :root {75 --foreground-rgb: 255, 255, 255;76 --background-start-rgb: 0, 0, 0;77 --background-end-rgb: 0, 0, 0;78 }79 }`,80 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"8182 export async function POST(request: Request) {83 const body = await request.json()84 const { email, password } = body8586 // Validate credentials87 if (!email || !password) {88 return NextResponse.json(89 { error: "Missing credentials" },90 { status: 400 }91 )92 }9394 // TODO: Implement actual authentication95 return NextResponse.json({ success: true })96 }`,97 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"9899 const users = [100 { id: 1, name: "Alice", email: "alice@example.com" },101 { id: 2, name: "Bob", email: "bob@example.com" },102 ]103104 export async function GET() {105 return NextResponse.json(users)106 }107108 export async function POST(request: Request) {109 const body = await request.json()110 const newUser = { id: users.length + 1, ...body }111 users.push(newUser)112 return NextResponse.json(newUser, { status: 201 })113 }`,114 "src/components/ui/button.tsx": `import * as React from "react"115 import { cva, type VariantProps } from "class-variance-authority"116 import { cn } from "@/lib/utils"117118 const buttonVariants = cva(119 "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",120 {121 variants: {122 variant: {123 default: "bg-primary text-primary-foreground hover:bg-primary/90",124 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",125 outline: "border border-input hover:bg-accent hover:text-accent-foreground",126 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",127 ghost: "hover:bg-accent hover:text-accent-foreground",128 link: "underline-offset-4 hover:underline text-primary",129 },130 size: {131 default: "h-10 px-4 py-2",132 sm: "h-9 rounded-md px-3",133 lg: "h-11 rounded-md px-8",134 icon: "h-10 w-10",135 },136 },137 defaultVariants: {138 variant: "default",139 size: "default",140 },141 }142 )143144 export interface ButtonProps145 extends React.ButtonHTMLAttributes<HTMLButtonElement>,146 VariantProps<typeof buttonVariants> {}147148 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(149 ({ className, variant, size, ...props }, ref) => {150 return (151 <button152 className={cn(buttonVariants({ variant, size, className }))}153 ref={ref}154 {...props}155 />156 )157 }158 )159 Button.displayName = "Button"160161 export { Button, buttonVariants }`,162 "src/components/ui/card.tsx": `import * as React from "react"163 import { cn } from "@/lib/utils"164165 const Card = React.forwardRef<166 HTMLDivElement,167 React.HTMLAttributes<HTMLDivElement>168 >(({ className, ...props }, ref) => (169 <div170 ref={ref}171 className={cn(172 "rounded-lg border bg-card text-card-foreground shadow-sm",173 className174 )}175 {...props}176 />177 ))178 Card.displayName = "Card"179180 const CardHeader = React.forwardRef<181 HTMLDivElement,182 React.HTMLAttributes<HTMLDivElement>183 >(({ className, ...props }, ref) => (184 <div185 ref={ref}186 className={cn("flex flex-col space-y-1.5 p-6", className)}187 {...props}188 />189 ))190 CardHeader.displayName = "CardHeader"191192 const CardTitle = React.forwardRef<193 HTMLParagraphElement,194 React.HTMLAttributes<HTMLHeadingElement>195 >(({ className, ...props }, ref) => (196 <h3197 ref={ref}198 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}199 {...props}200 />201 ))202 CardTitle.displayName = "CardTitle"203204 export { Card, CardHeader, CardTitle }`,205 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component206 // This is a simplified preview207208 import { cva } from "class-variance-authority"209210 export const fileTreeVariants = cva(211 "font-mono text-sm select-none",212 {213 variants: {214 variant: {215 default: "bg-background text-foreground",216 ghost: "bg-transparent",217 bordered: "bg-background border border-border rounded-lg p-2",218 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",219 },220 },221 }222 )`,223 "src/components/header.tsx": `import Link from "next/link"224 import { Button } from "@/components/ui/button"225226 export function Header() {227 return (228 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">229 <div className="container flex h-14 items-center">230 <Link href="/" className="mr-6 flex items-center space-x-2">231 <span className="font-bold">My App</span>232 </Link>233 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">234 <Link href="/about">About</Link>235 <Link href="/docs">Docs</Link>236 </nav>237 <Button size="sm">Sign In</Button>238 </div>239 </header>240 )241 }`,242 "src/components/footer.tsx": `export function Footer() {243 return (244 <footer className="border-t py-6 md:py-0">245 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">246 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">247 Built with Next.js and Tailwind CSS.248 </p>249 </div>250 </footer>251 )252 }`,253 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"254 import { twMerge } from "tailwind-merge"255256 export function cn(...inputs: ClassValue[]) {257 return twMerge(clsx(inputs))258 }`,259 "src/lib/constants.ts": `export const APP_NAME = "My Application"260 export const APP_VERSION = "1.0.0"261 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"262263 export const ROUTES = {264 home: "/",265 about: "/about",266 dashboard: "/dashboard",267 settings: "/settings",268 } as const`,269 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"270271 interface User {272 id: string273 email: string274 name: string275 }276277 export function useAuth() {278 const [user, setUser] = useState<User | null>(null)279 const [loading, setLoading] = useState(true)280281 useEffect(() => {282 // Check for existing session283 const checkAuth = async () => {284 try {285 const response = await fetch("/api/auth/me")286 if (response.ok) {287 const userData = await response.json()288 setUser(userData)289 }290 } catch (error) {291 console.error("Auth check failed:", error)292 } finally {293 setLoading(false)294 }295 }296 checkAuth()297 }, [])298299 const signOut = useCallback(async () => {300 await fetch("/api/auth/signout", { method: "POST" })301 setUser(null)302 }, [])303304 return { user, loading, signOut }305 }`,306 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"307308 type Theme = "light" | "dark" | "system"309310 export function useTheme() {311 const [theme, setTheme] = useState<Theme>("system")312313 useEffect(() => {314 const stored = localStorage.getItem("theme") as Theme | null315 if (stored) {316 setTheme(stored)317 }318 }, [])319320 useEffect(() => {321 const root = document.documentElement322 root.classList.remove("light", "dark")323324 if (theme === "system") {325 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches326 ? "dark"327 : "light"328 root.classList.add(systemTheme)329 } else {330 root.classList.add(theme)331 }332333 localStorage.setItem("theme", theme)334 }, [theme])335336 return { theme, setTheme }337 }`,338 "package.json": `{339 "name": "my-nextjs-app",340 "version": "0.1.0",341 "private": true,342 "scripts": {343 "dev": "next dev",344 "build": "next build",345 "start": "next start",346 "lint": "next lint"347 },348 "dependencies": {349 "next": "14.2.0",350 "react": "^18",351 "react-dom": "^18",352 "class-variance-authority": "^0.7.0",353 "clsx": "^2.1.0",354 "tailwind-merge": "^2.2.0",355 "lucide-react": "^0.344.0"356 },357 "devDependencies": {358 "typescript": "^5",359 "@types/node": "^20",360 "@types/react": "^18",361 "@types/react-dom": "^18",362 "tailwindcss": "^3.4.1",363 "postcss": "^8"364 }365 }`,366 "tsconfig.json": `{367 "compilerOptions": {368 "lib": ["dom", "dom.iterable", "esnext"],369 "allowJs": true,370 "skipLibCheck": true,371 "strict": true,372 "noEmit": true,373 "esModuleInterop": true,374 "module": "esnext",375 "moduleResolution": "bundler",376 "resolveJsonModule": true,377 "isolatedModules": true,378 "jsx": "preserve",379 "incremental": true,380 "plugins": [{ "name": "next" }],381 "paths": {382 "@/*": ["./*"]383 }384 },385 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],386 "exclude": ["node_modules"]387 }`,388 "next.config.mjs": `/** @type {import('next').NextConfig} */389 const nextConfig = {390 reactStrictMode: true,391 images: {392 domains: [],393 },394 }395396 export default nextConfig`,397 "tailwind.config.ts": `import type { Config } from "tailwindcss"398399 const config: Config = {400 darkMode: ["class"],401 content: [402 "./pages/**/*.{js,ts,jsx,tsx,mdx}",403 "./components/**/*.{js,ts,jsx,tsx,mdx}",404 "./app/**/*.{js,ts,jsx,tsx,mdx}",405 ],406 theme: {407 extend: {408 colors: {409 border: "hsl(var(--border))",410 background: "hsl(var(--background))",411 foreground: "hsl(var(--foreground))",412 },413 },414 },415 plugins: [],416 }417 export default config`,418 ".env.local": `# Environment Variables419 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"420 NEXT_PUBLIC_API_URL="http://localhost:3000/api"421 AUTH_SECRET="your-secret-key-here"`,422 ".gitignore": `# Dependencies423 node_modules424 .pnp425 .pnp.js426427 # Testing428 coverage429430 # Next.js431 .next/432 out/433434 # Production435 build436437 # Misc438 .DS_Store439 *.pem440441 # Debug442 npm-debug.log*443444 # Local env files445 .env*.local446447 # Vercel448 .vercel449450 # TypeScript451 *.tsbuildinfo452 next-env.d.ts`,453 "README.md": `# My Next.js App454455 A modern web application built with Next.js 14, React, and Tailwind CSS.456457 ## Getting Started458459 First, install dependencies:460461 \`\`\`bash462 npm install463 # or464 yarn install465 # or466 pnpm install467 \`\`\`468469 Then, run the development server:470471 \`\`\`bash472 npm run dev473 \`\`\`474475 Open [http://localhost:3000](http://localhost:3000) to see the result.476477 ## Features478479 - Next.js 14 App Router480 - TypeScript481 - Tailwind CSS482 - Component library with CVA variants483 `,484};485486const sampleData: FileTreeNode[] = [487 {488 name: "src",489 type: "folder",490 children: [491 {492 name: "app",493 type: "folder",494 children: [495 { name: "layout.tsx", type: "file" },496 { name: "page.tsx", type: "file" },497 { name: "globals.css", type: "file" },498 {499 name: "api",500 type: "folder",501 children: [502 {503 name: "auth",504 type: "folder",505 children: [{ name: "route.ts", type: "file" }],506 },507 {508 name: "users",509 type: "folder",510 children: [{ name: "route.ts", type: "file" }],511 },512 ],513 },514 ],515 },516 {517 name: "components",518 type: "folder",519 children: [520 {521 name: "ui",522 type: "folder",523 children: [524 { name: "button.tsx", type: "file" },525 { name: "card.tsx", type: "file" },526 { name: "file-tree.tsx", type: "file" },527 ],528 },529 { name: "header.tsx", type: "file" },530 { name: "footer.tsx", type: "file" },531 ],532 },533 {534 name: "lib",535 type: "folder",536 children: [537 { name: "utils.ts", type: "file" },538 { name: "constants.ts", type: "file" },539 ],540 },541 {542 name: "hooks",543 type: "folder",544 children: [545 { name: "use-auth.ts", type: "file" },546 { name: "use-theme.ts", type: "file" },547 ],548 },549 ],550 },551 {552 name: "public",553 type: "folder",554 children: [555 { name: "favicon.ico", type: "file" },556 { name: "logo.svg", type: "file" },557 {558 name: "images",559 type: "folder",560 children: [561 { name: "hero.png", type: "file" },562 { name: "avatar.jpg", type: "file" },563 ],564 },565 ],566 },567 { name: "package.json", type: "file" },568 { name: "tsconfig.json", type: "file" },569 { name: "next.config.mjs", type: "file" },570 { name: "tailwind.config.ts", type: "file" },571 { name: ".env.local", type: "file" },572 { name: ".gitignore", type: "file" },573 { name: "README.md", type: "file" },574];575576const imagePreviewUrls: Record<string, string> = {577 "public/images/hero.png":578 "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1200&auto=format&fit=crop",579 "public/images/avatar.jpg":580 "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=400&auto=format&fit=crop",581 "public/logo.svg": "/file.svg",582 "public/favicon.ico": "https://picsum.photos/seed/favicon/64/64",583};584585export function FileViewerDefault() {586 const [selectedPath, setSelectedPath] = useState<string>("");587 const [selectedFile, setSelectedFile] = useState<FileContent | null>(null);588589 const handleSelect = (node: FileTreeNode, path: string) => {590 setSelectedPath(path);591592 if (node.type === "file") {593 const content = fileContents[path];594 const previewUrl = imagePreviewUrls[path];595 if (content) {596 setSelectedFile({597 path,598 name: node.name,599 content,600 previewUrl,601 });602 } else {603 setSelectedFile({604 path,605 name: node.name,606 content: previewUrl607 ? ""608 : `// Content for ${node.name} not available in demo`,609 previewUrl,610 });611 }612 }613 };614615 const handleClose = () => {616 setSelectedFile(null);617 setSelectedPath("");618 };619 return (620 <div className="w-full">621 <FileExplorer622 variant="elevated"623 rounded="lg"624 sidebarWidth="300px"625 collapsible626 className="h-[600px]"627 >628 <FileExplorerSidebar className="p-3">629 <div className="mb-3 flex items-center justify-between gap-2 px-2">630 <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">631 Explorer632 </span>633 <FileExplorerSidebarToggleWhen when="open" />634 </div>635 <FileTree636 data={sampleData}637 variant="ghost"638 size="default"639 onSelect={handleSelect}640 selectedPath={selectedPath}641 />642 </FileExplorerSidebar>643 <FileExplorerContent>644 <FileExplorerSidebarToggleWhen645 when="closed"646 className="absolute left-2 top-2 z-10"647 />648 <FileViewer649 file={selectedFile}650 variant="ghost"651 size="default"652 rounded="none"653 maxHeight="100%"654 onClose={handleClose}655 className="h-full"656 />657 </FileExplorerContent>658 </FileExplorer>659 </div>660 );661}
Standalone
Standalone file viewer
src/lib/utils.ts
1import { type ClassValue, clsx } from "clsx"2 import { twMerge } from "tailwind-merge"34 export function cn(...inputs: ClassValue[]) {5 return twMerge(clsx(inputs))6 }
package.json
1{2 "name": "my-nextjs-app",3 "version": "0.1.0",4 "private": true,5 "scripts": {6 "dev": "next dev",7 "build": "next build",8 "start": "next start",9 "lint": "next lint"10 },11 "dependencies": {12 "next": "14.2.0",13 "react": "^18",14 "react-dom": "^18",15 "class-variance-authority": "^0.7.0",16 "clsx": "^2.1.0",17 "tailwind-merge": "^2.2.0",18 "lucide-react": "^0.344.0"19 },20 "devDependencies": {21 "typescript": "^5",22 "@types/node": "^20",23 "@types/react": "^18",24 "@types/react-dom": "^18",25 "tailwindcss": "^3.4.1",26 "postcss": "^8"27 }28 }
standalone-example.tsx
1"use client";23import { FileViewer } from "@/registry/ui";45const fileContents: Record<string, string> = {6 "src/app/layout.tsx": `import type { Metadata } from "next"7import { Inter } from "next/font/google"8import "./globals.css"910const inter = Inter({ subsets: ["latin"] })1112export const metadata: Metadata = {13 title: "My App",14 description: "A Next.js application",15}1617export default function RootLayout({18 children,19}: {20 children: React.ReactNode21}) {22 return (23 <html lang="en">24 <body className={inter.className}>{children}</body>25 </html>26 )27}`,28 "src/app/page.tsx": `export default function Home() {29 return (30 <main className="flex min-h-screen flex-col items-center justify-center p-24">31 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>32 <p className="mt-4 text-lg text-muted-foreground">33 Get started by editing app/page.tsx34 </p>35 </main>36 )37 }`,38 "src/app/globals.css": `@tailwind base;39 @tailwind components;40 @tailwind utilities;4142 :root {43 --foreground-rgb: 0, 0, 0;44 --background-start-rgb: 214, 219, 220;45 --background-end-rgb: 255, 255, 255;46 }4748 @media (prefers-color-scheme: dark) {49 :root {50 --foreground-rgb: 255, 255, 255;51 --background-start-rgb: 0, 0, 0;52 --background-end-rgb: 0, 0, 0;53 }54 }`,55 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"5657 export async function POST(request: Request) {58 const body = await request.json()59 const { email, password } = body6061 // Validate credentials62 if (!email || !password) {63 return NextResponse.json(64 { error: "Missing credentials" },65 { status: 400 }66 )67 }6869 // TODO: Implement actual authentication70 return NextResponse.json({ success: true })71 }`,72 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"7374 const users = [75 { id: 1, name: "Alice", email: "alice@example.com" },76 { id: 2, name: "Bob", email: "bob@example.com" },77 ]7879 export async function GET() {80 return NextResponse.json(users)81 }8283 export async function POST(request: Request) {84 const body = await request.json()85 const newUser = { id: users.length + 1, ...body }86 users.push(newUser)87 return NextResponse.json(newUser, { status: 201 })88 }`,89 "src/components/ui/button.tsx": `import * as React from "react"90 import { cva, type VariantProps } from "class-variance-authority"91 import { cn } from "@/lib/utils"9293 const buttonVariants = cva(94 "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",95 {96 variants: {97 variant: {98 default: "bg-primary text-primary-foreground hover:bg-primary/90",99 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",100 outline: "border border-input hover:bg-accent hover:text-accent-foreground",101 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",102 ghost: "hover:bg-accent hover:text-accent-foreground",103 link: "underline-offset-4 hover:underline text-primary",104 },105 size: {106 default: "h-10 px-4 py-2",107 sm: "h-9 rounded-md px-3",108 lg: "h-11 rounded-md px-8",109 icon: "h-10 w-10",110 },111 },112 defaultVariants: {113 variant: "default",114 size: "default",115 },116 }117 )118119 export interface ButtonProps120 extends React.ButtonHTMLAttributes<HTMLButtonElement>,121 VariantProps<typeof buttonVariants> {}122123 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(124 ({ className, variant, size, ...props }, ref) => {125 return (126 <button127 className={cn(buttonVariants({ variant, size, className }))}128 ref={ref}129 {...props}130 />131 )132 }133 )134 Button.displayName = "Button"135136 export { Button, buttonVariants }`,137 "src/components/ui/card.tsx": `import * as React from "react"138 import { cn } from "@/lib/utils"139140 const Card = React.forwardRef<141 HTMLDivElement,142 React.HTMLAttributes<HTMLDivElement>143 >(({ className, ...props }, ref) => (144 <div145 ref={ref}146 className={cn(147 "rounded-lg border bg-card text-card-foreground shadow-sm",148 className149 )}150 {...props}151 />152 ))153 Card.displayName = "Card"154155 const CardHeader = React.forwardRef<156 HTMLDivElement,157 React.HTMLAttributes<HTMLDivElement>158 >(({ className, ...props }, ref) => (159 <div160 ref={ref}161 className={cn("flex flex-col space-y-1.5 p-6", className)}162 {...props}163 />164 ))165 CardHeader.displayName = "CardHeader"166167 const CardTitle = React.forwardRef<168 HTMLParagraphElement,169 React.HTMLAttributes<HTMLHeadingElement>170 >(({ className, ...props }, ref) => (171 <h3172 ref={ref}173 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}174 {...props}175 />176 ))177 CardTitle.displayName = "CardTitle"178179 export { Card, CardHeader, CardTitle }`,180 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component181 // This is a simplified preview182183 import { cva } from "class-variance-authority"184185 export const fileTreeVariants = cva(186 "font-mono text-sm select-none",187 {188 variants: {189 variant: {190 default: "bg-background text-foreground",191 ghost: "bg-transparent",192 bordered: "bg-background border border-border rounded-lg p-2",193 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",194 },195 },196 }197 )`,198 "src/components/header.tsx": `import Link from "next/link"199 import { Button } from "@/components/ui/button"200201 export function Header() {202 return (203 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">204 <div className="container flex h-14 items-center">205 <Link href="/" className="mr-6 flex items-center space-x-2">206 <span className="font-bold">My App</span>207 </Link>208 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">209 <Link href="/about">About</Link>210 <Link href="/docs">Docs</Link>211 </nav>212 <Button size="sm">Sign In</Button>213 </div>214 </header>215 )216 }`,217 "src/components/footer.tsx": `export function Footer() {218 return (219 <footer className="border-t py-6 md:py-0">220 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">221 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">222 Built with Next.js and Tailwind CSS.223 </p>224 </div>225 </footer>226 )227 }`,228 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"229 import { twMerge } from "tailwind-merge"230231 export function cn(...inputs: ClassValue[]) {232 return twMerge(clsx(inputs))233 }`,234 "src/lib/constants.ts": `export const APP_NAME = "My Application"235 export const APP_VERSION = "1.0.0"236 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"237238 export const ROUTES = {239 home: "/",240 about: "/about",241 dashboard: "/dashboard",242 settings: "/settings",243 } as const`,244 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"245246 interface User {247 id: string248 email: string249 name: string250 }251252 export function useAuth() {253 const [user, setUser] = useState<User | null>(null)254 const [loading, setLoading] = useState(true)255256 useEffect(() => {257 // Check for existing session258 const checkAuth = async () => {259 try {260 const response = await fetch("/api/auth/me")261 if (response.ok) {262 const userData = await response.json()263 setUser(userData)264 }265 } catch (error) {266 console.error("Auth check failed:", error)267 } finally {268 setLoading(false)269 }270 }271 checkAuth()272 }, [])273274 const signOut = useCallback(async () => {275 await fetch("/api/auth/signout", { method: "POST" })276 setUser(null)277 }, [])278279 return { user, loading, signOut }280 }`,281 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"282283 type Theme = "light" | "dark" | "system"284285 export function useTheme() {286 const [theme, setTheme] = useState<Theme>("system")287288 useEffect(() => {289 const stored = localStorage.getItem("theme") as Theme | null290 if (stored) {291 setTheme(stored)292 }293 }, [])294295 useEffect(() => {296 const root = document.documentElement297 root.classList.remove("light", "dark")298299 if (theme === "system") {300 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches301 ? "dark"302 : "light"303 root.classList.add(systemTheme)304 } else {305 root.classList.add(theme)306 }307308 localStorage.setItem("theme", theme)309 }, [theme])310311 return { theme, setTheme }312 }`,313 "package.json": `{314 "name": "my-nextjs-app",315 "version": "0.1.0",316 "private": true,317 "scripts": {318 "dev": "next dev",319 "build": "next build",320 "start": "next start",321 "lint": "next lint"322 },323 "dependencies": {324 "next": "14.2.0",325 "react": "^18",326 "react-dom": "^18",327 "class-variance-authority": "^0.7.0",328 "clsx": "^2.1.0",329 "tailwind-merge": "^2.2.0",330 "lucide-react": "^0.344.0"331 },332 "devDependencies": {333 "typescript": "^5",334 "@types/node": "^20",335 "@types/react": "^18",336 "@types/react-dom": "^18",337 "tailwindcss": "^3.4.1",338 "postcss": "^8"339 }340 }`,341 "tsconfig.json": `{342 "compilerOptions": {343 "lib": ["dom", "dom.iterable", "esnext"],344 "allowJs": true,345 "skipLibCheck": true,346 "strict": true,347 "noEmit": true,348 "esModuleInterop": true,349 "module": "esnext",350 "moduleResolution": "bundler",351 "resolveJsonModule": true,352 "isolatedModules": true,353 "jsx": "preserve",354 "incremental": true,355 "plugins": [{ "name": "next" }],356 "paths": {357 "@/*": ["./*"]358 }359 },360 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],361 "exclude": ["node_modules"]362 }`,363 "next.config.mjs": `/** @type {import('next').NextConfig} */364 const nextConfig = {365 reactStrictMode: true,366 images: {367 domains: [],368 },369 }370371 export default nextConfig`,372 "tailwind.config.ts": `import type { Config } from "tailwindcss"373374 const config: Config = {375 darkMode: ["class"],376 content: [377 "./pages/**/*.{js,ts,jsx,tsx,mdx}",378 "./components/**/*.{js,ts,jsx,tsx,mdx}",379 "./app/**/*.{js,ts,jsx,tsx,mdx}",380 ],381 theme: {382 extend: {383 colors: {384 border: "hsl(var(--border))",385 background: "hsl(var(--background))",386 foreground: "hsl(var(--foreground))",387 },388 },389 },390 plugins: [],391 }392 export default config`,393 ".env.local": `# Environment Variables394 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"395 NEXT_PUBLIC_API_URL="http://localhost:3000/api"396 AUTH_SECRET="your-secret-key-here"`,397 ".gitignore": `# Dependencies398 node_modules399 .pnp400 .pnp.js401402 # Testing403 coverage404405 # Next.js406 .next/407 out/408409 # Production410 build411412 # Misc413 .DS_Store414 *.pem415416 # Debug417 npm-debug.log*418419 # Local env files420 .env*.local421422 # Vercel423 .vercel424425 # TypeScript426 *.tsbuildinfo427 next-env.d.ts`,428 "README.md": `# My Next.js App429430 A modern web application built with Next.js 14, React, and Tailwind CSS.431432 ## Getting Started433434 First, install dependencies:435436 \`\`\`bash437 npm install438 # or439 yarn install440 # or441 pnpm install442 \`\`\`443444 Then, run the development server:445446 \`\`\`bash447 npm run dev448 \`\`\`449450 Open [http://localhost:3000](http://localhost:3000) to see the result.451452 ## Features453454 - Next.js 14 App Router455 - TypeScript456 - Tailwind CSS457 - Component library with CVA variants458 `,459};460461export function FileViewerStandalone() {462 return (463 <section className="grid gap-8 lg:grid-cols-2">464 <div>465 <FileViewer466 file={{467 path: "src/lib/utils.ts",468 name: "utils.ts",469 content: fileContents["src/lib/utils.ts"],470 }}471 variant="bordered"472 size="default"473 maxHeight="300px"474 highlightedLines={{475 4: "modified",476 }}477 />478 </div>479 <div>480 <FileViewer481 file={{482 path: "package.json",483 name: "package.json",484 content: fileContents["package.json"],485 }}486 variant="elevated"487 size="sm"488 maxHeight="300px"489 />490 </div>491 </section>492 );493}
With editor sidebar
Full workbench with activity rail, search, and extensions
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/file-viewer
Props
| Name | Type | Default | Description |
|---|---|---|---|
| file | FileContent | undefined | The file to display |
| showLineNumbers | boolean | true | Show line numbers |
| highlightedLines | Record<number, string> | {} | Highlighted lines |
| onClose | () => void | undefined | Callback when the file viewer is closed |
| showHeader | boolean | true | Show the header |
| maxHeight | string | 100% | The maximum height of the file viewer |
| emptyMessage | string | Select a file to view its contents | The message to display when no file is selected |
| collapsible (FileExplorer) | boolean | false | When true, the explorer sidebar can collapse to give the viewer full width |
| defaultSidebarOpen (FileExplorer) | boolean | true | Initial open state when collapsible is enabled |
| sidebarOpen (FileExplorer) | boolean | undefined | Controlled sidebar open state |
| onSidebarOpenChange (FileExplorer) | (open: boolean) => void | undefined | Called when the collapsible sidebar opens or closes |
| sidebarWidth (FileExplorer) | string | 280px | Width of the explorer sidebar when expanded |