Browse Folder

PreviousNext

Folder browser with animated tabs and content (Base UI)

Docs
uitripledcomponent

Preview

Loading preview…
components/sections/baseui/browse-folder-baseui.tsx
"use client";

import { Button } from "@base-ui/react/button";
import { motion } from "framer-motion";
import { ChevronRight, Folder } from "lucide-react";
import type React from "react";
import { useState } from "react";

interface FolderItem {
  name: string;
  content: React.ReactNode;
}

interface FolderBrowserBaseuiProps {
  folders?: FolderItem[];
  initialFolder?: string;
}

export function FolderBrowserBaseui({
  folders = [],
  initialFolder,
}: FolderBrowserBaseuiProps) {
  const [activeTab, setActiveTab] = useState(() => {
    if (initialFolder) return initialFolder;
    if (folders && folders.length > 0 && folders[0]?.name) {
      return folders[0].name;
    }
    return "";
  });

  const activeFolder = folders.find((f) => f.name === activeTab);

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4 }}
      className="group relative overflow-hidden rounded-2xl border border-border/40 bg-background/60 backdrop-blur transition-all hover:border-border/60 hover:shadow-lg"
      role="region"
      aria-label="Folder browser"
    >
      {/* Gradient overlay */}
      <div className="absolute -z-10 inset-0 bg-gradient-to-br from-foreground/[0.04] via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />

      {/* Content */}
      <div className="relative">
        {/* Tab List */}
        <div
          role="tablist"
          className="relative flex h-10 w-full items-end justify-start overflow-x-auto overflow-y-hidden border-b border-border/40 bg-transparent p-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
        >
          {folders?.map((folder) => (
            <button
              key={folder.name}
              role="tab"
              type="button"
              aria-selected={activeTab === folder.name}
              onClick={() => setActiveTab(folder.name)}
              className={`group/trigger relative flex h-10 flex-shrink-0 items-center gap-2 px-3 text-xs font-medium transition-all sm:px-4 sm:text-sm hover:text-foreground ${
                activeTab === folder.name
                  ? "h-[calc(2.5rem+1px)] rounded-t-lg border border-border/40 border-b-transparent bg-background/60 text-foreground shadow-sm"
                  : "text-foreground/70"
              }`}
            >
              <Folder
                className="h-4 w-4 text-foreground/40 group-hover/trigger:text-foreground/70"
                aria-hidden="true"
              />
              {folder.name}
              <ChevronRight
                className="h-4 w-4 text-foreground/40 opacity-0 transition-opacity group-hover/trigger:opacity-100"
                aria-hidden="true"
              />
            </button>
          ))}
        </div>

        {/* Tab Panel */}
        {activeFolder && (
          <div role="tabpanel" className="p-6">
            <motion.div
              key={activeFolder.name}
              initial={{ opacity: 0, y: 10 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ duration: 0.3 }}
              className="space-y-4"
            >
              <div className="flex items-center justify-between">
                <h3 className="text-2xl font-semibold tracking-tight text-foreground">
                  {activeFolder.name} Contents
                </h3>
                <span className="inline-flex items-center rounded-full border border-border/50 bg-background/55 px-3 py-1 text-xs uppercase tracking-[0.2em] text-foreground/70 backdrop-blur">
                  {activeFolder.name.split(" ").pop()}
                </span>
              </div>
              <div className="text-foreground/70">{activeFolder.content}</div>
              <Button
                className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
                aria-label={`Explore ${activeFolder.name}`}
              >
                Explore More
                <ChevronRight className="h-4 w-4" aria-hidden="true" />
              </Button>
            </motion.div>
          </div>
        )}
      </div>
    </motion.div>
  );
}

// Example usage
export const BrowseFolderBaseui = () => {
  const sampleFolders: FolderItem[] = [
    {
      name: "Documents",
      content: (
        <ul className="list-disc space-y-2 pl-4">
          <li>Report.pdf</li>
          <li>Notes.txt</li>
          <li>Spreadsheet.xlsx</li>
        </ul>
      ),
    },
    {
      name: "Images",
      content: (
        <ul className="list-disc space-y-2 pl-4">
          <li>Photo1.jpg</li>
          <li>Photo2.png</li>
          <li>Graphic.svg</li>
        </ul>
      ),
    },
    {
      name: "Projects",
      content: (
        <ul className="list-disc space-y-2 pl-4">
          <li>App.zip</li>
          <li>Design.fig</li>
          <li>Code.ts</li>
        </ul>
      ),
    },
  ];

  return (
    <FolderBrowserBaseui folders={sampleFolders} initialFolder="Documents" />
  );
};

Installation

npx shadcn@latest add @uitripled/browse-folder-baseui

Usage

import { BrowseFolderBaseui } from "@/components/browse-folder-baseui"
<BrowseFolderBaseui />