{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-edit",
  "title": "Inline Edit",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Editar onde se lê. Enter salva, Esc cancela, o valor só troca depois que a promessa resolve.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/inline-edit.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { Check, Loader2, Pencil, X } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\n/**\n * Edição no lugar: o texto vira campo sem sair do lugar.\n *\n * Três defeitos moravam aqui, e todos eram do mesmo tipo — a peça não dizia o\n * que era e não ficava parada:\n *\n * 1. Nada indicava que aquilo era editável. A única pista era `hover:bg-muted`,\n *    ou seja, uma afordância que só existe DEPOIS que a pessoa já passou o\n *    mouse por cima por acaso. No celular ela nunca existe. Agora o repouso\n *    carrega a marca: sublinhado tracejado embaixo do texto e um lápis do lado.\n *    O lápis ocupa o espaço dele SEMPRE (só a opacidade muda no hover), senão\n *    aparecer no hover empurraria o texto.\n * 2. O texto quebrava em duas linhas e o vizinho pulava junto. Quem consome\n *    (a Mesa de trabalho) teve que escrever `whitespace-nowrap` do lado de fora\n *    pra segurar o cabeçalho — remendo no consumidor pra defeito do componente.\n *    Agora é `truncate` aqui dentro: uma linha, reticências, ponto final.\n * 3. Entrar em edição mudava o tamanho da caixa. Altura, porque leitura tinha\n *    `min-h-[32px]` e edição não tinha nada; largura, porque a caixa encolhia\n *    pro tamanho do input. Numa linha de tabela ou num cabeçalho em flex isso\n *    é a interface inteira dando um pulo a cada clique. Agora os dois estados\n *    compartilham uma altura (`--ie-h`) e a largura da leitura fica congelada\n *    enquanto edita — mesma caixa, conteúdo diferente.\n *\n * A largura é medida em vez de renderizada duas vezes de propósito: `value` é um\n * ReactNode que vem do consumidor (no data-table é um `flexRender` de célula) e\n * renderizar isso escondido só pra medir cobraria o dobro em toda linha da tabela.\n */\nexport interface InlineEditProps {\n    value: React.ReactNode;\n    onSave: () => Promise<void>;\n    onCancel: () => void;\n    isEditing: boolean;\n    onStartEdit: () => void;\n    children: React.ReactNode;\n    className?: string;\n    triggerClassName?: string;\n    showButtons?: boolean;\n}\n\nexport function InlineEdit({\n    value,\n    onSave,\n    onCancel,\n    isEditing,\n    onStartEdit,\n    children,\n    className,\n    triggerClassName,\n    showButtons = false,\n}: InlineEditProps) {\n    const [status, setStatus] = React.useState<'idle' | 'saving' | 'success' | 'error'>('idle');\n\n    // Largura da caixa em repouso, guardada a cada render de leitura. No render em\n    // que `isEditing` vira true ela já está aqui, então a caixa não encolhe nem\n    // por um frame — nada de medir depois e corrigir (isso É o pulo).\n    const readBoxRef = React.useRef<HTMLDivElement>(null);\n    const readWidth = React.useRef<number>(0);\n    React.useLayoutEffect(() => {\n        if (!isEditing && readBoxRef.current) readWidth.current = readBoxRef.current.offsetWidth;\n    });\n\n    React.useEffect(() => {\n        if (!isEditing) return;\n        const onDocKeyDown = (e: KeyboardEvent) => {\n            if (e.key !== 'Escape') return;\n            e.preventDefault();\n            e.stopPropagation();\n            setStatus('idle');\n            onCancel();\n        };\n        document.addEventListener('keydown', onDocKeyDown, true);\n        return () => document.removeEventListener('keydown', onDocKeyDown, true);\n    }, [isEditing, onCancel]);\n\n    const handleSave = async (e?: React.FormEvent) => {\n        if (e) {\n            e.preventDefault();\n            e.stopPropagation();\n        }\n        if (status === 'saving') return;\n        setStatus('saving');\n        try {\n            await onSave();\n            setStatus('success');\n            setTimeout(() => setStatus('idle'), 2000);\n        } catch (err) {\n            console.error('InlineEdit Save Error:', err);\n            setStatus('error');\n            setTimeout(() => setStatus('idle'), 3000);\n        }\n    };\n\n    const handleCancel = (e?: React.MouseEvent) => {\n        if (e) {\n            e.preventDefault();\n            e.stopPropagation();\n        }\n        setStatus('idle');\n        onCancel();\n    };\n\n    const handleEditKeyDown = (e: React.KeyboardEvent) => {\n        e.stopPropagation();\n        const target = e.target as HTMLElement;\n        if (e.key === 'Escape') {\n            e.preventDefault();\n            handleCancel();\n            return;\n        }\n        if (e.key === 'Enter' && target instanceof HTMLInputElement) {\n            e.preventDefault();\n            handleSave();\n        }\n    };\n\n    const feedbackRing = {\n        idle: '',\n        saving: 'ring-1 ring-primary/40',\n        success: 'ring-1 ring-emerald-500/40',\n        error: 'ring-1 ring-destructive/40',\n    }[status];\n\n    return (\n        <div\n            className={cn('relative max-w-full min-w-[20px] [--ie-h:2rem]', className)}\n            // `width`, não `minWidth`: com mínimo a caixa ainda CRESCIA ~18px na\n            // edição (a borda e o padding do input do consumidor são maiores que os\n            // do texto), e crescer empurra o vizinho exatamente como encolher. Só\n            // na edição — em repouso a caixa manda na própria largura.\n            style={isEditing && readWidth.current ? { width: readWidth.current } : undefined}\n        >\n            {!isEditing ? (\n                <div ref={readBoxRef} className=\"flex h-[var(--ie-h)] items-center\">\n                    <button\n                        type=\"button\"\n                        onClick={(e) => {\n                            e.stopPropagation();\n                            onStartEdit();\n                        }}\n                        onKeyDown={(e) => {\n                            if (e.key === 'Enter' || e.key === ' ') e.stopPropagation();\n                        }}\n                        className={cn(\n                            'group/ie flex h-full w-full items-center gap-1.5 rounded-[var(--r-control,0.5rem)] px-1.5 text-left',\n                            // Escopo explícito em vez de `transition-all`: a caixa\n                            // também anima largura quando o valor muda, e deixar a\n                            // largura entrar na transição faz o vizinho tremer.\n                            'transition-[background-color,color] [transition-duration:var(--dur-fast,160ms)] [transition-timing-function:var(--ease-out)]',\n                            'hover:bg-muted/60 focus-visible:outline-none focus-visible:[box-shadow:var(--focus)]',\n                            status === 'error' && 'text-destructive',\n                            triggerClassName\n                        )}\n                    >\n                        {/*\n                          O sublinhado tracejado é a afordância em repouso — a\n                          convenção de \"isto é um campo\" que o texto solto não\n                          tinha. `truncate` mora aqui e não no consumidor.\n                        */}\n                        <span className=\"min-w-0 flex-1 truncate border-b border-dashed border-muted-foreground/35 group-hover/ie:border-muted-foreground/60\">\n                            {value}\n                        </span>\n                        {/*\n                          Espaço do ícone reservado SEMPRE. Se ele só existisse no\n                          hover, cada passada de mouse empurraria o texto — o pulo\n                          que este conserto está tirando da peça.\n                        */}\n                        <span className=\"shrink-0 text-muted-foreground\">\n                            {status === 'success' ? (\n                                <Check className=\"h-3 w-3 text-emerald-600 dark:text-emerald-400\" />\n                            ) : (\n                                <Pencil\n                                    className={cn(\n                                        'h-3 w-3 opacity-40',\n                                        'transition-opacity [transition-duration:var(--dur-fast,160ms)] [transition-timing-function:var(--ease-out)]',\n                                        'group-hover/ie:opacity-90 group-focus-visible/ie:opacity-90'\n                                    )}\n                                />\n                            )}\n                        </span>\n                    </button>\n                </div>\n            ) : (\n                <div\n                    className={cn(\n                        // Mesma altura da leitura: `h-[var(--ie-h)]`, não \"o que o\n                        // input pedir\". É o que impede a linha de crescer no clique.\n                        'flex h-[var(--ie-h)] items-center gap-1 rounded-[var(--r-control,0.5rem)]',\n                        'transition-[box-shadow] [transition-duration:var(--dur-fast,160ms)] [transition-timing-function:var(--ease-out)]',\n                        feedbackRing\n                    )}\n                    onClick={(e) => e.stopPropagation()}\n                    onKeyDown={handleEditKeyDown}\n                >\n                    <div className=\"min-w-0 flex-1\">{children}</div>\n                    {status === 'saving' && (\n                        <Loader2 className=\"h-3 w-3 shrink-0 animate-spin text-muted-foreground\" />\n                    )}\n                    {showButtons && status !== 'saving' && status !== 'success' && (\n                        // Sem `backdrop-blur`: desfoque é linguagem de camada que\n                        // flutua sobre a página (dialog, sheet). Aqui é um controle\n                        // dentro da linha — ele pertence ao plano, não paira sobre ele.\n                        <div className=\"flex shrink-0 items-center gap-0.5 border-l border-border pl-1\">\n                            <button\n                                type=\"button\"\n                                onClick={handleSave}\n                                aria-label=\"Salvar\"\n                                className=\"grid size-6 place-items-center rounded text-emerald-600 outline-none pointer-coarse:size-11 transition-colors [transition-duration:var(--dur-press,120ms)] hover:bg-emerald-500/15 focus-visible:[box-shadow:var(--focus)] dark:text-emerald-400\"\n                            >\n                                <Check className=\"h-3 w-3\" />\n                            </button>\n                            <button\n                                type=\"button\"\n                                onClick={handleCancel}\n                                aria-label=\"Cancelar\"\n                                className=\"grid size-6 place-items-center rounded text-destructive outline-none pointer-coarse:size-11 transition-colors [transition-duration:var(--dur-press,120ms)] hover:bg-destructive/15 focus-visible:[box-shadow:var(--focus)]\"\n                            >\n                                <X className=\"h-3 w-3\" />\n                            </button>\n                        </div>\n                    )}\n                </div>\n            )}\n        </div>\n    );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Editar nome ou campo curto onde ele aparece, em tabela, card ou cabeçalho de página",
      "evite": "Vários campos com validação: profile-form; número arrastável, scrub-input"
    }
  },
  "categories": [
    "controles",
    "edicao",
    "formulario"
  ],
  "type": "registry:ui"
}