{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "env-editor",
  "type": "registry:component",
  "title": "Env Editor",
  "description": "Environment variables as a table with a column per environment: secrets masked until you reveal them, click-to-edit cells, paste a whole .env to bulk import, and every unsaved edit tinted until you save or discard.",
  "categories": [
    "component",
    "Dev Tools"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/select.json",
    "https://coss.com/ui/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/devl/env-editor.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, ClipboardPasteIcon, EyeIcon, EyeOffIcon, LockIcon, PlusIcon, Trash2Icon, Undo2Icon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ClipboardEvent, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface EnvEnvironment {\n  id: string;\n  name: string;\n}\n\nexport interface EnvVariable {\n  key: string;\n  values: Record<string, string | undefined>;\n  secret?: boolean;\n}\n\nexport interface EnvEditorProps {\n  environments: EnvEnvironment[];\n  variables: EnvVariable[];\n  project?: string;\n  onSave?: (variables: EnvVariable[]) => void | Promise<void>;\n  className?: string;\n}\n\ntype Row = EnvVariable & { uid: string; removed?: boolean };\n\nconst MASK = \"••••••••\";\nconst KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst spring = { type: \"spring\", stiffness: 480, damping: 38 } as const;\n\nconst isSecret = (v: EnvVariable) =>\n  v.secret ?? (!/^(NEXT_PUBLIC_|PUBLIC_|VITE_)/.test(v.key) && /SECRET|TOKEN|KEY|PASSWORD|_URL$|DSN/.test(v.key));\n\nlet seq = 0;\nconst toRows = (vars: EnvVariable[]): Row[] => vars.map((v) => ({ ...v, values: { ...v.values }, uid: `v${++seq}` }));\n\nfunction parseDotenv(text: string) {\n  const entries: [string, string][] = [];\n  let skipped = 0;\n  for (const raw of text.split(/\\r?\\n/)) {\n    const line = raw.trim();\n    if (!line || line.startsWith(\"#\")) continue;\n    const m = line.replace(/^export\\s+/, \"\").match(/^([^=\\s]+)\\s*=\\s*(.*)$/);\n    if (!m || !KEY_RE.test(m[1])) {\n      skipped++;\n      continue;\n    }\n    let value = m[2].replace(/\\s+#.*$/, \"\");\n    if (/^([\"']).*\\1$/.test(value)) value = value.slice(1, -1);\n    entries.push([m[1], value]);\n  }\n  return { entries, skipped };\n}\n\nfunction IconButton({ label, onClick, children, className }: { label: string; onClick: () => void; children: React.ReactNode; className?: string }) {\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={\n          <button\n            aria-label={label}\n            className={cn(\n              \"grid size-8 shrink-0 place-items-center rounded-lg text-muted-foreground outline-none transition-[color,background-color,scale,opacity] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] [@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-foreground\",\n              className,\n            )}\n            onClick={onClick}\n            type=\"button\"\n          />\n        }\n      >\n        {children}\n      </TooltipTrigger>\n      <TooltipPopup>{label}</TooltipPopup>\n    </Tooltip>\n  );\n}\n\nexport function EnvEditor({ environments, variables, project, onSave, className }: EnvEditorProps) {\n  const reduce = useReducedMotion();\n  const [baseline, setBaseline] = useState<Row[]>(() => toRows(variables));\n  const [rows, setRows] = useState<Row[]>(() => baseline.map((r) => ({ ...r, values: { ...r.values } })));\n  const [revealed, setRevealed] = useState<Set<string>>(new Set());\n  const [editing, setEditing] = useState<{ uid: string; env: string } | null>(null);\n  const [draft, setDraft] = useState(\"\");\n  const [importOpen, setImportOpen] = useState(false);\n  const [importText, setImportText] = useState(\"\");\n  const [importEnv, setImportEnv] = useState(environments[0]?.id ?? \"\");\n  const [saving, setSaving] = useState<\"idle\" | \"saving\" | \"saved\">(\"idle\");\n  const [newKey, setNewKey] = useState<string | null>(null);\n  const keyInput = useRef<HTMLInputElement>(null);\n\n  const base = useMemo(() => new Map(baseline.map((r) => [r.uid, r])), [baseline]);\n\n  const cellChanged = (r: Row, env: string) => {\n    const b = base.get(r.uid);\n    return !b || (b.values[env] ?? \"\") !== (r.values[env] ?? \"\");\n  };\n\n  const changes = useMemo(() => {\n    let added = 0;\n    let removed = 0;\n    let edited = 0;\n    for (const r of rows) {\n      const b = base.get(r.uid);\n      if (!b) {\n        if (!r.removed) added++;\n      } else if (r.removed) removed++;\n      else if (environments.some((e) => (b.values[e.id] ?? \"\") !== (r.values[e.id] ?? \"\"))) edited++;\n    }\n    return { added, removed, edited, total: added + removed + edited };\n  }, [rows, base, environments]);\n\n  useEffect(() => {\n    if (saving !== \"saved\") return;\n    const t = setTimeout(() => setSaving(\"idle\"), 1600);\n    return () => clearTimeout(t);\n  }, [saving]);\n\n  useEffect(() => {\n    if (newKey !== null) keyInput.current?.focus();\n  }, [newKey]);\n\n  const allRevealed = rows.filter(isSecret).every((r) => revealed.has(r.uid));\n\n  const startEdit = (r: Row, env: string) => {\n    if (r.removed) return;\n    setEditing({ uid: r.uid, env });\n    setDraft(r.values[env] ?? \"\");\n  };\n\n  const commitEdit = (save: boolean) => {\n    if (!editing) return;\n    if (save) {\n      setRows((rs) =>\n        rs.map((r) => (r.uid === editing.uid ? { ...r, values: { ...r.values, [editing.env]: draft === \"\" ? undefined : draft } } : r)),\n      );\n    }\n    setEditing(null);\n  };\n\n  const parsed = useMemo(() => parseDotenv(importText), [importText]);\n  const importSummary = useMemo(() => {\n    let fresh = 0;\n    let updated = 0;\n    for (const [k, v] of parsed.entries) {\n      const row = rows.find((r) => r.key === k && !r.removed);\n      if (!row) fresh++;\n      else if ((row.values[importEnv] ?? \"\") !== v) updated++;\n    }\n    return { fresh, updated };\n  }, [parsed, rows, importEnv]);\n\n  const applyImport = () => {\n    setRows((rs) => {\n      const next = rs.map((r) => ({ ...r, values: { ...r.values } }));\n      for (const [k, v] of parsed.entries) {\n        const row = next.find((r) => r.key === k && !r.removed);\n        if (row) row.values[importEnv] = v;\n        else next.push({ key: k, values: { [importEnv]: v }, uid: `v${++seq}` });\n      }\n      return next;\n    });\n    setImportText(\"\");\n    setImportOpen(false);\n  };\n\n  const onPaste = (e: ClipboardEvent) => {\n    const target = e.target as HTMLElement;\n    if (target.closest(\"input, textarea\")) return;\n    const text = e.clipboardData.getData(\"text\");\n    if (!/^\\s*(export\\s+)?[A-Za-z_]\\w*\\s*=/m.test(text)) return;\n    e.preventDefault();\n    setImportText(text);\n    setImportOpen(true);\n  };\n\n  const addVariable = () => {\n    const key = (newKey ?? \"\").trim().toUpperCase();\n    if (!KEY_RE.test(key) || rows.some((r) => r.key === key && !r.removed)) return;\n    const row: Row = { key, values: {}, uid: `v${++seq}` };\n    setRows((rs) => [...rs, row]);\n    setNewKey(null);\n    setTimeout(() => startEdit(row, environments[0].id), 0);\n  };\n\n  const discard = () => {\n    setRows(baseline.map((r) => ({ ...r, values: { ...r.values } })));\n    setEditing(null);\n  };\n\n  const save = async () => {\n    setSaving(\"saving\");\n    const kept = rows.filter((r) => !r.removed);\n    const clean = kept.map(({ key, values, secret }) => ({ key, values, secret }));\n    await onSave?.(clean);\n    setBaseline(kept.map((r) => ({ ...r, values: { ...r.values } })));\n    setRows(kept);\n    setSaving(\"saved\");\n  };\n\n  const newKeyError =\n    newKey && newKey.trim()\n      ? !KEY_RE.test(newKey.trim())\n        ? \"Letters, digits and _ only\"\n        : rows.some((r) => r.key === newKey.trim().toUpperCase() && !r.removed)\n          ? \"Already exists\"\n          : null\n      : null;\n\n  const cols = `minmax(224px,1.5fr) repeat(${environments.length}, minmax(118px,1fr)) 36px`;\n  const showFooter = changes.total > 0 || saving !== \"idle\";\n\n  return (\n    <TooltipProvider delay={500}>\n      <div className={cn(\"flex w-full min-w-0 max-w-[640px] flex-col overflow-hidden rounded-2xl border bg-card text-sm shadow-xs/5\", className)} onPaste={onPaste}>\n        <div className=\"flex items-center gap-3 px-4 py-3\">\n          <div className=\"min-w-0 flex-1\">\n            <h3 className=\"truncate font-medium\">Environment variables</h3>\n            <p className=\"truncate text-muted-foreground text-xs\">\n              {project ? `${project} · ` : \"\"}\n              {rows.filter((r) => !r.removed).length} variables<span className=\"max-sm:hidden\"> · paste a .env anywhere here</span>\n            </p>\n          </div>\n          <IconButton label={allRevealed ? \"Hide all values\" : \"Reveal all values\"} onClick={() => setRevealed(allRevealed ? new Set() : new Set(rows.map((r) => r.uid)))}>\n            {allRevealed ? <EyeOffIcon className=\"size-4\" /> : <EyeIcon className=\"size-4\" />}\n          </IconButton>\n          <Button aria-expanded={importOpen} onClick={() => setImportOpen((o) => !o)} size=\"sm\" variant=\"outline\">\n            <ClipboardPasteIcon />\n            Import<span className=\"-ms-1 max-sm:hidden\">.env</span>\n          </Button>\n        </div>\n\n        <AnimatePresence initial={false}>\n          {importOpen && (\n            <motion.div\n              animate={{ height: \"auto\", opacity: 1 }}\n              className=\"overflow-hidden\"\n              exit={{ height: 0, opacity: 0 }}\n              initial={{ height: 0, opacity: 0 }}\n              transition={reduce ? { duration: 0 } : { duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <div className=\"mx-4 mb-3 flex flex-col gap-2.5 rounded-xl border bg-muted/40 p-3\">\n                <textarea\n                  aria-label=\"Paste .env contents\"\n                  autoFocus\n                  className=\"h-24 w-full resize-none rounded-lg border border-input bg-background px-2.5 py-2 font-mono text-xs leading-5 shadow-xs/5 outline-none ring-ring/24 placeholder:text-muted-foreground/72 focus-visible:border-ring focus-visible:ring-[3px] dark:bg-input/32\"\n                  onChange={(e) => setImportText(e.target.value)}\n                  placeholder={\"# Paste KEY=value lines\\nREDIS_URL=redis://localhost:6379\\nEMAIL_FROM=hello@northwind.dev\"}\n                  spellCheck={false}\n                  value={importText}\n                />\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <span className=\"text-muted-foreground text-xs\">Into</span>\n                  <Select onValueChange={(v) => v && setImportEnv(String(v))} value={importEnv}>\n                    <SelectTrigger aria-label=\"Target environment\" className=\"h-7 w-auto min-w-32 text-xs sm:min-h-7 sm:text-xs\" size=\"sm\">\n                      <SelectValue>{(v: string) => environments.find((e) => e.id === v)?.name}</SelectValue>\n                    </SelectTrigger>\n                    <SelectPopup>\n                      {environments.map((e) => (\n                        <SelectItem key={e.id} value={e.id}>\n                          {e.name}\n                        </SelectItem>\n                      ))}\n                    </SelectPopup>\n                  </Select>\n                  <span aria-live=\"polite\" className=\"flex-1 text-muted-foreground text-xs tabular-nums\">\n                    {parsed.entries.length > 0 && (\n                      <>\n                        {importSummary.fresh > 0 && <span className=\"text-success-foreground\">{importSummary.fresh} new</span>}\n                        {importSummary.fresh > 0 && importSummary.updated > 0 && \" · \"}\n                        {importSummary.updated > 0 && <span className=\"text-warning-foreground\">{importSummary.updated} updated</span>}\n                        {importSummary.fresh + importSummary.updated === 0 && \"No changes\"}\n                      </>\n                    )}\n                    {parsed.skipped > 0 && <span> · {parsed.skipped} skipped</span>}\n                  </span>\n                  <Button onClick={() => setImportOpen(false)} size=\"sm\" variant=\"ghost\">\n                    Cancel\n                  </Button>\n                  <Button disabled={importSummary.fresh + importSummary.updated === 0} onClick={applyImport} size=\"sm\">\n                    Add to table\n                  </Button>\n                </div>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <div className=\"relative overflow-x-auto border-t [scrollbar-width:thin]\">\n          <div className=\"min-w-fit\">\n            <div aria-label=\"Environment variables\" role=\"table\">\n            <div className=\"grid border-b bg-card bg-linear-to-r from-muted/40 to-muted/40 text-muted-foreground text-xs\" role=\"row\" style={{ gridTemplateColumns: cols }}>\n              <div className=\"sticky left-0 z-10 bg-card bg-linear-to-r from-muted/40 to-muted/40 px-4 py-2 font-medium\" role=\"columnheader\">\n                Key\n              </div>\n              {environments.map((e) => (\n                <div className=\"px-3 py-2 font-medium\" key={e.id} role=\"columnheader\">\n                  {e.name}\n                </div>\n              ))}\n              <div role=\"columnheader\">\n                <span className=\"sr-only\">Actions</span>\n              </div>\n            </div>\n\n            <AnimatePresence initial={false}>\n              {rows.map((r) => {\n                const isNew = !base.has(r.uid);\n                const secret = isSecret(r);\n                const shown = !secret || revealed.has(r.uid);\n                const rowTone = r.removed ? \"bg-destructive/6\" : isNew ? \"bg-success/6\" : \"\";\n                return (\n                  <motion.div\n                    animate={{ opacity: 1, height: \"auto\" }}\n                    className={cn(\"group/row grid border-b last:border-b-0\", rowTone)}\n                    exit={{ opacity: 0, height: 0 }}\n                    initial={reduce ? false : { opacity: 0, height: 0 }}\n                    key={r.uid}\n                    role=\"row\"\n                    style={{ gridTemplateColumns: cols }}\n                    transition={reduce ? { duration: 0 } : { duration: 0.2, ease: [0.23, 1, 0.32, 1] }}\n                  >\n                    <div\n                      className={cn(\n                        \"sticky left-0 z-10 flex min-w-0 items-center gap-1.5 bg-card py-1 ps-4 pe-2\",\n                        r.removed && \"bg-[color-mix(in_srgb,var(--color-destructive)_6%,var(--color-card))]\",\n                        isNew && !r.removed && \"bg-[color-mix(in_srgb,var(--color-success)_6%,var(--color-card))]\",\n                      )}\n                      role=\"rowheader\"\n                    >\n                      {secret ? <LockIcon aria-label=\"Secret\" className=\"size-3 shrink-0 text-muted-foreground/70\" /> : <span className=\"w-3 shrink-0\" />}\n                      <span className={cn(\"min-w-0 flex-1 truncate font-medium font-mono text-xs\", r.removed && \"text-muted-foreground line-through\")} title={r.key}>\n                        {r.key}\n                      </span>\n                      {isNew && !r.removed && (\n                        <span className=\"shrink-0 rounded-full bg-success/12 px-1.5 font-medium text-[10px] text-success-foreground leading-4\">new</span>\n                      )}\n                      {secret && !r.removed && (\n                        <IconButton\n                          label={shown ? `Hide ${r.key}` : `Reveal ${r.key}`}\n                          onClick={() =>\n                            setRevealed((s) => {\n                              const n = new Set(s);\n                              if (n.has(r.uid)) n.delete(r.uid);\n                              else n.add(r.uid);\n                              return n;\n                            })\n                          }\n                        >\n                          {shown ? <EyeOffIcon className=\"size-3.5\" /> : <EyeIcon className=\"size-3.5\" />}\n                        </IconButton>\n                      )}\n                    </div>\n\n                    {environments.map((e) => {\n                      const value = r.values[e.id];\n                      const changed = !isNew && !r.removed && cellChanged(r, e.id);\n                      const isEditing = editing?.uid === r.uid && editing.env === e.id;\n                      const before = base.get(r.uid)?.values[e.id];\n                      return (\n                        <div className={cn(\"relative min-w-0 px-1.5 py-1.5\", changed && \"bg-warning/10\")} key={e.id} role=\"cell\">\n                          {changed && <span aria-hidden className=\"absolute top-1/2 left-1 size-1 -translate-y-1/2 rounded-full bg-warning\" />}\n                          {isEditing ? (\n                            <input\n                              aria-label={`${r.key} in ${e.name}`}\n                              autoFocus\n                              className=\"h-7 w-full min-w-0 rounded-md border border-ring bg-background px-1.5 font-mono text-xs outline-none ring-[3px] ring-ring/24 dark:bg-input/32\"\n                              onBlur={() => commitEdit(true)}\n                              onChange={(ev) => setDraft(ev.target.value)}\n                              onFocus={(ev) => ev.currentTarget.select()}\n                              onKeyDown={(ev) => {\n                                if (ev.key === \"Enter\") commitEdit(true);\n                                if (ev.key === \"Escape\") commitEdit(false);\n                              }}\n                              placeholder=\"Not set\"\n                              spellCheck={false}\n                              value={draft}\n                            />\n                          ) : (\n                            <button\n                              aria-label={`Edit ${r.key} in ${e.name}${changed ? \" (changed)\" : \"\"}`}\n                              className={cn(\n                                \"flex h-7 w-full min-w-0 items-center rounded-md px-1.5 text-start font-mono text-xs outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default [@media(hover:hover)]:hover:bg-accent\",\n                                r.removed && \"text-muted-foreground line-through\",\n                              )}\n                              disabled={r.removed}\n                              onClick={() => startEdit(r, e.id)}\n                              title={changed ? `Was: ${before === undefined ? \"not set\" : shown ? before : MASK}` : undefined}\n                              type=\"button\"\n                            >\n                              {value === undefined ? (\n                                <span className=\"text-muted-foreground/60\">—</span>\n                              ) : shown ? (\n                                <span className=\"truncate\" title={value}>{value || <span className=\"text-muted-foreground\">\"\"</span>}</span>\n                              ) : (\n                                <span className=\"tracking-[0.12em] text-muted-foreground\">{MASK}</span>\n                              )}\n                            </button>\n                          )}\n                        </div>\n                      );\n                    })}\n                    <div className=\"flex items-center justify-center\" role=\"cell\">\n                      {r.removed ? (\n                        <IconButton label={`Restore ${r.key}`} onClick={() => setRows((rs) => rs.map((x) => (x.uid === r.uid ? { ...x, removed: false } : x)))}>\n                          <Undo2Icon className=\"size-3.5\" />\n                        </IconButton>\n                      ) : (\n                        <IconButton\n                          className=\"opacity-0 focus-visible:opacity-100 group-focus-within/row:opacity-100 pointer-coarse:opacity-100 [@media(hover:hover)]:group-hover/row:opacity-100\"\n                          label={`Delete ${r.key}`}\n                          onClick={() =>\n                            isNew ? setRows((rs) => rs.filter((x) => x.uid !== r.uid)) : setRows((rs) => rs.map((x) => (x.uid === r.uid ? { ...x, removed: true } : x)))\n                          }\n                        >\n                          <Trash2Icon className=\"size-3.5\" />\n                        </IconButton>\n                      )}\n                    </div>\n                  </motion.div>\n                );\n              })}\n            </AnimatePresence>\n            </div>\n\n            <div className=\"sticky left-0 flex w-full max-w-[640px] items-center gap-2 border-t px-3 py-2\">\n              {newKey === null ? (\n                <Button className=\"text-muted-foreground\" onClick={() => setNewKey(\"\")} size=\"sm\" variant=\"ghost\">\n                  <PlusIcon />\n                  Add variable\n                </Button>\n              ) : (\n                <form\n                  className=\"flex min-w-0 flex-1 items-center gap-2\"\n                  onSubmit={(ev) => {\n                    ev.preventDefault();\n                    addVariable();\n                  }}\n                >\n                  <input\n                    aria-invalid={Boolean(newKeyError) || undefined}\n                    aria-label=\"New variable name\"\n                    className=\"h-7 w-52 min-w-0 rounded-md border border-input bg-background px-2 font-mono text-xs uppercase shadow-xs/5 outline-none ring-ring/24 placeholder:normal-case placeholder:text-muted-foreground/72 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/50 dark:bg-input/32\"\n                    onBlur={() => !newKey?.trim() && setNewKey(null)}\n                    onChange={(ev) => setNewKey(ev.target.value)}\n                    onKeyDown={(ev) => ev.key === \"Escape\" && setNewKey(null)}\n                    placeholder=\"NEW_VARIABLE\"\n                    ref={keyInput}\n                    spellCheck={false}\n                    value={newKey}\n                  />\n                  <Button disabled={!newKey.trim() || Boolean(newKeyError)} size=\"sm\" type=\"submit\" variant=\"outline\">\n                    Add\n                  </Button>\n                  {newKeyError && <span className=\"truncate text-destructive-foreground text-xs\">{newKeyError}</span>}\n                </form>\n              )}\n            </div>\n          </div>\n        </div>\n\n        <AnimatePresence initial={false}>\n          {showFooter && (\n            <motion.div\n              animate={{ height: \"auto\", opacity: 1 }}\n              className=\"overflow-hidden\"\n              exit={{ height: 0, opacity: 0 }}\n              initial={{ height: 0, opacity: 0 }}\n              transition={reduce ? { duration: 0 } : spring}\n            >\n              <div className=\"flex items-center gap-3 border-t bg-muted/40 px-4 py-2.5\">\n                <AnimatePresence initial={false} mode=\"popLayout\">\n                  <motion.p\n                    animate={{ opacity: 1, y: 0 }}\n                    className=\"min-w-0 flex-1 truncate text-xs\"\n                    exit={{ opacity: 0, y: -6 }}\n                    initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}\n                    key={saving === \"saved\" ? \"saved\" : \"dirty\"}\n                    transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n                  >\n                    {saving === \"saved\" ? (\n                      <span className=\"flex items-center gap-1.5 text-success-foreground\">\n                        <CheckIcon className=\"size-3.5\" /> Saved · redeploy to apply\n                      </span>\n                    ) : (\n                      <>\n                        <span className=\"font-medium tabular-nums\">\n                          {changes.total} unsaved {changes.total === 1 ? \"change\" : \"changes\"}\n                        </span>\n                        <span className=\"text-muted-foreground tabular-nums max-sm:hidden\">\n                          {changes.added > 0 && ` · ${changes.added} added`}\n                          {changes.edited > 0 && ` · ${changes.edited} edited`}\n                          {changes.removed > 0 && ` · ${changes.removed} removed`}\n                        </span>\n                      </>\n                    )}\n                  </motion.p>\n                </AnimatePresence>\n                {saving !== \"saved\" && (\n                  <>\n                    <Button disabled={saving === \"saving\"} onClick={discard} size=\"sm\" variant=\"ghost\">\n                      Discard\n                    </Button>\n                    <Button loading={saving === \"saving\"} onClick={save} size=\"sm\">\n                      Save changes\n                    </Button>\n                  </>\n                )}\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </TooltipProvider>\n  );\n}\n"
    }
  ]
}