{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "paper-shuttle",
  "title": "Paper Shuttle",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Progresso com narrativa de papel passando pela máquina.",
  "dependencies": [
    "framer-motion"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/paper-shuttle.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"framer-motion\";\n\nimport { DUR, INSTANT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Papéis voando de uma pasta para outra — o loader de transferência.\n *\n * Existe para uma classe específica de espera: quando o sistema está MOVENDO\n * algo (importar, extrair, converter, sincronizar). Um spinner nesse caso não\n * conta nada; a metáfora da pasta conta a operação inteira sem uma palavra, e o\n * usuário entende que fechar a aba interrompe algo.\n *\n * Três papéis com delay escalonado (0 / .35 / .7 de um loop de 1.2s) dão fluxo\n * contínuo com uma única animação declarada. Mais que três vira confete.\n *\n * A parábola (`y` sobe e volta) importa: linha reta lê como barra de progresso\n * disfarçada; o arco lê como objeto atravessando espaço.\n *\n * Sem cor própria: a pasta usa `currentColor` (default: `--muted-foreground`,\n * neutra; passe `className=\"text-...\"` pra tingir) e o papel os\n * tokens de superfície, então o loader troca de marca junto com o tema.\n *\n * `prefers-reduced-motion`: papéis parados no meio do trajeto, texto e barra\n * (que são informação real) intactos.\n */\n\n// O voo em laço é o próprio indicador de espera: precisa ser visto inteiro.\nconst VOO_S = 1.2;\nexport function PaperShuttle({\n  progress,\n  label,\n  className,\n}: {\n  /** 0–100. Omitido ⇒ modo indeterminado, só o rótulo. */\n  progress?: number;\n  /** Texto sob a animação. Default: \"Transferindo\" (com a porcentagem ao lado) ou \"Processando…\". */\n  label?: string;\n  className?: string;\n}) {\n  const reduced = useReducedMotion();\n\n  // Grampeado uma vez só: antes a barra respeitava o limite e o número não, então\n  // um `progress={130}` desenhava a barra cheia com \"130%\" escrito embaixo.\n  const pct = progress == null ? null : Math.min(100, Math.max(0, progress));\n\n  return (\n    <div className={cn(\"text-muted-foreground\", className)} role=\"status\" aria-live=\"polite\">\n      <div className=\"relative mx-auto h-[64px] w-[200px]\">\n        <div className=\"absolute bottom-0 left-0\">\n          <Folder open />\n        </div>\n        <div className=\"absolute bottom-0 right-0\">\n          <Folder />\n        </div>\n\n        <div className=\"pointer-events-none absolute inset-0\">\n          {[0, 0.35, 0.7].map((delay) => (\n            <motion.div\n              key={delay}\n              className=\"absolute left-[14px] top-[6px]\"\n              initial={{ x: 0, y: 20, opacity: 0, rotate: -5 }}\n              animate={\n                reduced\n                  ? { x: 70, y: -6, opacity: 1, rotate: -8 }\n                  : {\n                      x: [0, 50, 110, 150],\n                      y: [20, -8, -4, 22],\n                      opacity: [0, 1, 1, 0],\n                      rotate: [-5, -18, 12, 5],\n                    }\n              }\n              transition={\n                reduced\n                  ? INSTANT\n                  : {\n                      duration: VOO_S,\n                      repeat: Infinity,\n                      delay,\n                      ease: \"easeInOut\",\n                    }\n              }\n            >\n              <Sheet />\n            </motion.div>\n          ))}\n        </div>\n      </div>\n\n      <div className=\"mx-auto mt-4 w-[200px]\">\n        {pct != null && (\n          <div\n            className=\"h-1 overflow-hidden rounded-full bg-muted\"\n            role=\"progressbar\"\n            aria-valuenow={Math.round(pct)}\n            aria-valuemin={0}\n            aria-valuemax={100}\n          >\n            <motion.div\n              className=\"h-full rounded-full bg-[var(--brand,var(--foreground))]\"\n              initial={{ width: 0 }}\n              animate={{ width: `${pct}%` }}\n              transition={{ duration: DUR.slow, ease: \"easeOut\" }}\n            />\n          </div>\n        )}\n        {/* A linha é `aria-hidden` porque ela muda a cada quadro de progresso: numa\n            região viva o leitor de tela leria \"41%, 43%, 45%\" sem parar. O que a\n            região anuncia é o rótulo, a frase que de fato muda de sentido.\n            Texto comum, sem ruído de máquina: a peça é genérica de propósito e\n            herda a voz da interface onde for instalada. */}\n        <p\n          aria-hidden=\"true\"\n          className=\"mt-2 flex items-baseline justify-between gap-3 text-xs text-muted-foreground\"\n        >\n          <span className=\"truncate\">{label ?? (pct != null ? \"Transferindo\" : \"Processando…\")}</span>\n          {pct != null ? <span className=\"tabular-nums text-foreground\">{Math.round(pct)}%</span> : null}\n        </p>\n        <span className=\"sr-only\">{label ?? \"Processando\"}</span>\n      </div>\n    </div>\n  );\n}\n\n/** Pasta suspensa. `open` = a de origem (aba levantada, boca visível). */\nfunction Folder({ open }: { open?: boolean }) {\n  return (\n    <svg width=\"40\" height=\"34\" viewBox=\"0 0 40 34\" fill=\"none\" aria-hidden=\"true\">\n      {open ? (\n        <>\n          <rect\n            x=\"0\"\n            y=\"8\"\n            width=\"36\"\n            height=\"24\"\n            rx=\"1\"\n            fill=\"currentColor\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n          />\n          <path\n            d=\"M0 8 L14 8 L17 3 L36 3 L36 8\"\n            fill=\"currentColor\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            opacity=\"0.75\"\n          />\n          {/* faixa mais escura = a boca da pasta, o que dá o volume */}\n          <rect\n            x=\"0\"\n            y=\"8\"\n            width=\"36\"\n            height=\"4\"\n            fill=\"var(--foreground)\"\n            opacity=\"0.15\"\n          />\n        </>\n      ) : (\n        <>\n          <rect\n            x=\"2\"\n            y=\"10\"\n            width=\"36\"\n            height=\"22\"\n            rx=\"1\"\n            fill=\"currentColor\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n          />\n          <path\n            d=\"M2 10 L16 10 L19 5 L38 5 L38 10\"\n            fill=\"currentColor\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            opacity=\"0.75\"\n          />\n        </>\n      )}\n    </svg>\n  );\n}\n\n/** Folha com canto dobrado — o dobrado é o que a faz ler como papel em 20px. */\nfunction Sheet() {\n  return (\n    <svg width=\"20\" height=\"24\" viewBox=\"0 0 20 24\" fill=\"none\" aria-hidden=\"true\">\n      <path\n        d=\"M1 1 H13 L19 7 V23 H1 Z\"\n        fill=\"var(--card)\"\n        stroke=\"var(--border)\"\n        strokeWidth=\"1.2\"\n      />\n      <path\n        d=\"M13 1 V7 H19\"\n        fill=\"var(--muted)\"\n        stroke=\"var(--border)\"\n        strokeWidth=\"1.2\"\n      />\n      <g stroke=\"var(--muted-foreground)\" strokeWidth=\"1\" opacity=\"0.5\">\n        <line x1=\"4\" y1=\"11\" x2=\"16\" y2=\"11\" />\n        <line x1=\"4\" y1=\"14\" x2=\"16\" y2=\"14\" />\n        <line x1=\"4\" y1=\"17\" x2=\"12\" y2=\"17\" />\n      </g>\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Espera de operação que move dados: importar, exportar, converter ou sincronizar",
      "evite": "Espera de IA com etapas: status-ticker; conteúdo da tela carregando, skeleton-rail"
    }
  },
  "categories": [
    "feedback",
    "progresso",
    "export"
  ],
  "type": "registry:ui"
}