{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "record-editor",
  "type": "registry:block",
  "title": "Record editor",
  "description": "Dialog editor with validation, asynchronous save, retained failed drafts, discard confirmation and focus restoration.",
  "registryDependencies": [
    "button",
    "input",
    "label",
    "dialog",
    "alert-dialog"
  ],
  "files": [
    {
      "path": "blocks/record-editor.tsx",
      "type": "registry:component",
      "target": "components/shine/record-editor.tsx",
      "content": "\"use client\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from \"@/components/ui/dialog\";\nimport { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from \"@/components/ui/alert-dialog\";\n\nexport type EditorField = { name: string; label: string; type?: \"text\" | \"email\" | \"number\" | \"date\"; required?: boolean; help?: string; validate?: (value: string) => string | undefined };\n/** Mount per record (key=record.id). Success closes only after the real save resolves. */\nexport function RecordEditor({ title, description, initialValues, fields, onSave, onClose, returnFocus }: {\n  title: string; description: string; initialValues: Record<string, string>; fields: EditorField[];\n  onSave: (values: Record<string, string>) => Promise<void>; onClose: () => void; returnFocus?: () => HTMLElement | null;\n}) {\n  const id = useId(), saving = useRef(false), errorRef = useRef<HTMLDivElement>(null);\n  const opener = useRef<HTMLElement | null>(typeof document === \"undefined\" ? null : document.activeElement as HTMLElement);\n  const [values, setValues] = useState({ ...initialValues });\n  const [busy, setBusy] = useState(false), [confirm, setConfirm] = useState(false);\n  const [errors, setErrors] = useState<Record<string, string>>({}), [error, setError] = useState(\"\");\n  const dirty = fields.some(field => (values[field.name] || \"\") !== (initialValues[field.name] || \"\"));\n  useEffect(() => { if (!dirty) return; const handler = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = \"\"; }; window.addEventListener(\"beforeunload\", handler); return () => window.removeEventListener(\"beforeunload\", handler); }, [dirty]);\n  useEffect(() => { if (error || Object.keys(errors).length) errorRef.current?.focus(); }, [error, errors]);\n  const close = () => { if (!saving.current) { if (dirty) setConfirm(true); else onClose(); } };\n  return <>\n    <Dialog open onOpenChange={open => { if (!open) close(); }}>\n      <DialogContent data-product-pattern=\"record-editor\" className=\"flex max-h-[90dvh] flex-col overflow-hidden p-0\" onCloseAutoFocus={event => { event.preventDefault(); const target=returnFocus?.()||opener.current; if(target?.isConnected)target.focus(); }} onInteractOutside={event => { event.preventDefault(); close(); }} onEscapeKeyDown={event => { event.preventDefault(); close(); }}>\n        <DialogHeader className=\"shrink-0 border-b p-6\"><DialogTitle>{title}</DialogTitle><DialogDescription>{description}</DialogDescription></DialogHeader>\n        <form noValidate className=\"flex min-h-0 flex-col\" onSubmit={async event => {\n          event.preventDefault(); if (saving.current) return;\n          const next: Record<string, string> = {};\n          for (const field of fields) { const value = values[field.name] || \"\"; const message = field.required && !value.trim() ? `${field.label} is required` : field.type === \"email\" && value && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value) ? \"Enter a valid email address\" : field.validate?.(value); if (message) next[field.name] = message; }\n          setErrors(next); setError(\"\"); if (Object.keys(next).length) return;\n          saving.current = true; setBusy(true);\n          try { await onSave({ ...values }); onClose(); }\n          catch (cause) { setError(cause instanceof Error ? cause.message : \"Could not save. Your changes are still here.\"); }\n          finally { saving.current = false; setBusy(false); }\n        }}>\n          <div className=\"min-h-0 space-y-4 overflow-y-auto p-6\">\n            {(error || Object.keys(errors).length > 0) && <div ref={errorRef} tabIndex={-1} role=\"alert\" className=\"rounded-md border border-destructive p-3 text-sm\"><p>{error || \"Review the fields below.\"}</p>{Object.entries(errors).map(([name, message]) => <a key={name} className=\"block underline\" href={`#${id}-${name}`}>{message}</a>)}</div>}\n            {fields.map(field => <div className=\"space-y-2\" key={field.name}>\n              <Label htmlFor={`${id}-${field.name}`}>{field.label}{field.required ? \" (required)\" : \"\"}</Label>\n              <Input className=\"h-11\" id={`${id}-${field.name}`} name={field.name} type={field.type || \"text\"} value={values[field.name] || \"\"} disabled={busy} required={field.required} aria-invalid={Boolean(errors[field.name])} aria-describedby={field.help || errors[field.name] ? `${id}-${field.name}-help` : undefined} onChange={event => setValues(current => ({ ...current, [field.name]: event.target.value }))} />\n              {(field.help || errors[field.name]) && <p id={`${id}-${field.name}-help`} className=\"text-sm text-muted-foreground\">{errors[field.name] || field.help}</p>}\n            </div>)}\n          </div>\n          <DialogFooter className=\"shrink-0 border-t p-4\"><Button className=\"min-h-11\" type=\"button\" variant=\"outline\" disabled={busy} onClick={close}>Cancel</Button><Button className=\"min-h-11\" type=\"submit\" disabled={busy}>{busy ? \"Saving…\" : \"Save changes\"}</Button></DialogFooter>\n        </form>\n      </DialogContent>\n    </Dialog>\n    <AlertDialog open={confirm} onOpenChange={setConfirm}><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Discard unsaved changes?</AlertDialogTitle><AlertDialogDescription>Your changes have not been saved.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>Keep editing</AlertDialogCancel><AlertDialogAction onClick={onClose}>Discard changes</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog>\n  </>;\n}\n"
    }
  ]
}
