All components

File Viewer

Displays file content with an optional FileExplorer layout that combines a file tree sidebar and content panel. Supports a collapsible sidebar when used with FileTree.

Default
File tree and viewer with collapsible explorer sidebar
Explorer
src
app
layout.tsx
page.tsx
globals.css
api
auth
route.ts
users
route.ts
components
ui
button.tsx
card.tsx
file-tree.tsx
header.tsx
footer.tsx
lib
utils.ts
constants.ts
hooks
use-auth.ts
use-theme.ts
public
favicon.ico
logo.svg
images
hero.png
avatar.jpg
package.json
tsconfig.json
next.config.mjs
tailwind.config.ts
.env.local
.gitignore
README.md

Select a file to view its contents

1"use client";
2
3import {
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";
15
16function 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}
29
30const fileContents: Record<string, string> = {
31 "src/app/layout.tsx": `import type { Metadata } from "next"
32import { Inter } from "next/font/google"
33import "./globals.css"
34
35const inter = Inter({ subsets: ["latin"] })
36
37export const metadata: Metadata = {
38 title: "My App",
39 description: "A Next.js application",
40}
41
42export default function RootLayout({
43 children,
44}: {
45 children: React.ReactNode
46}) {
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.tsx
59 </p>
60 </main>
61 )
62 }`,
63 "src/app/globals.css": `@tailwind base;
64 @tailwind components;
65 @tailwind utilities;
66
67 :root {
68 --foreground-rgb: 0, 0, 0;
69 --background-start-rgb: 214, 219, 220;
70 --background-end-rgb: 255, 255, 255;
71 }
72
73 @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"
81
82 export async function POST(request: Request) {
83 const body = await request.json()
84 const { email, password } = body
85
86 // Validate credentials
87 if (!email || !password) {
88 return NextResponse.json(
89 { error: "Missing credentials" },
90 { status: 400 }
91 )
92 }
93
94 // TODO: Implement actual authentication
95 return NextResponse.json({ success: true })
96 }`,
97 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"
98
99 const users = [
100 { id: 1, name: "Alice", email: "alice@example.com" },
101 { id: 2, name: "Bob", email: "bob@example.com" },
102 ]
103
104 export async function GET() {
105 return NextResponse.json(users)
106 }
107
108 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"
117
118 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 )
143
144 export interface ButtonProps
145 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
146 VariantProps<typeof buttonVariants> {}
147
148 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
149 function Button({ className, variant, size, ...props }, ref) {
150 return (
151 <button
152 className={cn(buttonVariants({ variant, size, className }))}
153 ref={ref}
154 {...props}
155 />
156 )
157 }
158 )
159
160 export { Button, buttonVariants }`,
161 "src/components/ui/card.tsx": `import * as React from "react"
162 import { cn } from "@/lib/utils"
163
164 const Card = React.forwardRef<
165 HTMLDivElement,
166 React.HTMLAttributes<HTMLDivElement>
167 >(function Card({ className, ...props }, ref) {
168 return (
169 <div
170 ref={ref}
171 className={cn(
172 "rounded-lg border bg-card text-card-foreground shadow-sm",
173 className
174 )}
175 {...props}
176 />
177 )
178 })
179
180 const CardHeader = React.forwardRef<
181 HTMLDivElement,
182 React.HTMLAttributes<HTMLDivElement>
183 >(function CardHeader({ className, ...props }, ref) {
184 return (
185 <div
186 ref={ref}
187 className={cn("flex flex-col space-y-1.5 p-6", className)}
188 {...props}
189 />
190 )
191 })
192
193 const CardTitle = React.forwardRef<
194 HTMLParagraphElement,
195 React.HTMLAttributes<HTMLHeadingElement>
196 >(function CardTitle({ className, ...props }, ref) {
197 return (
198 <h3
199 ref={ref}
200 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
201 {...props}
202 />
203 )
204 })
205
206 export { Card, CardHeader, CardTitle }`,
207 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
208 // This is a simplified preview
209
210 import { cva } from "class-variance-authority"
211
212 export const fileTreeVariants = cva(
213 "font-mono text-sm select-none",
214 {
215 variants: {
216 variant: {
217 default: "bg-background text-foreground",
218 ghost: "bg-transparent",
219 bordered: "bg-background border border-border rounded-lg p-2",
220 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",
221 },
222 },
223 }
224 )`,
225 "src/components/header.tsx": `import Link from "next/link"
226 import { Button } from "@/components/ui/button"
227
228 export function Header() {
229 return (
230 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
231 <div className="container flex h-14 items-center">
232 <Link href="/" className="mr-6 flex items-center space-x-2">
233 <span className="font-bold">My App</span>
234 </Link>
235 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">
236 <Link href="/about">About</Link>
237 <Link href="/docs">Docs</Link>
238 </nav>
239 <Button size="sm">Sign In</Button>
240 </div>
241 </header>
242 )
243 }`,
244 "src/components/footer.tsx": `export function Footer() {
245 return (
246 <footer className="border-t py-6 md:py-0">
247 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
248 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">
249 Built with Next.js and Tailwind CSS.
250 </p>
251 </div>
252 </footer>
253 )
254 }`,
255 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"
256 import { twMerge } from "tailwind-merge"
257
258 export function cn(...inputs: ClassValue[]) {
259 return twMerge(clsx(inputs))
260 }`,
261 "src/lib/constants.ts": `export const APP_NAME = "My Application"
262 export const APP_VERSION = "1.0.0"
263 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"
264
265 export const ROUTES = {
266 home: "/",
267 about: "/about",
268 dashboard: "/dashboard",
269 settings: "/settings",
270 } as const`,
271 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"
272
273 interface User {
274 id: string
275 email: string
276 name: string
277 }
278
279 export function useAuth() {
280 const [user, setUser] = useState<User | null>(null)
281 const [loading, setLoading] = useState(true)
282
283 useEffect(() => {
284 // Check for existing session
285 const checkAuth = async () => {
286 try {
287 const response = await fetch("/api/auth/me")
288 if (response.ok) {
289 const userData = await response.json()
290 setUser(userData)
291 }
292 } catch (error) {
293 console.error("Auth check failed:", error)
294 } finally {
295 setLoading(false)
296 }
297 }
298 checkAuth()
299 }, [])
300
301 const signOut = useCallback(async () => {
302 await fetch("/api/auth/signout", { method: "POST" })
303 setUser(null)
304 }, [])
305
306 return { user, loading, signOut }
307 }`,
308 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
309
310 type Theme = "light" | "dark" | "system"
311
312 export function useTheme() {
313 const [theme, setTheme] = useState<Theme>("system")
314
315 useEffect(() => {
316 const stored = localStorage.getItem("theme") as Theme | null
317 if (stored) {
318 setTheme(stored)
319 }
320 }, [])
321
322 useEffect(() => {
323 const root = document.documentElement
324 root.classList.remove("light", "dark")
325
326 if (theme === "system") {
327 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
328 ? "dark"
329 : "light"
330 root.classList.add(systemTheme)
331 } else {
332 root.classList.add(theme)
333 }
334
335 localStorage.setItem("theme", theme)
336 }, [theme])
337
338 return { theme, setTheme }
339 }`,
340 "package.json": `{
341 "name": "my-nextjs-app",
342 "version": "0.1.0",
343 "private": true,
344 "scripts": {
345 "dev": "next dev",
346 "build": "next build",
347 "start": "next start",
348 "lint": "next lint"
349 },
350 "dependencies": {
351 "next": "14.2.0",
352 "react": "^18",
353 "react-dom": "^18",
354 "class-variance-authority": "^0.7.0",
355 "clsx": "^2.1.0",
356 "tailwind-merge": "^2.2.0",
357 "lucide-react": "^0.344.0"
358 },
359 "devDependencies": {
360 "typescript": "^5",
361 "@types/node": "^20",
362 "@types/react": "^18",
363 "@types/react-dom": "^18",
364 "tailwindcss": "^3.4.1",
365 "postcss": "^8"
366 }
367 }`,
368 "tsconfig.json": `{
369 "compilerOptions": {
370 "lib": ["dom", "dom.iterable", "esnext"],
371 "allowJs": true,
372 "skipLibCheck": true,
373 "strict": true,
374 "noEmit": true,
375 "esModuleInterop": true,
376 "module": "esnext",
377 "moduleResolution": "bundler",
378 "resolveJsonModule": true,
379 "isolatedModules": true,
380 "jsx": "preserve",
381 "incremental": true,
382 "plugins": [{ "name": "next" }],
383 "paths": {
384 "@/*": ["./*"]
385 }
386 },
387 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
388 "exclude": ["node_modules"]
389 }`,
390 "next.config.mjs": `/** @type {import('next').NextConfig} */
391 const nextConfig = {
392 reactStrictMode: true,
393 images: {
394 domains: [],
395 },
396 }
397
398 export default nextConfig`,
399 "tailwind.config.ts": `import type { Config } from "tailwindcss"
400
401 const config: Config = {
402 darkMode: ["class"],
403 content: [
404 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
405 "./components/**/*.{js,ts,jsx,tsx,mdx}",
406 "./app/**/*.{js,ts,jsx,tsx,mdx}",
407 ],
408 theme: {
409 extend: {
410 colors: {
411 border: "hsl(var(--border))",
412 background: "hsl(var(--background))",
413 foreground: "hsl(var(--foreground))",
414 },
415 },
416 },
417 plugins: [],
418 }
419 export default config`,
420 ".env.local": `# Environment Variables
421 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
422 NEXT_PUBLIC_API_URL="http://localhost:3000/api"
423 AUTH_SECRET="your-secret-key-here"`,
424 ".gitignore": `# Dependencies
425 node_modules
426 .pnp
427 .pnp.js
428
429 # Testing
430 coverage
431
432 # Next.js
433 .next/
434 out/
435
436 # Production
437 build
438
439 # Misc
440 .DS_Store
441 *.pem
442
443 # Debug
444 npm-debug.log*
445
446 # Local env files
447 .env*.local
448
449 # Vercel
450 .vercel
451
452 # TypeScript
453 *.tsbuildinfo
454 next-env.d.ts`,
455 "README.md": `# My Next.js App
456
457 A modern web application built with Next.js 14, React, and Tailwind CSS.
458
459 ## Getting Started
460
461 First, install dependencies:
462
463 \`\`\`bash
464 npm install
465 # or
466 yarn install
467 # or
468 pnpm install
469 \`\`\`
470
471 Then, run the development server:
472
473 \`\`\`bash
474 npm run dev
475 \`\`\`
476
477 Open [http://localhost:3000](http://localhost:3000) to see the result.
478
479 ## Features
480
481 - Next.js 14 App Router
482 - TypeScript
483 - Tailwind CSS
484 - Component library with CVA variants
485 `,
486};
487
488const sampleData: FileTreeNode[] = [
489 {
490 name: "src",
491 type: "folder",
492 children: [
493 {
494 name: "app",
495 type: "folder",
496 children: [
497 { name: "layout.tsx", type: "file" },
498 { name: "page.tsx", type: "file" },
499 { name: "globals.css", type: "file" },
500 {
501 name: "api",
502 type: "folder",
503 children: [
504 {
505 name: "auth",
506 type: "folder",
507 children: [{ name: "route.ts", type: "file" }],
508 },
509 {
510 name: "users",
511 type: "folder",
512 children: [{ name: "route.ts", type: "file" }],
513 },
514 ],
515 },
516 ],
517 },
518 {
519 name: "components",
520 type: "folder",
521 children: [
522 {
523 name: "ui",
524 type: "folder",
525 children: [
526 { name: "button.tsx", type: "file" },
527 { name: "card.tsx", type: "file" },
528 { name: "file-tree.tsx", type: "file" },
529 ],
530 },
531 { name: "header.tsx", type: "file" },
532 { name: "footer.tsx", type: "file" },
533 ],
534 },
535 {
536 name: "lib",
537 type: "folder",
538 children: [
539 { name: "utils.ts", type: "file" },
540 { name: "constants.ts", type: "file" },
541 ],
542 },
543 {
544 name: "hooks",
545 type: "folder",
546 children: [
547 { name: "use-auth.ts", type: "file" },
548 { name: "use-theme.ts", type: "file" },
549 ],
550 },
551 ],
552 },
553 {
554 name: "public",
555 type: "folder",
556 children: [
557 { name: "favicon.ico", type: "file" },
558 { name: "logo.svg", type: "file" },
559 {
560 name: "images",
561 type: "folder",
562 children: [
563 { name: "hero.png", type: "file" },
564 { name: "avatar.jpg", type: "file" },
565 ],
566 },
567 ],
568 },
569 { name: "package.json", type: "file" },
570 { name: "tsconfig.json", type: "file" },
571 { name: "next.config.mjs", type: "file" },
572 { name: "tailwind.config.ts", type: "file" },
573 { name: ".env.local", type: "file" },
574 { name: ".gitignore", type: "file" },
575 { name: "README.md", type: "file" },
576];
577
578const imagePreviewUrls: Record<string, string> = {
579 "public/images/hero.png":
580 "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1200&auto=format&fit=crop",
581 "public/images/avatar.jpg":
582 "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=400&auto=format&fit=crop",
583 "public/logo.svg": "/file.svg",
584 "public/favicon.ico": "https://picsum.photos/seed/favicon/64/64",
585};
586
587export function FileViewerDefault() {
588 const [selectedPath, setSelectedPath] = useState<string>("");
589 const [selectedFile, setSelectedFile] = useState<FileContent | null>(null);
590
591 const handleSelect = (node: FileTreeNode, path: string) => {
592 setSelectedPath(path);
593
594 if (node.type === "file") {
595 const content = fileContents[path];
596 const previewUrl = imagePreviewUrls[path];
597 if (content) {
598 setSelectedFile({
599 path,
600 name: node.name,
601 content,
602 previewUrl,
603 });
604 } else {
605 setSelectedFile({
606 path,
607 name: node.name,
608 content: previewUrl
609 ? ""
610 : `// Content for ${node.name} not available in demo`,
611 previewUrl,
612 });
613 }
614 }
615 };
616
617 const handleClose = () => {
618 setSelectedFile(null);
619 setSelectedPath("");
620 };
621 return (
622 <div className="w-full">
623 <FileExplorer
624 variant="elevated"
625 rounded="lg"
626 sidebarWidth="300px"
627 collapsible
628 className="h-[600px]"
629 >
630 <FileExplorerSidebar className="p-3">
631 <div className="mb-3 flex items-center justify-between gap-2 px-2">
632 <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
633 Explorer
634 </span>
635 <FileExplorerSidebarToggleWhen when="open" />
636 </div>
637 <FileTree
638 data={sampleData}
639 variant="ghost"
640 size="default"
641 onSelect={handleSelect}
642 selectedPath={selectedPath}
643 />
644 </FileExplorerSidebar>
645 <FileExplorerContent>
646 <FileExplorerSidebarToggleWhen
647 when="closed"
648 className="absolute left-2 top-2 z-10"
649 />
650 <FileViewer
651 file={selectedFile}
652 variant="ghost"
653 size="default"
654 rounded="none"
655 maxHeight="100%"
656 onClose={handleClose}
657 className="h-full"
658 />
659 </FileExplorerContent>
660 </FileExplorer>
661 </div>
662 );
663}
Standalone
Standalone file viewer
src/lib/utils.ts
1import { type ClassValue, clsx } from "clsx"
2 import { twMerge } from "tailwind-merge"
3
4 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 }
1"use client";
2
3import { FileViewer } from "@/registry/ui";
4
5const fileContents: Record<string, string> = {
6 "src/app/layout.tsx": `import type { Metadata } from "next"
7import { Inter } from "next/font/google"
8import "./globals.css"
9
10const inter = Inter({ subsets: ["latin"] })
11
12export const metadata: Metadata = {
13 title: "My App",
14 description: "A Next.js application",
15}
16
17export default function RootLayout({
18 children,
19}: {
20 children: React.ReactNode
21}) {
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.tsx
34 </p>
35 </main>
36 )
37 }`,
38 "src/app/globals.css": `@tailwind base;
39 @tailwind components;
40 @tailwind utilities;
41
42 :root {
43 --foreground-rgb: 0, 0, 0;
44 --background-start-rgb: 214, 219, 220;
45 --background-end-rgb: 255, 255, 255;
46 }
47
48 @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"
56
57 export async function POST(request: Request) {
58 const body = await request.json()
59 const { email, password } = body
60
61 // Validate credentials
62 if (!email || !password) {
63 return NextResponse.json(
64 { error: "Missing credentials" },
65 { status: 400 }
66 )
67 }
68
69 // TODO: Implement actual authentication
70 return NextResponse.json({ success: true })
71 }`,
72 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"
73
74 const users = [
75 { id: 1, name: "Alice", email: "alice@example.com" },
76 { id: 2, name: "Bob", email: "bob@example.com" },
77 ]
78
79 export async function GET() {
80 return NextResponse.json(users)
81 }
82
83 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"
92
93 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 )
118
119 export interface ButtonProps
120 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
121 VariantProps<typeof buttonVariants> {}
122
123 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
124 function Button({ className, variant, size, ...props }, ref) {
125 return (
126 <button
127 className={cn(buttonVariants({ variant, size, className }))}
128 ref={ref}
129 {...props}
130 />
131 )
132 }
133 )
134
135 export { Button, buttonVariants }`,
136 "src/components/ui/card.tsx": `import * as React from "react"
137 import { cn } from "@/lib/utils"
138
139 const Card = React.forwardRef<
140 HTMLDivElement,
141 React.HTMLAttributes<HTMLDivElement>
142 >(function Card({ className, ...props }, ref) {
143 return (
144 <div
145 ref={ref}
146 className={cn(
147 "rounded-lg border bg-card text-card-foreground shadow-sm",
148 className
149 )}
150 {...props}
151 />
152 )
153 })
154
155 const CardHeader = React.forwardRef<
156 HTMLDivElement,
157 React.HTMLAttributes<HTMLDivElement>
158 >(function CardHeader({ className, ...props }, ref) {
159 return (
160 <div
161 ref={ref}
162 className={cn("flex flex-col space-y-1.5 p-6", className)}
163 {...props}
164 />
165 )
166 })
167
168 const CardTitle = React.forwardRef<
169 HTMLParagraphElement,
170 React.HTMLAttributes<HTMLHeadingElement>
171 >(function CardTitle({ className, ...props }, ref) {
172 return (
173 <h3
174 ref={ref}
175 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
176 {...props}
177 />
178 )
179 })
180
181 export { Card, CardHeader, CardTitle }`,
182 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
183 // This is a simplified preview
184
185 import { cva } from "class-variance-authority"
186
187 export const fileTreeVariants = cva(
188 "font-mono text-sm select-none",
189 {
190 variants: {
191 variant: {
192 default: "bg-background text-foreground",
193 ghost: "bg-transparent",
194 bordered: "bg-background border border-border rounded-lg p-2",
195 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",
196 },
197 },
198 }
199 )`,
200 "src/components/header.tsx": `import Link from "next/link"
201 import { Button } from "@/components/ui/button"
202
203 export function Header() {
204 return (
205 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
206 <div className="container flex h-14 items-center">
207 <Link href="/" className="mr-6 flex items-center space-x-2">
208 <span className="font-bold">My App</span>
209 </Link>
210 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">
211 <Link href="/about">About</Link>
212 <Link href="/docs">Docs</Link>
213 </nav>
214 <Button size="sm">Sign In</Button>
215 </div>
216 </header>
217 )
218 }`,
219 "src/components/footer.tsx": `export function Footer() {
220 return (
221 <footer className="border-t py-6 md:py-0">
222 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
223 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">
224 Built with Next.js and Tailwind CSS.
225 </p>
226 </div>
227 </footer>
228 )
229 }`,
230 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"
231 import { twMerge } from "tailwind-merge"
232
233 export function cn(...inputs: ClassValue[]) {
234 return twMerge(clsx(inputs))
235 }`,
236 "src/lib/constants.ts": `export const APP_NAME = "My Application"
237 export const APP_VERSION = "1.0.0"
238 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"
239
240 export const ROUTES = {
241 home: "/",
242 about: "/about",
243 dashboard: "/dashboard",
244 settings: "/settings",
245 } as const`,
246 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"
247
248 interface User {
249 id: string
250 email: string
251 name: string
252 }
253
254 export function useAuth() {
255 const [user, setUser] = useState<User | null>(null)
256 const [loading, setLoading] = useState(true)
257
258 useEffect(() => {
259 // Check for existing session
260 const checkAuth = async () => {
261 try {
262 const response = await fetch("/api/auth/me")
263 if (response.ok) {
264 const userData = await response.json()
265 setUser(userData)
266 }
267 } catch (error) {
268 console.error("Auth check failed:", error)
269 } finally {
270 setLoading(false)
271 }
272 }
273 checkAuth()
274 }, [])
275
276 const signOut = useCallback(async () => {
277 await fetch("/api/auth/signout", { method: "POST" })
278 setUser(null)
279 }, [])
280
281 return { user, loading, signOut }
282 }`,
283 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
284
285 type Theme = "light" | "dark" | "system"
286
287 export function useTheme() {
288 const [theme, setTheme] = useState<Theme>("system")
289
290 useEffect(() => {
291 const stored = localStorage.getItem("theme") as Theme | null
292 if (stored) {
293 setTheme(stored)
294 }
295 }, [])
296
297 useEffect(() => {
298 const root = document.documentElement
299 root.classList.remove("light", "dark")
300
301 if (theme === "system") {
302 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
303 ? "dark"
304 : "light"
305 root.classList.add(systemTheme)
306 } else {
307 root.classList.add(theme)
308 }
309
310 localStorage.setItem("theme", theme)
311 }, [theme])
312
313 return { theme, setTheme }
314 }`,
315 "package.json": `{
316 "name": "my-nextjs-app",
317 "version": "0.1.0",
318 "private": true,
319 "scripts": {
320 "dev": "next dev",
321 "build": "next build",
322 "start": "next start",
323 "lint": "next lint"
324 },
325 "dependencies": {
326 "next": "14.2.0",
327 "react": "^18",
328 "react-dom": "^18",
329 "class-variance-authority": "^0.7.0",
330 "clsx": "^2.1.0",
331 "tailwind-merge": "^2.2.0",
332 "lucide-react": "^0.344.0"
333 },
334 "devDependencies": {
335 "typescript": "^5",
336 "@types/node": "^20",
337 "@types/react": "^18",
338 "@types/react-dom": "^18",
339 "tailwindcss": "^3.4.1",
340 "postcss": "^8"
341 }
342 }`,
343 "tsconfig.json": `{
344 "compilerOptions": {
345 "lib": ["dom", "dom.iterable", "esnext"],
346 "allowJs": true,
347 "skipLibCheck": true,
348 "strict": true,
349 "noEmit": true,
350 "esModuleInterop": true,
351 "module": "esnext",
352 "moduleResolution": "bundler",
353 "resolveJsonModule": true,
354 "isolatedModules": true,
355 "jsx": "preserve",
356 "incremental": true,
357 "plugins": [{ "name": "next" }],
358 "paths": {
359 "@/*": ["./*"]
360 }
361 },
362 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
363 "exclude": ["node_modules"]
364 }`,
365 "next.config.mjs": `/** @type {import('next').NextConfig} */
366 const nextConfig = {
367 reactStrictMode: true,
368 images: {
369 domains: [],
370 },
371 }
372
373 export default nextConfig`,
374 "tailwind.config.ts": `import type { Config } from "tailwindcss"
375
376 const config: Config = {
377 darkMode: ["class"],
378 content: [
379 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
380 "./components/**/*.{js,ts,jsx,tsx,mdx}",
381 "./app/**/*.{js,ts,jsx,tsx,mdx}",
382 ],
383 theme: {
384 extend: {
385 colors: {
386 border: "hsl(var(--border))",
387 background: "hsl(var(--background))",
388 foreground: "hsl(var(--foreground))",
389 },
390 },
391 },
392 plugins: [],
393 }
394 export default config`,
395 ".env.local": `# Environment Variables
396 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
397 NEXT_PUBLIC_API_URL="http://localhost:3000/api"
398 AUTH_SECRET="your-secret-key-here"`,
399 ".gitignore": `# Dependencies
400 node_modules
401 .pnp
402 .pnp.js
403
404 # Testing
405 coverage
406
407 # Next.js
408 .next/
409 out/
410
411 # Production
412 build
413
414 # Misc
415 .DS_Store
416 *.pem
417
418 # Debug
419 npm-debug.log*
420
421 # Local env files
422 .env*.local
423
424 # Vercel
425 .vercel
426
427 # TypeScript
428 *.tsbuildinfo
429 next-env.d.ts`,
430 "README.md": `# My Next.js App
431
432 A modern web application built with Next.js 14, React, and Tailwind CSS.
433
434 ## Getting Started
435
436 First, install dependencies:
437
438 \`\`\`bash
439 npm install
440 # or
441 yarn install
442 # or
443 pnpm install
444 \`\`\`
445
446 Then, run the development server:
447
448 \`\`\`bash
449 npm run dev
450 \`\`\`
451
452 Open [http://localhost:3000](http://localhost:3000) to see the result.
453
454 ## Features
455
456 - Next.js 14 App Router
457 - TypeScript
458 - Tailwind CSS
459 - Component library with CVA variants
460 `,
461};
462
463export function FileViewerStandalone() {
464 return (
465 <section className="grid gap-8 lg:grid-cols-2">
466 <div>
467 <FileViewer
468 file={{
469 path: "src/lib/utils.ts",
470 name: "utils.ts",
471 content: fileContents["src/lib/utils.ts"],
472 }}
473 variant="bordered"
474 size="default"
475 maxHeight="300px"
476 highlightedLines={{
477 4: "modified",
478 }}
479 />
480 </div>
481 <div>
482 <FileViewer
483 file={{
484 path: "package.json",
485 name: "package.json",
486 content: fileContents["package.json"],
487 }}
488 variant="elevated"
489 size="sm"
490 maxHeight="300px"
491 />
492 </div>
493 </section>
494 );
495}
With editor sidebar
Full workbench with activity rail, search, and extensions
Explorer
src
app
layout.tsx
page.tsx
globals.css
api
auth
route.ts
users
route.ts
components
ui
button.tsx
card.tsx
file-tree.tsx
header.tsx
footer.tsx
lib
utils.ts
constants.ts
hooks
use-auth.ts
use-theme.ts
public
favicon.ico
logo.svg
images
hero.png
avatar.jpg
package.json
tsconfig.json
next.config.mjs
tailwind.config.ts
.env.local
.gitignore
README.md

Select a file to view its contents

1"use client";
2
3import {
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";
39
40const fileContents: Record<string, string> = {
41 "src/app/layout.tsx": `import type { Metadata } from "next"
42import { Inter } from "next/font/google"
43import "./globals.css"
44
45const inter = Inter({ subsets: ["latin"] })
46
47export const metadata: Metadata = {
48 title: "My App",
49 description: "A Next.js application",
50}
51
52export default function RootLayout({
53 children,
54}: {
55 children: React.ReactNode
56}) {
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.tsx
69 </p>
70 </main>
71 )
72 }`,
73 "src/app/globals.css": `@tailwind base;
74 @tailwind components;
75 @tailwind utilities;
76
77 :root {
78 --foreground-rgb: 0, 0, 0;
79 --background-start-rgb: 214, 219, 220;
80 --background-end-rgb: 255, 255, 255;
81 }
82
83 @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"
91
92 export async function POST(request: Request) {
93 const body = await request.json()
94 const { email, password } = body
95
96 // Validate credentials
97 if (!email || !password) {
98 return NextResponse.json(
99 { error: "Missing credentials" },
100 { status: 400 }
101 )
102 }
103
104 // TODO: Implement actual authentication
105 return NextResponse.json({ success: true })
106 }`,
107 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"
108
109 const users = [
110 { id: 1, name: "Alice", email: "alice@example.com" },
111 { id: 2, name: "Bob", email: "bob@example.com" },
112 ]
113
114 export async function GET() {
115 return NextResponse.json(users)
116 }
117
118 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"
127
128 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 )
153
154 export interface ButtonProps
155 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
156 VariantProps<typeof buttonVariants> {}
157
158 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
159 function Button({ className, variant, size, ...props }, ref) {
160 return (
161 <button
162 className={cn(buttonVariants({ variant, size, className }))}
163 ref={ref}
164 {...props}
165 />
166 )
167 }
168 )
169
170 export { Button, buttonVariants }`,
171 "src/components/ui/card.tsx": `import * as React from "react"
172 import { cn } from "@/lib/utils"
173
174 const Card = React.forwardRef<
175 HTMLDivElement,
176 React.HTMLAttributes<HTMLDivElement>
177 >(function Card({ className, ...props }, ref) {
178 return (
179 <div
180 ref={ref}
181 className={cn(
182 "rounded-lg border bg-card text-card-foreground shadow-sm",
183 className
184 )}
185 {...props}
186 />
187 )
188 })
189
190 const CardHeader = React.forwardRef<
191 HTMLDivElement,
192 React.HTMLAttributes<HTMLDivElement>
193 >(function CardHeader({ className, ...props }, ref) {
194 return (
195 <div
196 ref={ref}
197 className={cn("flex flex-col space-y-1.5 p-6", className)}
198 {...props}
199 />
200 )
201 })
202
203 const CardTitle = React.forwardRef<
204 HTMLParagraphElement,
205 React.HTMLAttributes<HTMLHeadingElement>
206 >(function CardTitle({ className, ...props }, ref) {
207 return (
208 <h3
209 ref={ref}
210 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
211 {...props}
212 />
213 )
214 })
215
216 export { Card, CardHeader, CardTitle }`,
217 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
218 // This is a simplified preview
219
220 import { cva } from "class-variance-authority"
221
222 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"
237
238 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"
267
268 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"
274
275 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"
282
283 interface User {
284 id: string
285 email: string
286 name: string
287 }
288
289 export function useAuth() {
290 const [user, setUser] = useState<User | null>(null)
291 const [loading, setLoading] = useState(true)
292
293 useEffect(() => {
294 // Check for existing session
295 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 }, [])
310
311 const signOut = useCallback(async () => {
312 await fetch("/api/auth/signout", { method: "POST" })
313 setUser(null)
314 }, [])
315
316 return { user, loading, signOut }
317 }`,
318 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
319
320 type Theme = "light" | "dark" | "system"
321
322 export function useTheme() {
323 const [theme, setTheme] = useState<Theme>("system")
324
325 useEffect(() => {
326 const stored = localStorage.getItem("theme") as Theme | null
327 if (stored) {
328 setTheme(stored)
329 }
330 }, [])
331
332 useEffect(() => {
333 const root = document.documentElement
334 root.classList.remove("light", "dark")
335
336 if (theme === "system") {
337 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
338 ? "dark"
339 : "light"
340 root.classList.add(systemTheme)
341 } else {
342 root.classList.add(theme)
343 }
344
345 localStorage.setItem("theme", theme)
346 }, [theme])
347
348 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 }
407
408 export default nextConfig`,
409 "tailwind.config.ts": `import type { Config } from "tailwindcss"
410
411 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 Variables
431 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": `# Dependencies
435 node_modules
436 .pnp
437 .pnp.js
438
439 # Testing
440 coverage
441
442 # Next.js
443 .next/
444 out/
445
446 # Production
447 build
448
449 # Misc
450 .DS_Store
451 *.pem
452
453 # Debug
454 npm-debug.log*
455
456 # Local env files
457 .env*.local
458
459 # Vercel
460 .vercel
461
462 # TypeScript
463 *.tsbuildinfo
464 next-env.d.ts`,
465 "README.md": `# My Next.js App
466
467 A modern web application built with Next.js 14, React, and Tailwind CSS.
468
469 ## Getting Started
470
471 First, install dependencies:
472
473 \`\`\`bash
474 npm install
475 # or
476 yarn install
477 # or
478 pnpm install
479 \`\`\`
480
481 Then, run the development server:
482
483 \`\`\`bash
484 npm run dev
485 \`\`\`
486
487 Open [http://localhost:3000](http://localhost:3000) to see the result.
488
489 ## Features
490
491 - Next.js 14 App Router
492 - TypeScript
493 - Tailwind CSS
494 - Component library with CVA variants
495 `,
496};
497
498const 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];
587
588const 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};
596
597type OpenFileTab = FileContent & { id: string };
598
599function getTabFileIcon(filename: string) {
600 const iconClass = "shrink-0";
601 const ext = filename.split(".").pop()?.toLowerCase();
602
603 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 };
617
618 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 }
627
628 return (
629 iconMap[ext || ""] || (
630 <File className={cn(iconClass, "text-muted-foreground")} size={14} />
631 )
632 );
633}
634
635function buildFileContent(node: FileTreeNode, path: string): FileContent {
636 const content = fileContents[path];
637 const previewUrl = imagePreviewUrls[path];
638
639 if (content) {
640 return { path, name: node.name, content, previewUrl };
641 }
642
643 return {
644 path,
645 name: node.name,
646 content: previewUrl
647 ? ""
648 : `// Content for ${node.name} not available in demo`,
649 previewUrl,
650 };
651}
652
653type FlatFileEntry = {
654 path: string;
655 name: string;
656 node: FileTreeNode;
657};
658
659function flattenFilePaths(nodes: FileTreeNode[], prefix = ""): FlatFileEntry[] {
660 const entries: FlatFileEntry[] = [];
661
662 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 }
670
671 return entries;
672}
673
674const 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];
700
701export 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);
708
709 const activeFile = useMemo(
710 () => tabs.find((tab) => tab.id === activeId) ?? null,
711 [tabs, activeId],
712 );
713
714 const allFiles = useMemo(() => flattenFilePaths(sampleData), []);
715
716 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]);
725
726 const handleSelect = (node: FileTreeNode, path: string) => {
727 if (node.type !== "file") return;
728
729 const file = buildFileContent(node, path);
730 const tab: OpenFileTab = { ...file, id: path };
731
732 if (tabs.some((item) => item.id === path)) {
733 focusTab(path);
734 return;
735 }
736
737 openTab(tab);
738 };
739
740 const handleSearchSelect = (entry: FlatFileEntry) => {
741 handleSelect(entry.node, entry.path);
742 setSidebarView("explorer");
743 };
744
745 return (
746 <div className="w-full">
747 <FileExplorer variant="elevated" rounded="lg" className="h-[600px]">
748 <EditorSidebar
749 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 <EditorSidebarTrigger
757 value="explorer"
758 icon={<Files />}
759 label="Explorer"
760 />
761 <EditorSidebarTrigger
762 value="search"
763 icon={<Search />}
764 label="Search"
765 />
766 <EditorSidebarTrigger
767 value="extensions"
768 icon={<Blocks />}
769 label="Extensions"
770 badge
771 />
772 </div>
773 <Button
774 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 explorerPanelOpen
781 ? "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 <div
795 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 <EditorSidebarContent
802 width="260px"
803 className="h-full min-h-0 min-w-[260px] flex-none"
804 >
805 <EditorSidebarPanel value="explorer" title="Explorer">
806 <FileTree
807 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 <input
817 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 <button
832 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 <li
851 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 <EditorTabs
870 value={activeId}
871 onValueChange={setActiveId}
872 variant="ghost"
873 className="h-full"
874 >
875 {tabs.length > 0 ? (
876 <EditorTabsList>
877 {tabs.map((tab) => (
878 <EditorTab
879 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 <FileViewer
891 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/file-viewer

Props

NameTypeDefaultDescription
fileFileContentundefinedThe file to display
showLineNumbersbooleantrueShow line numbers
highlightedLinesRecord<number, string>{}Highlighted lines
onClose() => voidundefinedCallback when the file viewer is closed
showHeaderbooleantrueShow the header
maxHeightstring100%The maximum height of the file viewer
emptyMessagestringSelect a file to view its contentsThe message to display when no file is selected
collapsible (FileExplorer)booleanfalseWhen true, the explorer sidebar can collapse to give the viewer full width
defaultSidebarOpen (FileExplorer)booleantrueInitial open state when collapsible is enabled
sidebarOpen (FileExplorer)booleanundefinedControlled sidebar open state
onSidebarOpenChange (FileExplorer)(open: boolean) => voidundefinedCalled when the collapsible sidebar opens or closes
sidebarWidth (FileExplorer)string280pxWidth of the explorer sidebar when expanded