{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-morph",
  "title": "Confirm Morph",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Botão de apagar que vira a pergunta no próprio lugar, depois o andamento e o resultado com desfazer.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/confirm-morph.tsx",
      "content": "\n// O texto de confirmação pede leitura: passa do token base de propósito.\nconst TROCA_S = 0.24;\n\"use client\";\n\n// Adaptado de Confirm morph — https://uiarc.dev/components/confirm-morph — Copyright (c) 2026 Elia Kuratli, licença MIT (texto integral em ATTRIBUTIONS.md do @visant/ui).\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useIsPresent,\n  useMotionValue,\n  useReducedMotion,\n  type AnimationPlaybackControls,\n  type Transition,\n  type Variants,\n} from \"motion/react\";\nimport { CircleAlert, CircleCheck, LoaderCircle } from \"lucide-react\";\n\nimport { DUR, EASE_OUT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Confirm Morph: o botão destrutivo que pergunta no próprio lugar.\n *\n * Apagar um projeto pede confirmação, e um modal pra isso é desproporcional:\n * escurece a página, rouba o foco e pergunta longe de onde a pessoa clicou.\n * Aqui a mesma pílula vira a pergunta (Cancelar, Apagar), depois o \"apagando\",\n * depois o resultado com Desfazer. Nada em volta pula de lugar.\n *\n * ── Decisões que não são óbvias ────────────────────────────────────────────\n *\n * 1. **Só a largura anda.** Cada rosto fica na largura natural, encostado na\n *    borda de início, e a pílula revela o resto conforme a mola abre. Texto\n *    nunca escala, nunca espreme, nunca sai da linha de base. Crescer tem um\n *    pouco de vida; encolher assenta sem passar do ponto.\n * 2. **Um clique colado no que abriu a pergunta não responde.** Os primeiros\n *    280 ms depois de abrir ignoram clique e tecla repetida: um duplo clique\n *    ou um Enter segurado nunca apagam nada sem querer.\n * 3. **O foco acompanha o morph, mas só se já estava dentro.** Apagar na\n *    pergunta, a raiz enquanto trabalha, Desfazer no resultado, o gatilho no\n *    repouso. Um tempo esgotado nunca rouba foco de outro lugar da página.\n * 4. **Os tempos esgotam num relógio que pausa.** Ponteiro em cima da peça ou\n *    aba escondida seguram o relógio: ninguém perde o Desfazer porque foi ler\n *    o que estava ao lado.\n * 5. **`onConfirm` pode devolver promessa.** Aí aparece o \"apagando\"; se a\n *    promessa falhar, o rosto de erro oferece Tentar de novo. Sem promessa, vai\n *    direto pro resultado.\n *\n * Esc cancela a pergunta ou dispensa o resultado; clique fora cancela.\n */\n\nexport type ConfirmMorphState = \"idle\" | \"confirming\" | \"pending\" | \"done\" | \"error\";\n\nexport interface ConfirmMorphProps {\n  /** Rótulo em repouso, como \"Apagar projeto\". */\n  label: React.ReactNode;\n  /** Ícone antes do rótulo em repouso. */\n  icon?: React.ReactNode;\n  /** A pergunta, como \"Apagar 3 arquivos?\". Sem ela, vira o rótulo com interrogação. */\n  prompt?: React.ReactNode;\n  confirmLabel?: string;\n  cancelLabel?: string;\n  /** Ao lado do giro, enquanto `onConfirm` resolve. */\n  pendingLabel?: string;\n  doneLabel?: string;\n  errorLabel?: string;\n  retryLabel?: string;\n  undoLabel?: string;\n  /** Ao lado do giro, enquanto `onUndo` resolve. */\n  undoingLabel?: string;\n  /** `danger` pinta o Apagar de destrutivo; `brand` usa a cor da marca, pra ação importante mas reversível. */\n  tone?: \"danger\" | \"brand\";\n  /** Roda ao confirmar. Devolva promessa pra mostrar o \"apagando\"; rejeição mostra o erro. */\n  onConfirm?: () => void | Promise<unknown>;\n  /** Se existir, o resultado ganha Desfazer. */\n  onUndo?: () => void | Promise<unknown>;\n  onCancel?: () => void;\n  /** Ms até a pergunta sem resposta voltar ao repouso. 0 desliga. */\n  confirmTimeout?: number;\n  /** Ms que o resultado fica antes de voltar ao repouso. 0 desliga. */\n  resultTimeout?: number;\n  disabled?: boolean;\n  className?: string;\n}\n\nconst ARMA_MS = 280;\nconst CRESCE: Transition = { type: \"spring\", visualDuration: 0.3, bounce: 0.18 };\nconst ENCOLHE: Transition = { type: \"spring\", visualDuration: 0.28, bounce: 0 };\n\n// O rosto novo chega por partes, em ordem de leitura, cada uma limpando um desfoque leve.\nconst rostoV: Variants = {\n  escondido: { opacity: 1 },\n  visivel: { opacity: 1, transition: { staggerChildren: 0.04, delayChildren: 0.05 } },\n  saindo: { opacity: 0, filter: \"blur(2px)\", transition: { duration: DUR.press } },\n};\nconst parteV: Variants = {\n  escondido: { opacity: 0, filter: \"blur(4px)\" },\n  visivel: { opacity: 1, filter: \"blur(0px)\", transition: { duration: TROCA_S, ease: EASE_OUT } },\n};\nconst rostoReduzido: Variants = {\n  escondido: { opacity: 0 },\n  visivel: { opacity: 1, transition: { duration: DUR.fast } },\n  saindo: { opacity: 0, transition: { duration: DUR.press } },\n};\nconst parteReduzida: Variants = { escondido: { opacity: 1 }, visivel: { opacity: 1 } };\n\nfunction Rosto({\n  id,\n  reduzido,\n  onLargura,\n  rotuladoPor,\n  children,\n}: {\n  id: ConfirmMorphState;\n  reduzido: boolean;\n  onLargura: (w: number) => void;\n  rotuladoPor?: string;\n  children: React.ReactNode;\n}) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const presente = useIsPresent();\n  React.useLayoutEffect(() => {\n    const no = ref.current;\n    if (!no) return;\n    // `inert` por propriedade: o React 18 não repassa o atributo booleano.\n    (no as HTMLElement & { inert: boolean }).inert = !presente;\n    if (!presente) return;\n    // Largura fracionária, pra mola pousar exatamente na largura natural e a volta pro `auto` não dar salto.\n    // Do estilo computado, não do retângulo: dentro de um `transform: scale` (um palco, um preview)\n    // o retângulo vem encolhido, e a largura de tela aplicada como largura local cortava o botão.\n    const relatar = () => onLargura(parseFloat(getComputedStyle(no).width) || no.offsetWidth);\n    relatar();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const obs = new ResizeObserver(relatar);\n    obs.observe(no);\n    return () => obs.disconnect();\n  }, [onLargura, presente]);\n  return (\n    <motion.div\n      ref={ref}\n      data-face={id}\n      role={rotuladoPor ? \"group\" : undefined}\n      aria-labelledby={rotuladoPor}\n      variants={reduzido ? rostoReduzido : rostoV}\n      initial=\"escondido\"\n      animate=\"visivel\"\n      exit=\"saindo\"\n      className={cn(\n        \"flex h-full flex-none items-center gap-0.5 whitespace-nowrap\",\n        id === \"idle\" ? \"p-0\" : \"px-1\",\n        // O rosto que sai deixa o fluxo exatamente onde estava e some enquanto a pílula anda.\n        !presente && \"pointer-events-none absolute left-0 top-0\",\n      )}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nfunction Parte({ reduzido, className, children }: { reduzido: boolean; className?: string; children: React.ReactNode }) {\n  return (\n    <motion.span variants={reduzido ? parteReduzida : parteV} className={cn(\"inline-flex h-full items-center\", className)}>\n      {children}\n    </motion.span>\n  );\n}\n\nconst BOTAO =\n  \"relative inline-flex h-7 items-center rounded-[var(--r-pill)] px-3 font-medium outline-none pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-2 transition-colors [transition-duration:var(--dur-fast)] focus-visible:[box-shadow:var(--focus)]\";\n\nexport function ConfirmMorph({\n  label,\n  icon,\n  prompt,\n  confirmLabel = \"Apagar\",\n  cancelLabel = \"Cancelar\",\n  pendingLabel = \"Apagando\",\n  doneLabel = \"Apagado\",\n  errorLabel = \"Não deu certo\",\n  retryLabel = \"Tentar de novo\",\n  undoLabel = \"Desfazer\",\n  undoingLabel = \"Restaurando\",\n  tone = \"danger\",\n  onConfirm,\n  onUndo,\n  onCancel,\n  confirmTimeout = 6000,\n  resultTimeout = 5000,\n  disabled = false,\n  className,\n}: ConfirmMorphProps) {\n  const reduzido = useReducedMotion() ?? false;\n  const uid = React.useId();\n  const perguntaId = `${uid}-pergunta`;\n  const raiz = React.useRef<HTMLDivElement>(null);\n\n  const [estado, setEstado] = React.useState<ConfirmMorphState>(\"idle\");\n  const [tarefa, setTarefa] = React.useState<\"confirm\" | \"undo\">(\"confirm\");\n  const [aviso, setAviso] = React.useState(\"\");\n  const vivo = React.useRef(estado);\n  vivo.current = estado;\n  const focoPendente = React.useRef(false);\n  const rodada = React.useRef(0);\n  const perguntouEm = React.useRef(0);\n\n  const ir = React.useCallback((prox: ConfirmMorphState) => {\n    if (prox === vivo.current) return;\n    const r = raiz.current;\n    focoPendente.current = !!r && (r.contains(document.activeElement) || document.activeElement === document.body);\n    if (prox === \"confirming\") perguntouEm.current = performance.now();\n    vivo.current = prox;\n    setEstado(prox);\n  }, []);\n\n  const repousar = React.useCallback(() => {\n    rodada.current++;\n    ir(\"idle\");\n  }, [ir]);\n\n  const executar = React.useCallback(\n    async (tipo: \"confirm\" | \"undo\") => {\n      const fn = tipo === \"confirm\" ? onConfirm : onUndo;\n      const minha = ++rodada.current;\n      setTarefa(tipo);\n      let res: void | Promise<unknown> | undefined;\n      try {\n        res = fn?.();\n      } catch {\n        ir(\"error\");\n        setAviso(errorLabel);\n        return;\n      }\n      if (res && typeof (res as Promise<unknown>).then === \"function\") {\n        ir(\"pending\");\n        setAviso(tipo === \"confirm\" ? pendingLabel : undoingLabel);\n        try {\n          await res;\n        } catch {\n          if (minha !== rodada.current) return;\n          ir(\"error\");\n          setAviso(errorLabel);\n          return;\n        }\n        if (minha !== rodada.current) return;\n      }\n      if (tipo === \"undo\") {\n        ir(\"idle\");\n        setAviso(\"Desfeito\");\n        return;\n      }\n      ir(\"done\");\n      setAviso(onUndo ? `${doneLabel}. ${undoLabel} disponível.` : doneLabel);\n    },\n    [doneLabel, errorLabel, ir, onConfirm, onUndo, pendingLabel, undoLabel, undoingLabel],\n  );\n\n  const cancelar = React.useCallback(() => {\n    onCancel?.();\n    repousar();\n    setAviso(\"Cancelado\");\n  }, [onCancel, repousar]);\n\n  /* A forma: uma pílula cuja largura anda até o rosto atual. Em repouso é `auto`, então renderiza certo antes de hidratar. */\n  const largura = useMotionValue<number | \"auto\">(\"auto\");\n  const alvo = React.useRef(0);\n  const voo = React.useRef(0);\n  const onLargura = React.useCallback(\n    (w: number) => {\n      if (Math.abs(w - alvo.current) < 0.25) return;\n      const de = alvo.current;\n      alvo.current = w;\n      if (!de || reduzido) {\n        voo.current++;\n        largura.jump(\"auto\");\n        return;\n      }\n      const agora = largura.get();\n      if (agora === \"auto\") largura.jump(de);\n      const meu = ++voo.current;\n      animate(largura, w, w > (typeof agora === \"number\" ? agora : de) ? CRESCE : ENCOLHE).then(() => {\n        if (meu === voo.current) largura.jump(\"auto\");\n      });\n    },\n    [reduzido, largura],\n  );\n\n  /* O relógio invisível dos tempos esgotados. Ponteiro em cima ou aba escondida seguram. */\n  const relogio = useMotionValue(1);\n  const controle = React.useRef<AnimationPlaybackControls | null>(null);\n  const seguras = React.useRef({ hover: false, oculta: false });\n  const prazo = estado === \"confirming\" ? confirmTimeout : estado === \"done\" || estado === \"error\" ? resultTimeout : 0;\n  const esgotar = React.useRef(() => {});\n  esgotar.current = () => (vivo.current === \"confirming\" ? cancelar() : repousar());\n  const sincronizar = React.useCallback(() => {\n    const c = controle.current;\n    if (!c) return;\n    if (seguras.current.hover || seguras.current.oculta) c.pause();\n    else c.play();\n  }, []);\n  React.useEffect(() => {\n    if (!prazo) return;\n    relogio.jump(1);\n    const c = animate(relogio, 0, { duration: prazo / 1000, ease: \"linear\" });\n    controle.current = c;\n    c.then(() => {\n      if (controle.current === c) {\n        controle.current = null;\n        esgotar.current();\n      }\n    });\n    sincronizar();\n    return () => {\n      if (controle.current === c) controle.current = null;\n      c.stop();\n    };\n  }, [relogio, estado, sincronizar, prazo]);\n  React.useEffect(() => {\n    const vis = () => {\n      seguras.current.oculta = document.hidden;\n      sincronizar();\n    };\n    document.addEventListener(\"visibilitychange\", vis);\n    return () => document.removeEventListener(\"visibilitychange\", vis);\n  }, [sincronizar]);\n\n  // Clique fora responde \"não\".\n  React.useEffect(() => {\n    if (estado !== \"confirming\") return;\n    const baixo = (e: PointerEvent) => {\n      if (!raiz.current?.contains(e.target as Node)) cancelar();\n    };\n    document.addEventListener(\"pointerdown\", baixo);\n    return () => document.removeEventListener(\"pointerdown\", baixo);\n  }, [cancelar, estado]);\n\n  React.useLayoutEffect(() => {\n    if (!focoPendente.current) return;\n    focoPendente.current = false;\n    const r = raiz.current;\n    if (!r) return;\n    const alvoFoco = r.querySelector<HTMLElement>(`[data-face=\"${estado}\"] [data-autofocus]:not(:disabled)`);\n    (alvoFoco ?? r).focus({ preventScroll: true });\n  }, [estado]);\n\n  const responder = () => {\n    if (performance.now() - perguntouEm.current < ARMA_MS) return;\n    void executar(\"confirm\");\n  };\n\n  const perguntaVista = prompt ?? <>{label}?</>;\n  const secundario = cn(BOTAO, \"text-foreground hover:bg-muted\");\n\n  const rosto = (() => {\n    switch (estado) {\n      case \"confirming\":\n        return (\n          <>\n            <Parte reduzido={reduzido} className=\"pl-2.5 pr-1.5 font-medium\">\n              <span id={perguntaId}>{perguntaVista}</span>\n            </Parte>\n            <Parte reduzido={reduzido}>\n              <button type=\"button\" className={secundario} onClick={cancelar}>\n                {cancelLabel}\n              </button>\n            </Parte>\n            <Parte reduzido={reduzido}>\n              <button\n                type=\"button\"\n                data-autofocus\n                onClick={responder}\n                onKeyDown={(e) => e.repeat && e.preventDefault()}\n                className={cn(\n                  BOTAO,\n                  tone === \"danger\"\n                    ? \"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                    : \"bg-[var(--brand)] text-[var(--brand-foreground)] hover:opacity-90\",\n                )}\n              >\n                {confirmLabel}\n              </button>\n            </Parte>\n          </>\n        );\n      case \"pending\":\n        return (\n          <Parte reduzido={reduzido} className=\"gap-2 px-2.5\">\n            <LoaderCircle\n              aria-hidden=\"true\"\n              className=\"size-4 animate-spin text-muted-foreground motion-reduce:[animation-duration:2.4s]\"\n              strokeWidth={1.75}\n            />\n            <span>{tarefa === \"undo\" ? undoingLabel : pendingLabel}</span>\n          </Parte>\n        );\n      case \"done\":\n        return (\n          <>\n            <Parte reduzido={reduzido} className=\"gap-2 px-2.5\">\n              <motion.span\n                aria-hidden=\"true\"\n                className=\"grid place-items-center text-foreground\"\n                initial={reduzido ? false : { scale: 0.5, opacity: 0 }}\n                animate={{ scale: 1, opacity: 1 }}\n                transition={{ type: \"spring\", visualDuration: 0.2, bounce: 0.3 }}\n              >\n                <CircleCheck className=\"size-4\" strokeWidth={1.75} />\n              </motion.span>\n              <span>{doneLabel}</span>\n            </Parte>\n            {onUndo && (\n              <Parte reduzido={reduzido}>\n                <button type=\"button\" data-autofocus className={secundario} onClick={() => void executar(\"undo\")}>\n                  {undoLabel}\n                </button>\n              </Parte>\n            )}\n          </>\n        );\n      case \"error\":\n        return (\n          <>\n            <Parte reduzido={reduzido} className=\"gap-2 px-2.5\">\n              <CircleAlert aria-hidden=\"true\" className=\"size-4 text-destructive\" strokeWidth={1.75} />\n              <span>{errorLabel}</span>\n            </Parte>\n            <Parte reduzido={reduzido}>\n              <button type=\"button\" data-autofocus className={secundario} onClick={() => void executar(tarefa)}>\n                {retryLabel}\n              </button>\n            </Parte>\n          </>\n        );\n      default:\n        return (\n          <Parte reduzido={reduzido}>\n            <button\n              type=\"button\"\n              data-autofocus\n              disabled={disabled}\n              onClick={() => {\n                setAviso(typeof perguntaVista === \"string\" ? perguntaVista : \"\");\n                ir(\"confirming\");\n              }}\n              className=\"inline-flex h-full items-center gap-2 rounded-[var(--r-pill)] pl-3.5 pr-4 font-medium text-foreground outline-none transition-colors [transition-duration:var(--dur-fast)] hover:bg-muted focus-visible:[box-shadow:var(--focus)] disabled:cursor-not-allowed disabled:text-muted-foreground disabled:hover:bg-transparent\"\n            >\n              {icon && (\n                <span aria-hidden=\"true\" className=\"grid size-4 place-items-center text-muted-foreground [&_svg]:size-4\">\n                  {icon}\n                </span>\n              )}\n              <span>{label}</span>\n            </button>\n          </Parte>\n        );\n    }\n  })();\n\n  return (\n    <div\n      ref={raiz}\n      tabIndex={-1}\n      data-state={estado}\n      aria-busy={estado === \"pending\" || undefined}\n      className={cn(\"relative inline-flex min-w-0 max-w-full align-middle text-sm text-foreground outline-none\", className)}\n      onKeyDown={(e) => {\n        if (e.key !== \"Escape\") return;\n        if (estado === \"confirming\") {\n          e.preventDefault();\n          e.stopPropagation();\n          cancelar();\n        } else if (estado === \"done\" || estado === \"error\") {\n          e.preventDefault();\n          e.stopPropagation();\n          repousar();\n        }\n      }}\n      onPointerEnter={() => {\n        seguras.current.hover = true;\n        sincronizar();\n      }}\n      onPointerLeave={() => {\n        seguras.current.hover = false;\n        sincronizar();\n      }}\n    >\n      {/* A borda é um contorno por cima (after), pra nunca entrar na medida da largura. */}\n      <motion.div\n        style={{ width: largura }}\n        className=\"relative flex h-9 pointer-coarse:h-11 max-w-full justify-start overflow-clip rounded-[var(--r-pill)] bg-card [box-shadow:var(--e-raised)] after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-border after:content-['']\"\n      >\n        <AnimatePresence initial={false}>\n          <Rosto\n            key={estado}\n            id={estado}\n            reduzido={reduzido}\n            onLargura={onLargura}\n            rotuladoPor={estado === \"confirming\" ? perguntaId : undefined}\n          >\n            {rosto}\n          </Rosto>\n        </AnimatePresence>\n      </motion.div>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {aviso}\n      </span>\n    </div>\n  );\n}\n\nexport default ConfirmMorph;\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Apagar item com confirmação no próprio botão, andamento e desfazer, quando a ação é reversível",
      "evite": "Ação irreversível e cara: hold-to-confirm; pagamento ou envio no celular, slide-to-confirm"
    }
  },
  "categories": [
    "controles",
    "botao",
    "confirmacao",
    "destrutivo",
    "desfazer"
  ],
  "type": "registry:ui"
}