Web InnoventixFreeCode

File Manager Breadcrumb

Original · free

file path breadcrumb with toolbar

byWeb InnoventixReact + Tailwind
filemgrbreadcrumbfilemanager
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/filemgr-breadcrumb.json
filemgr-breadcrumb.tsx
"use client";

import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Crumb = {
  id: string;
  name: string;
  itemCount: number;
};

type ViewMode = "grid" | "list";
type SortKey = "name" | "modified" | "size";

const FULL_PATH: Crumb[] = [
  { id: "root", name: "Workspace", itemCount: 14 },
  { id: "clients", name: "Clients", itemCount: 9 },
  { id: "northwind", name: "Northwind Rail", itemCount: 6 },
  { id: "2026", name: "2026", itemCount: 4 },
  { id: "q3-brand-refresh", name: "Q3 Brand Refresh", itemCount: 12 },
  { id: "exports", name: "Exports", itemCount: 5 },
  { id: "final-masters", name: "Final Masters", itemCount: 3 },
];

const SIBLINGS: Record<string, string[]> = {
  root: ["Clients", "Archive", "Templates", "Shared with me"],
  clients: ["Northwind Rail", "Halcyon Foods", "Meridian Health"],
  northwind: ["2026", "2025", "Contracts"],
  "2026": ["Q3 Brand Refresh", "Q2 Site Rebuild", "Q1 Discovery"],
  "q3-brand-refresh": ["Exports", "Source Files", "Feedback"],
  exports: ["Final Masters", "Web Optimised", "Print Ready"],
  "final-masters": ["northwind-logo-master.ai", "typeface-specimen.pdf", "colour-tokens.json"],
};

const COLLAPSE_AT = 4;

function ChevronRight() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5">
      <path
        d="M9 5l7 7-7 7"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function FolderIcon({ open }: { open: boolean }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 shrink-0">
      {open ? (
        <path
          d="M3 7.5A1.5 1.5 0 014.5 6h4.2l1.8 2h6A1.5 1.5 0 0118 9.5v.5H7.6a2 2 0 00-1.94 1.515L4 18V7.5z"
          stroke="currentColor"
          strokeWidth="1.6"
          strokeLinejoin="round"
        />
      ) : (
        <path
          d="M3 7.5A1.5 1.5 0 014.5 6h4.2l1.8 2h8A1.5 1.5 0 0120 9.5v8a1.5 1.5 0 01-1.5 1.5h-14A1.5 1.5 0 013 17.5v-10z"
          stroke="currentColor"
          strokeWidth="1.6"
          strokeLinejoin="round"
        />
      )}
      {open ? (
        <path
          d="M4 18l1.66-6.485A2 2 0 017.6 10H20.4a1 1 0 01.97 1.243L19.9 17.5A2 2 0 0117.96 19H4z"
          stroke="currentColor"
          strokeWidth="1.6"
          strokeLinejoin="round"
        />
      ) : null}
    </svg>
  );
}

function EllipsisIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-4 w-4">
      <circle cx="5.5" cy="12" r="1.6" />
      <circle cx="12" cy="12" r="1.6" />
      <circle cx="18.5" cy="12" r="1.6" />
    </svg>
  );
}

function ArrowIcon({ dir }: { dir: "left" | "right" | "up" }) {
  const d =
    dir === "left"
      ? "M15 5l-7 7 7 7"
      : dir === "right"
        ? "M9 5l7 7-7 7"
        : "M12 19V5m0 0l-6 6m6-6l6 6";
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path d={d} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function GridIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <rect x="4" y="4" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
      <rect x="13.5" y="4" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
      <rect x="4" y="13.5" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
      <rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
    </svg>
  );
}

function ListIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path
        d="M4.5 6.5h15M4.5 12h15M4.5 17.5h15"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
      />
    </svg>
  );
}

function SortIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path
        d="M7 4v16m0 0l-3-3m3 3l3-3M17 20V4m0 0l-3 3m3-3l3 3"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function CopyIcon({ done }: { done: boolean }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      {done ? (
        <path
          d="M5 12.5l4.5 4.5L19 7"
          stroke="currentColor"
          strokeWidth="1.9"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      ) : (
        <>
          <rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.7" />
          <path
            d="M15 6.5A2.5 2.5 0 0012.5 4h-6A2.5 2.5 0 004 6.5v6A2.5 2.5 0 006.5 15"
            stroke="currentColor"
            strokeWidth="1.7"
            strokeLinecap="round"
          />
        </>
      )}
    </svg>
  );
}

function UploadIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path
        d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export default function FilemgrBreadcrumb() {
  const reduceMotion = useReducedMotion();
  const uid = useId().replace(/[^a-zA-Z0-9]/g, "");

  const [depth, setDepth] = useState<number>(FULL_PATH.length);
  const [history, setHistory] = useState<number[]>([FULL_PATH.length]);
  const [historyIndex, setHistoryIndex] = useState<number>(0);
  const [openMenu, setOpenMenu] = useState<string | null>(null);
  const [view, setView] = useState<ViewMode>("list");
  const [sort, setSort] = useState<SortKey>("modified");
  const [copied, setCopied] = useState<boolean>(false);
  const [announcement, setAnnouncement] = useState<string>("");

  const wrapRef = useRef<HTMLDivElement | null>(null);
  const copyTimer = useRef<number | null>(null);

  const trail = useMemo<Crumb[]>(() => FULL_PATH.slice(0, depth), [depth]);
  const current = trail[trail.length - 1];

  const pathString = useMemo(() => "/" + trail.map((c) => c.name).join("/"), [trail]);

  const navigate = useCallback(
    (nextDepth: number) => {
      if (nextDepth === depth) return;
      setHistory((prev) => {
        const truncated = prev.slice(0, historyIndex + 1);
        return [...truncated, nextDepth];
      });
      setHistoryIndex((i) => i + 1);
      setDepth(nextDepth);
      setOpenMenu(null);
      setAnnouncement(`Opened ${FULL_PATH[nextDepth - 1].name}`);
    },
    [depth, historyIndex],
  );

  const goBack = useCallback(() => {
    if (historyIndex <= 0) return;
    const next = historyIndex - 1;
    setHistoryIndex(next);
    setDepth(history[next]);
    setOpenMenu(null);
    setAnnouncement(`Back to ${FULL_PATH[history[next] - 1].name}`);
  }, [history, historyIndex]);

  const goForward = useCallback(() => {
    if (historyIndex >= history.length - 1) return;
    const next = historyIndex + 1;
    setHistoryIndex(next);
    setDepth(history[next]);
    setOpenMenu(null);
    setAnnouncement(`Forward to ${FULL_PATH[history[next] - 1].name}`);
  }, [history, historyIndex]);

  const goUp = useCallback(() => {
    if (depth <= 1) return;
    navigate(depth - 1);
  }, [depth, navigate]);

  const copyPath = useCallback(() => {
    const write = navigator.clipboard?.writeText(pathString);
    const finish = () => {
      setCopied(true);
      setAnnouncement(`Path copied: ${pathString}`);
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
      copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
    };
    if (write) {
      write.then(finish).catch(finish);
    } else {
      finish();
    }
  }, [pathString]);

  useEffect(() => {
    return () => {
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
    };
  }, []);

  useEffect(() => {
    if (!openMenu) return;
    const onDown = (e: MouseEvent) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpenMenu(null);
    };
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") setOpenMenu(null);
    };
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [openMenu]);

  const collapsed = trail.length > COLLAPSE_AT;
  const head = collapsed ? trail.slice(0, 1) : trail;
  const hidden = collapsed ? trail.slice(1, trail.length - 2) : [];
  const tail = collapsed ? trail.slice(trail.length - 2) : [];

  const fade = reduceMotion
    ? { initial: false as const, animate: {}, exit: {} }
    : {
        initial: { opacity: 0, y: -4, scale: 0.98 },
        animate: { opacity: 1, y: 0, scale: 1 },
        exit: { opacity: 0, y: -4, scale: 0.98 },
      };

  const btn =
    "inline-flex items-center justify-center gap-1.5 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950";

  const crumbBtn =
    "inline-flex max-w-[10rem] items-center gap-1.5 truncate rounded-md px-2 py-1 text-sm text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950";

  const menuItem =
    "flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:bg-slate-100 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:focus-visible:bg-zinc-800 dark:focus-visible:ring-indigo-400";

  const siblingsOf = (crumbId: string, index: number): string[] => {
    const parentId = index === 0 ? null : FULL_PATH[index - 1].id;
    return parentId ? (SIBLINGS[parentId] ?? []) : [];
  };

  const renderCrumb = (crumb: Crumb, absoluteIndex: number, isLast: boolean) => {
    const menuId = `${uid}-menu-${crumb.id}`;
    const opts = siblingsOf(crumb.id, absoluteIndex);
    const isOpen = openMenu === crumb.id;

    return (
      <li key={crumb.id} className="flex shrink-0 items-center">
        <div className="relative flex items-center">
          {isLast ? (
            <span
              aria-current="page"
              className="inline-flex max-w-[12rem] items-center gap-1.5 truncate rounded-md bg-indigo-50 px-2 py-1 text-sm font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/30"
            >
              <FolderIcon open />
              <span className="truncate">{crumb.name}</span>
            </span>
          ) : (
            <button
              type="button"
              onClick={() => navigate(absoluteIndex + 1)}
              className={crumbBtn}
              title={`Open ${crumb.name}`}
            >
              <FolderIcon open={false} />
              <span className="truncate">{crumb.name}</span>
            </button>
          )}

          {opts.length > 0 && !isLast ? (
            <>
              <button
                type="button"
                aria-label={`Sibling folders of ${crumb.name}`}
                aria-haspopup="menu"
                aria-expanded={isOpen}
                aria-controls={isOpen ? menuId : undefined}
                onClick={() => setOpenMenu(isOpen ? null : crumb.id)}
                className="ml-0.5 inline-flex h-6 w-5 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 dark:focus-visible:ring-indigo-400"
              >
                <ChevronRight />
              </button>
              <AnimatePresence>
                {isOpen ? (
                  <motion.div
                    id={menuId}
                    role="menu"
                    aria-label={`Folders beside ${crumb.name}`}
                    {...fade}
                    transition={{ duration: 0.14, ease: "easeOut" }}
                    className="absolute left-0 top-full z-30 mt-1.5 w-52 rounded-xl border border-slate-200 bg-white p-1 shadow-xl shadow-slate-900/10 dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-black/40"
                  >
                    {opts.map((name) => {
                      const isCurrentBranch = FULL_PATH[absoluteIndex]?.name === name;
                      return (
                        <button
                          key={name}
                          type="button"
                          role="menuitem"
                          onClick={() => {
                            if (isCurrentBranch) {
                              navigate(absoluteIndex + 2);
                            } else {
                              setOpenMenu(null);
                              setAnnouncement(`${name} is empty in this demo path`);
                            }
                          }}
                          className={menuItem}
                        >
                          <span className="text-slate-400 dark:text-zinc-500">
                            <FolderIcon open={false} />
                          </span>
                          <span className="truncate">{name}</span>
                          {isCurrentBranch ? (
                            <span className="ml-auto text-[10px] font-semibold uppercase tracking-wide text-indigo-600 dark:text-indigo-400">
                              open
                            </span>
                          ) : null}
                        </button>
                      );
                    })}
                  </motion.div>
                ) : null}
              </AnimatePresence>
            </>
          ) : (
            !isLast && (
              <span className="mx-0.5 text-slate-300 dark:text-zinc-600">
                <ChevronRight />
              </span>
            )
          )}
        </div>
      </li>
    );
  };

  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-24 dark:bg-zinc-950">
      <style>{`
        @keyframes ${uid}-fmbc-sweep {
          0% { transform: translateX(-100%); }
          100% { transform: translateX(220%); }
        }
        .${uid}-fmbc-sweep::after {
          content: "";
          position: absolute;
          inset: 0;
          width: 40%;
          background: linear-gradient(90deg, transparent, rgba(99,102,241,0.16), transparent);
          animation: ${uid}-fmbc-sweep 2.6s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .${uid}-fmbc-sweep::after { animation: none; opacity: 0; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-5xl">
        <header className="mb-6">
          <p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
            File manager
          </p>
          <h2 className="mt-2 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-zinc-50">
            Path breadcrumb with toolbar
          </h2>
          <p className="mt-2 max-w-2xl text-sm text-slate-600 dark:text-zinc-400">
            Long paths collapse behind an overflow menu. Every crumb opens its folder, and the chevron
            beside it lists sibling folders at that level.
          </p>
        </header>

        <div
          ref={wrapRef}
          className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900"
        >
          <div className="relative h-0.5 overflow-hidden bg-slate-100 dark:bg-zinc-800">
            <div className={`${uid}-fmbc-sweep absolute inset-0`} aria-hidden="true" />
          </div>

          <div className="flex flex-wrap items-center gap-2 border-b border-slate-200 px-3 py-2.5 dark:border-zinc-800">
            <div className="flex items-center gap-1" role="group" aria-label="History navigation">
              <button
                type="button"
                onClick={goBack}
                disabled={historyIndex <= 0}
                aria-label="Go back"
                title="Back"
                className={`${btn} px-2`}
              >
                <ArrowIcon dir="left" />
              </button>
              <button
                type="button"
                onClick={goForward}
                disabled={historyIndex >= history.length - 1}
                aria-label="Go forward"
                title="Forward"
                className={`${btn} px-2`}
              >
                <ArrowIcon dir="right" />
              </button>
              <button
                type="button"
                onClick={goUp}
                disabled={depth <= 1}
                aria-label="Go up one level"
                title="Up one level"
                className={`${btn} px-2`}
              >
                <ArrowIcon dir="up" />
              </button>
            </div>

            <div className="mx-1 hidden h-6 w-px bg-slate-200 sm:block dark:bg-zinc-800" />

            <nav aria-label="Folder path" className="min-w-0 flex-1">
              <ol className="flex flex-nowrap items-center gap-0.5 overflow-x-auto py-0.5">
                {head.map((crumb, i) => renderCrumb(crumb, i, trail.length === 1 && i === 0))}

                {collapsed ? (
                  <li className="flex shrink-0 items-center">
                    <span className="mx-0.5 text-slate-300 dark:text-zinc-600">
                      <ChevronRight />
                    </span>
                    <div className="relative">
                      <button
                        type="button"
                        aria-label={`Show ${hidden.length} hidden folders in path`}
                        aria-haspopup="menu"
                        aria-expanded={openMenu === "__overflow"}
                        aria-controls={openMenu === "__overflow" ? `${uid}-overflow` : undefined}
                        onClick={() => setOpenMenu(openMenu === "__overflow" ? null : "__overflow")}
                        className="inline-flex h-7 w-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
                      >
                        <EllipsisIcon />
                      </button>
                      <AnimatePresence>
                        {openMenu === "__overflow" ? (
                          <motion.div
                            id={`${uid}-overflow`}
                            role="menu"
                            aria-label="Hidden path segments"
                            {...fade}
                            transition={{ duration: 0.14, ease: "easeOut" }}
                            className="absolute left-0 top-full z-30 mt-1.5 w-56 rounded-xl border border-slate-200 bg-white p-1 shadow-xl shadow-slate-900/10 dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-black/40"
                          >
                            {hidden.map((crumb) => {
                              const idx = FULL_PATH.findIndex((c) => c.id === crumb.id);
                              return (
                                <button
                                  key={crumb.id}
                                  type="button"
                                  role="menuitem"
                                  onClick={() => navigate(idx + 1)}
                                  className={menuItem}
                                >
                                  <span className="text-slate-400 dark:text-zinc-500">
                                    <FolderIcon open={false} />
                                  </span>
                                  <span className="truncate">{crumb.name}</span>
                                  <span className="ml-auto shrink-0 text-xs tabular-nums text-slate-400 dark:text-zinc-500">
                                    {crumb.itemCount}
                                  </span>
                                </button>
                              );
                            })}
                          </motion.div>
                        ) : null}
                      </AnimatePresence>
                    </div>
                    <span className="mx-0.5 text-slate-300 dark:text-zinc-600">
                      <ChevronRight />
                    </span>
                  </li>
                ) : null}

                {tail.map((crumb, i) => {
                  const idx = FULL_PATH.findIndex((c) => c.id === crumb.id);
                  return renderCrumb(crumb, idx, i === tail.length - 1);
                })}
              </ol>
            </nav>

            <div className="ml-auto flex items-center gap-1.5">
              <button
                type="button"
                onClick={copyPath}
                aria-label={copied ? "Path copied to clipboard" : "Copy path to clipboard"}
                title="Copy path"
                className={`${btn} px-2 ${copied ? "text-emerald-600 dark:text-emerald-400" : ""}`}
              >
                <CopyIcon done={copied} />
              </button>

              <div
                role="group"
                aria-label="View mode"
                className="flex items-center gap-0.5 rounded-lg border border-slate-200 bg-slate-50 p-0.5 dark:border-zinc-700 dark:bg-zinc-800/60"
              >
                {(["list", "grid"] as const).map((mode) => (
                  <button
                    key={mode}
                    type="button"
                    aria-pressed={view === mode}
                    aria-label={`${mode === "list" ? "List" : "Grid"} view`}
                    title={`${mode === "list" ? "List" : "Grid"} view`}
                    onClick={() => {
                      setView(mode);
                      setAnnouncement(`${mode === "list" ? "List" : "Grid"} view`);
                    }}
                    className={`inline-flex h-7 w-8 items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 ${
                      view === mode
                        ? "bg-white text-slate-900 shadow-sm dark:bg-zinc-950 dark:text-zinc-50"
                        : "text-slate-500 hover:text-slate-800 dark:text-zinc-400 dark:hover:text-zinc-100"
                    }`}
                  >
                    {mode === "list" ? <ListIcon /> : <GridIcon />}
                  </button>
                ))}
              </div>

              <label className="sr-only" htmlFor={`${uid}-sort`}>
                Sort files by
              </label>
              <div className="relative">
                <span className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-slate-400 dark:text-zinc-500">
                  <SortIcon />
                </span>
                <select
                  id={`${uid}-sort`}
                  value={sort}
                  onChange={(e) => {
                    const next = e.target.value as SortKey;
                    setSort(next);
                    setAnnouncement(`Sorted by ${next}`);
                  }}
                  className="appearance-none rounded-lg border border-slate-200 bg-white py-1.5 pl-8 pr-7 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
                >
                  <option value="name">Name</option>
                  <option value="modified">Modified</option>
                  <option value="size">Size</option>
                </select>
                <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rotate-90 text-slate-400 dark:text-zinc-500">
                  <ChevronRight />
                </span>
              </div>

              <button
                type="button"
                onClick={() => setAnnouncement(`Upload dialog would open in ${current.name}`)}
                className="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
              >
                <UploadIcon />
                <span className="hidden sm:inline">Upload</span>
              </button>
            </div>
          </div>

          <div className="flex flex-wrap items-center justify-between gap-2 bg-slate-50/70 px-4 py-2 text-xs dark:bg-zinc-950/40">
            <code className="truncate font-mono text-slate-500 dark:text-zinc-500">{pathString}</code>
            <span className="shrink-0 text-slate-500 dark:text-zinc-500">
              {current.itemCount} items &middot; sorted by {sort} &middot; {view} view
            </span>
          </div>

          <div className="px-4 py-5">
            <ul
              className={
                view === "grid"
                  ? "grid grid-cols-2 gap-3 sm:grid-cols-3"
                  : "divide-y divide-slate-100 dark:divide-zinc-800"
              }
            >
              {(SIBLINGS[current.id] ?? ["This folder is empty"]).map((name) => (
                <li
                  key={name}
                  className={
                    view === "grid"
                      ? "flex flex-col items-start gap-2 rounded-xl border border-slate-200 bg-slate-50/60 p-3 dark:border-zinc-800 dark:bg-zinc-800/30"
                      : "flex items-center gap-2.5 py-2.5"
                  }
                >
                  <span className="text-slate-400 dark:text-zinc-500">
                    <FolderIcon open={false} />
                  </span>
                  <span className="truncate text-sm text-slate-700 dark:text-zinc-300">{name}</span>
                </li>
              ))}
            </ul>
          </div>
        </div>

        <p aria-live="polite" className="sr-only">
          {announcement}
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →