{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "folder-tabs",
  "title": "Folder Tabs",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Abas como pastas suspensas numa gaveta: arrastar folheia a pilha, e a pasta escolhida fica de pé e aberta.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/visant/ui/folder-tabs.tsx",
      "content": "\"use client\";\n\n/*\n * PASTAS: section tabs as hanging folders in a drawer. Drag up or down across\n * the stack to thumb through it; the folders in front drop forward until the\n * one you want stands open.\n *\n * Collision: a project archive where every section is a physical folder with its\n * own tab + the hanging file drawer, where you read a folder by tipping the ones\n * in front of it down. One position p runs the whole stack: folder k sits at its\n * rest height while k ≤ p and drops by the stack height as p passes it, so a\n * slow drag shows two folders mid-tip. On release p snaps to the nearest folder\n * on a spring. A tap on a tab picks it; a tablist, so arrows, Home and End work.\n */\nimport * as React from \"react\";\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform, type MotionValue } from \"motion/react\";\n\nconst FOLDERS = [\n  { tab: \"Briefing\", line: \"Site novo, 6 páginas, lançamento em março.\", n: \"6\", unit: \"páginas\" },\n  { tab: \"Refs\", line: \"Moodboard aprovado, 3 direções descartadas.\", n: \"24\", unit: \"imagens\" },\n  { tab: \"Entregas\", line: \"Home e Sobre prontas, faltam 4 páginas.\", n: \"2/6\", unit: \"prontas\" },\n  { tab: \"Contrato\", line: \"Assinado, segunda parcela na entrega.\", n: \"50%\", unit: \"pago\" },\n];\nconst N = FOLDERS.length;\nconst TAB = 28; // px between folder tops\nconst PITCH = 36; // px of drag per folder\nconst START = 2;\nconst SNAP = { type: \"spring\", stiffness: 380, damping: 34 } as const;\nconst clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v));\n\nfunction Folder({ k, p, drop, on, onPick }: { k: number; p: MotionValue<number>; drop: number; on: boolean; onPick: (k: number) => void }) {\n  const f = FOLDERS[k];\n  const y = useTransform(p, (v) => k * TAB + clamp(k - v, 0, 1) * drop);\n  return (\n    <motion.div style={{ y, zIndex: k }} className=\"absolute inset-x-0 top-0 h-full\">\n      <button\n        type=\"button\"\n        role=\"tab\"\n        id={`pastas-tab-${k}`}\n        aria-selected={on}\n        aria-controls=\"pastas-panel\"\n        tabIndex={on ? 0 : -1}\n        data-k={k}\n        onClick={() => onPick(k)}\n        style={{ marginLeft: `${k * 25}%` }}\n        className={`relative h-7 w-[24%] truncate rounded-t-control border border-b-0 px-2 text-left font-mono text-[10px] uppercase tracking-[0.02em] sm:px-3 sm:text-[11px] sm:tracking-[0.08em] outline-none focus-visible:[box-shadow:var(--focus)] ${\n          on ? \"border-border bg-background text-foreground\" : \"border-border bg-card text-muted-foreground\"\n        }`}\n      >\n        {f.tab}\n      </button>\n      <div className={`h-full rounded-[var(--r-surface)] rounded-tl-none border border-border shadow-[0_-6px_16px_-10px_color-mix(in_srgb,var(--foreground)_30%,transparent)] ${on ? \"bg-background\" : \"bg-card\"}`} />\n    </motion.div>\n  );\n}\n\nexport function FolderTabs() {\n  const reduce = useReducedMotion();\n  const box = React.useRef<HTMLDivElement>(null);\n  const list = React.useRef<HTMLDivElement>(null);\n  const [h, setH] = React.useState(240);\n  const [i, setI] = React.useState(START);\n  const p = useMotionValue(START);\n  const drag = React.useRef<{ y: number; p: number; moved: boolean } | null>(null);\n  const drop = Math.max(0, h - N * TAB - 8);\n  const f = FOLDERS[i];\n\n  React.useLayoutEffect(() => {\n    const el = box.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([e]) => setH(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  const settle = (n: number) => {\n    const k = clamp(Math.round(n), 0, N - 1);\n    setI(k);\n    if (reduce) p.jump(k);\n    else animate(p, k, SNAP);\n    return k;\n  };\n\n  const onDown = (e: React.PointerEvent) => {\n    e.currentTarget.setPointerCapture(e.pointerId);\n    p.stop();\n    drag.current = { y: e.clientY, p: p.get(), moved: false };\n  };\n  const onMove = (e: React.PointerEvent) => {\n    const d = drag.current;\n    if (!d) return;\n    const dy = e.clientY - d.y;\n    if (Math.abs(dy) > 4) d.moved = true;\n    if (!d.moved) return;\n    const v = clamp(d.p - dy / PITCH, -0.3, N - 0.7);\n    if (reduce) {\n      p.jump(clamp(Math.round(v), 0, N - 1));\n      setI(clamp(Math.round(v), 0, N - 1));\n    } else {\n      p.set(v);\n      setI(clamp(Math.round(v), 0, N - 1));\n    }\n  };\n  const onUp = (e: React.PointerEvent) => {\n    const d = drag.current;\n    drag.current = null;\n    if (!d) return;\n    if (d.moved) return void settle(p.get());\n    // A tap: the tab under the finger, if any.\n    const t = (document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null)?.closest<HTMLElement>(\"[data-k]\");\n    if (t) settle(Number(t.dataset.k));\n    else settle(p.get());\n  };\n\n  const onKey = (e: React.KeyboardEvent) => {\n    const map: Record<string, number> = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: N - 1 };\n    if (!(e.key in map)) return;\n    e.preventDefault();\n    const k = settle(map[e.key]);\n    list.current?.querySelector<HTMLElement>(`[data-k=\"${k}\"]`)?.focus();\n  };\n\n  return (\n    <div className=\"flex flex-col\">\n      <div\n        ref={box}\n        data-lab-target\n        onPointerDown={onDown}\n        onPointerMove={onMove}\n        onPointerUp={onUp}\n        onPointerCancel={onUp}\n        className=\"relative h-64 cursor-grab touch-none select-none overflow-hidden active:cursor-grabbing\"\n      >\n        <div ref={list} role=\"tablist\" aria-label=\"Seções do projeto\" onKeyDown={onKey} className=\"absolute inset-0\">\n          {FOLDERS.map((_, k) => (\n            <Folder key={k} k={k} p={p} drop={drop} on={k === i} onPick={settle} />\n          ))}\n        </div>\n        {/* What the open folder holds: one number, one line. */}\n        <motion.div\n          key={i}\n          id=\"pastas-panel\"\n          role=\"tabpanel\"\n          aria-labelledby={`pastas-tab-${i}`}\n          initial={reduce ? false : { opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}\n          style={{ top: i * TAB + 48 }}\n          className=\"pointer-events-none absolute inset-x-4 z-10 flex flex-col gap-2\"\n        >\n          <div className=\"flex items-baseline gap-1.5\">\n            <span className=\"[font-family:var(--font-display,inherit)] text-[clamp(2.25rem,10vw,3rem)] font-medium leading-none tracking-[-0.03em] tabular-nums text-foreground\">{f.n}</span>\n            <span className=\"[font-family:var(--font-display,inherit)] text-xs text-muted-foreground\">{f.unit}</span>\n          </div>\n          <span className=\"[font-family:var(--font-display,inherit)] text-xs text-muted-foreground\">{f.line}</span>\n        </motion.div>\n      </div>\n      <span className=\"sr-only\" aria-live=\"polite\">{`${f.tab}: ${f.n} ${f.unit}. ${f.line}`}</span>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Navegar entre seções de um arquivo, portfólio ou projeto com cara de objeto",
      "evite": "Abas sobre foto ou vídeo: glass-tabs; poucas opções em linha: segmented-control"
    }
  },
  "categories": [
    "controles",
    "abas",
    "pastas",
    "navegacao",
    "arquivo",
    "gaveta"
  ],
  "type": "registry:ui"
}