Search

Search the site

All components

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
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 ({ 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 Button.displayName = "Button"
160
161 export { Button, buttonVariants }`,
162 "src/components/ui/card.tsx": `import * as React from "react"
163 import { cn } from "@/lib/utils"
164
165 const Card = React.forwardRef<
166 HTMLDivElement,
167 React.HTMLAttributes<HTMLDivElement>
168 >(({ className, ...props }, ref) => (
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 Card.displayName = "Card"
179
180 const CardHeader = React.forwardRef<
181 HTMLDivElement,
182 React.HTMLAttributes<HTMLDivElement>
183 >(({ className, ...props }, ref) => (
184 <div
185 ref={ref}
186 className={cn("flex flex-col space-y-1.5 p-6", className)}
187 {...props}
188 />
189 ))
190 CardHeader.displayName = "CardHeader"
191
192 const CardTitle = React.forwardRef<
193 HTMLParagraphElement,
194 React.HTMLAttributes<HTMLHeadingElement>
195 >(({ className, ...props }, ref) => (
196 <h3
197 ref={ref}
198 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
199 {...props}
200 />
201 ))
202 CardTitle.displayName = "CardTitle"
203
204 export { Card, CardHeader, CardTitle }`,
205 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
206 // This is a simplified preview
207
208 import { cva } from "class-variance-authority"
209
210 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"
225
226 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"
255
256 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"
262
263 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"
270
271 interface User {
272 id: string
273 email: string
274 name: string
275 }
276
277 export function useAuth() {
278 const [user, setUser] = useState<User | null>(null)
279 const [loading, setLoading] = useState(true)
280
281 useEffect(() => {
282 // Check for existing session
283 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 }, [])
298
299 const signOut = useCallback(async () => {
300 await fetch("/api/auth/signout", { method: "POST" })
301 setUser(null)
302 }, [])
303
304 return { user, loading, signOut }
305 }`,
306 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
307
308 type Theme = "light" | "dark" | "system"
309
310 export function useTheme() {
311 const [theme, setTheme] = useState<Theme>("system")
312
313 useEffect(() => {
314 const stored = localStorage.getItem("theme") as Theme | null
315 if (stored) {
316 setTheme(stored)
317 }
318 }, [])
319
320 useEffect(() => {
321 const root = document.documentElement
322 root.classList.remove("light", "dark")
323
324 if (theme === "system") {
325 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
326 ? "dark"
327 : "light"
328 root.classList.add(systemTheme)
329 } else {
330 root.classList.add(theme)
331 }
332
333 localStorage.setItem("theme", theme)
334 }, [theme])
335
336 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 }
395
396 export default nextConfig`,
397 "tailwind.config.ts": `import type { Config } from "tailwindcss"
398
399 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 Variables
419 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": `# Dependencies
423 node_modules
424 .pnp
425 .pnp.js
426
427 # Testing
428 coverage
429
430 # Next.js
431 .next/
432 out/
433
434 # Production
435 build
436
437 # Misc
438 .DS_Store
439 *.pem
440
441 # Debug
442 npm-debug.log*
443
444 # Local env files
445 .env*.local
446
447 # Vercel
448 .vercel
449
450 # TypeScript
451 *.tsbuildinfo
452 next-env.d.ts`,
453 "README.md": `# My Next.js App
454
455 A modern web application built with Next.js 14, React, and Tailwind CSS.
456
457 ## Getting Started
458
459 First, install dependencies:
460
461 \`\`\`bash
462 npm install
463 # or
464 yarn install
465 # or
466 pnpm install
467 \`\`\`
468
469 Then, run the development server:
470
471 \`\`\`bash
472 npm run dev
473 \`\`\`
474
475 Open [http://localhost:3000](http://localhost:3000) to see the result.
476
477 ## Features
478
479 - Next.js 14 App Router
480 - TypeScript
481 - Tailwind CSS
482 - Component library with CVA variants
483 `,
484};
485
486const 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];
575
576const 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};
584
585export function FileViewerDefault() {
586 const [selectedPath, setSelectedPath] = useState<string>("");
587 const [selectedFile, setSelectedFile] = useState<FileContent | null>(null);
588
589 const handleSelect = (node: FileTreeNode, path: string) => {
590 setSelectedPath(path);
591
592 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: previewUrl
607 ? ""
608 : `// Content for ${node.name} not available in demo`,
609 previewUrl,
610 });
611 }
612 }
613 };
614
615 const handleClose = () => {
616 setSelectedFile(null);
617 setSelectedPath("");
618 };
619 return (
620 <div className="w-full">
621 <FileExplorer
622 variant="elevated"
623 rounded="lg"
624 sidebarWidth="300px"
625 collapsible
626 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 Explorer
632 </span>
633 <FileExplorerSidebarToggleWhen when="open" />
634 </div>
635 <FileTree
636 data={sampleData}
637 variant="ghost"
638 size="default"
639 onSelect={handleSelect}
640 selectedPath={selectedPath}
641 />
642 </FileExplorerSidebar>
643 <FileExplorerContent>
644 <FileExplorerSidebarToggleWhen
645 when="closed"
646 className="absolute left-2 top-2 z-10"
647 />
648 <FileViewer
649 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"
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 ({ 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 Button.displayName = "Button"
135
136 export { Button, buttonVariants }`,
137 "src/components/ui/card.tsx": `import * as React from "react"
138 import { cn } from "@/lib/utils"
139
140 const Card = React.forwardRef<
141 HTMLDivElement,
142 React.HTMLAttributes<HTMLDivElement>
143 >(({ className, ...props }, ref) => (
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 Card.displayName = "Card"
154
155 const CardHeader = React.forwardRef<
156 HTMLDivElement,
157 React.HTMLAttributes<HTMLDivElement>
158 >(({ className, ...props }, ref) => (
159 <div
160 ref={ref}
161 className={cn("flex flex-col space-y-1.5 p-6", className)}
162 {...props}
163 />
164 ))
165 CardHeader.displayName = "CardHeader"
166
167 const CardTitle = React.forwardRef<
168 HTMLParagraphElement,
169 React.HTMLAttributes<HTMLHeadingElement>
170 >(({ className, ...props }, ref) => (
171 <h3
172 ref={ref}
173 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
174 {...props}
175 />
176 ))
177 CardTitle.displayName = "CardTitle"
178
179 export { Card, CardHeader, CardTitle }`,
180 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
181 // This is a simplified preview
182
183 import { cva } from "class-variance-authority"
184
185 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"
200
201 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"
230
231 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"
237
238 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"
245
246 interface User {
247 id: string
248 email: string
249 name: string
250 }
251
252 export function useAuth() {
253 const [user, setUser] = useState<User | null>(null)
254 const [loading, setLoading] = useState(true)
255
256 useEffect(() => {
257 // Check for existing session
258 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 }, [])
273
274 const signOut = useCallback(async () => {
275 await fetch("/api/auth/signout", { method: "POST" })
276 setUser(null)
277 }, [])
278
279 return { user, loading, signOut }
280 }`,
281 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
282
283 type Theme = "light" | "dark" | "system"
284
285 export function useTheme() {
286 const [theme, setTheme] = useState<Theme>("system")
287
288 useEffect(() => {
289 const stored = localStorage.getItem("theme") as Theme | null
290 if (stored) {
291 setTheme(stored)
292 }
293 }, [])
294
295 useEffect(() => {
296 const root = document.documentElement
297 root.classList.remove("light", "dark")
298
299 if (theme === "system") {
300 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
301 ? "dark"
302 : "light"
303 root.classList.add(systemTheme)
304 } else {
305 root.classList.add(theme)
306 }
307
308 localStorage.setItem("theme", theme)
309 }, [theme])
310
311 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 }
370
371 export default nextConfig`,
372 "tailwind.config.ts": `import type { Config } from "tailwindcss"
373
374 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 Variables
394 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": `# Dependencies
398 node_modules
399 .pnp
400 .pnp.js
401
402 # Testing
403 coverage
404
405 # Next.js
406 .next/
407 out/
408
409 # Production
410 build
411
412 # Misc
413 .DS_Store
414 *.pem
415
416 # Debug
417 npm-debug.log*
418
419 # Local env files
420 .env*.local
421
422 # Vercel
423 .vercel
424
425 # TypeScript
426 *.tsbuildinfo
427 next-env.d.ts`,
428 "README.md": `# My Next.js App
429
430 A modern web application built with Next.js 14, React, and Tailwind CSS.
431
432 ## Getting Started
433
434 First, install dependencies:
435
436 \`\`\`bash
437 npm install
438 # or
439 yarn install
440 # or
441 pnpm install
442 \`\`\`
443
444 Then, run the development server:
445
446 \`\`\`bash
447 npm run dev
448 \`\`\`
449
450 Open [http://localhost:3000](http://localhost:3000) to see the result.
451
452 ## Features
453
454 - Next.js 14 App Router
455 - TypeScript
456 - Tailwind CSS
457 - Component library with CVA variants
458 `,
459};
460
461export function FileViewerStandalone() {
462 return (
463 <section className="grid gap-8 lg:grid-cols-2">
464 <div>
465 <FileViewer
466 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 <FileViewer
481 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
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 ({ 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 Button.displayName = "Button"
170
171 export { Button, buttonVariants }`,
172 "src/components/ui/card.tsx": `import * as React from "react"
173 import { cn } from "@/lib/utils"
174
175 const Card = React.forwardRef<
176 HTMLDivElement,
177 React.HTMLAttributes<HTMLDivElement>
178 >(({ className, ...props }, ref) => (
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 Card.displayName = "Card"
189
190 const CardHeader = React.forwardRef<
191 HTMLDivElement,
192 React.HTMLAttributes<HTMLDivElement>
193 >(({ className, ...props }, ref) => (
194 <div
195 ref={ref}
196 className={cn("flex flex-col space-y-1.5 p-6", className)}
197 {...props}
198 />
199 ))
200 CardHeader.displayName = "CardHeader"
201
202 const CardTitle = React.forwardRef<
203 HTMLParagraphElement,
204 React.HTMLAttributes<HTMLHeadingElement>
205 >(({ className, ...props }, ref) => (
206 <h3
207 ref={ref}
208 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
209 {...props}
210 />
211 ))
212 CardTitle.displayName = "CardTitle"
213
214 export { Card, CardHeader, CardTitle }`,
215 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
216 // This is a simplified preview
217
218 import { cva } from "class-variance-authority"
219
220 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"
235
236 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"
265
266 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"
272
273 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"
280
281 interface User {
282 id: string
283 email: string
284 name: string
285 }
286
287 export function useAuth() {
288 const [user, setUser] = useState<User | null>(null)
289 const [loading, setLoading] = useState(true)
290
291 useEffect(() => {
292 // Check for existing session
293 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 }, [])
308
309 const signOut = useCallback(async () => {
310 await fetch("/api/auth/signout", { method: "POST" })
311 setUser(null)
312 }, [])
313
314 return { user, loading, signOut }
315 }`,
316 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
317
318 type Theme = "light" | "dark" | "system"
319
320 export function useTheme() {
321 const [theme, setTheme] = useState<Theme>("system")
322
323 useEffect(() => {
324 const stored = localStorage.getItem("theme") as Theme | null
325 if (stored) {
326 setTheme(stored)
327 }
328 }, [])
329
330 useEffect(() => {
331 const root = document.documentElement
332 root.classList.remove("light", "dark")
333
334 if (theme === "system") {
335 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
336 ? "dark"
337 : "light"
338 root.classList.add(systemTheme)
339 } else {
340 root.classList.add(theme)
341 }
342
343 localStorage.setItem("theme", theme)
344 }, [theme])
345
346 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 }
405
406 export default nextConfig`,
407 "tailwind.config.ts": `import type { Config } from "tailwindcss"
408
409 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 Variables
429 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": `# Dependencies
433 node_modules
434 .pnp
435 .pnp.js
436
437 # Testing
438 coverage
439
440 # Next.js
441 .next/
442 out/
443
444 # Production
445 build
446
447 # Misc
448 .DS_Store
449 *.pem
450
451 # Debug
452 npm-debug.log*
453
454 # Local env files
455 .env*.local
456
457 # Vercel
458 .vercel
459
460 # TypeScript
461 *.tsbuildinfo
462 next-env.d.ts`,
463 "README.md": `# My Next.js App
464
465 A modern web application built with Next.js 14, React, and Tailwind CSS.
466
467 ## Getting Started
468
469 First, install dependencies:
470
471 \`\`\`bash
472 npm install
473 # or
474 yarn install
475 # or
476 pnpm install
477 \`\`\`
478
479 Then, run the development server:
480
481 \`\`\`bash
482 npm run dev
483 \`\`\`
484
485 Open [http://localhost:3000](http://localhost:3000) to see the result.
486
487 ## Features
488
489 - Next.js 14 App Router
490 - TypeScript
491 - Tailwind CSS
492 - Component library with CVA variants
493 `,
494};
495
496const 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];
585
586const 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};
594
595type OpenFileTab = FileContent & { id: string };
596
597function getTabFileIcon(filename: string) {
598 const iconClass = "shrink-0";
599 const ext = filename.split(".").pop()?.toLowerCase();
600
601 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 };
615
616 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 }
625
626 return (
627 iconMap[ext || ""] || (
628 <File className={cn(iconClass, "text-muted-foreground")} size={14} />
629 )
630 );
631}
632
633function buildFileContent(node: FileTreeNode, path: string): FileContent {
634 const content = fileContents[path];
635 const previewUrl = imagePreviewUrls[path];
636
637 if (content) {
638 return { path, name: node.name, content, previewUrl };
639 }
640
641 return {
642 path,
643 name: node.name,
644 content: previewUrl
645 ? ""
646 : `// Content for ${node.name} not available in demo`,
647 previewUrl,
648 };
649}
650
651type FlatFileEntry = {
652 path: string;
653 name: string;
654 node: FileTreeNode;
655};
656
657function flattenFilePaths(nodes: FileTreeNode[], prefix = ""): FlatFileEntry[] {
658 const entries: FlatFileEntry[] = [];
659
660 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 }
668
669 return entries;
670}
671
672const 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];
698
699export 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);
706
707 const activeFile = useMemo(
708 () => tabs.find((tab) => tab.id === activeId) ?? null,
709 [tabs, activeId],
710 );
711
712 const allFiles = useMemo(() => flattenFilePaths(sampleData), []);
713
714 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]);
723
724 const handleSelect = (node: FileTreeNode, path: string) => {
725 if (node.type !== "file") return;
726
727 const file = buildFileContent(node, path);
728 const tab: OpenFileTab = { ...file, id: path };
729
730 if (tabs.some((item) => item.id === path)) {
731 focusTab(path);
732 return;
733 }
734
735 openTab(tab);
736 };
737
738 const handleSearchSelect = (entry: FlatFileEntry) => {
739 handleSelect(entry.node, entry.path);
740 setSidebarView("explorer");
741 };
742
743 return (
744 <div className="w-full">
745 <FileExplorer variant="elevated" rounded="lg" className="h-[600px]">
746 <EditorSidebar
747 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 <EditorSidebarTrigger
755 value="explorer"
756 icon={<Files />}
757 label="Explorer"
758 />
759 <EditorSidebarTrigger
760 value="search"
761 icon={<Search />}
762 label="Search"
763 />
764 <EditorSidebarTrigger
765 value="extensions"
766 icon={<Blocks />}
767 label="Extensions"
768 badge
769 />
770 </div>
771 <Button
772 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 explorerPanelOpen
779 ? "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 <div
793 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 <EditorSidebarContent
800 width="260px"
801 className="h-full min-h-0 min-w-[260px] flex-none"
802 >
803 <EditorSidebarPanel value="explorer" title="Explorer">
804 <FileTree
805 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 <input
815 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 <button
830 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 <li
849 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 <EditorTabs
868 value={activeId}
869 onValueChange={setActiveId}
870 variant="ghost"
871 className="h-full"
872 >
873 {tabs.length > 0 ? (
874 <EditorTabsList>
875 {tabs.map((tab) => (
876 <EditorTab
877 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 <FileViewer
889 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

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