{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "form-panel",
  "type": "registry:block",
  "title": "Validated form panel",
  "description": "Typed fields, validation links, failed drafts, guarded save and confirmed reset.",
  "registryDependencies": [
    "button",
    "input",
    "label",
    "alert-dialog"
  ],
  "files": [
    {
      "path": "blocks/form-panel.tsx",
      "type": "registry:component",
      "target": "components/shine/form-panel.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 { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from \"@/components/ui/alert-dialog\";\nexport type FormField = { name: string; label: string; type?: \"text\" | \"email\" | \"number\" | \"date\" | \"textarea\" | \"select\" | \"checkbox\"; required?: boolean; help?: string; options?: { value: string; label: string }[]; validate?: (value: string) => string | undefined };\n/** Mount with a key when record identity changes. Successful saves become the reset baseline. */\nexport function FormPanel({ title, description, fields, initialValues, onSave, saveLabel = \"Save changes\" }: {\n title: string; description: string; fields: FormField[]; initialValues: Record<string, string>; onSave: (value: Record<string, string>) => Promise<void>; saveLabel?: string;\n}) {\n const id = useId(), lock = useRef(false), alert = useRef<HTMLDivElement>(null);\n const [baseline, setBaseline] = useState({ ...initialValues }), [values, setValues] = useState({ ...initialValues });\n const [busy, setBusy] = useState(false), [discard, setDiscard] = useState(false), [error, setError] = useState(\"\");\n const [errors, setErrors] = useState<Record<string, string>>({}), [status, setStatus] = useState(\"\");\n const dirty = fields.some(field => (values[field.name] || \"\") !== (baseline[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) alert.current?.focus(); }, [error, errors]);\n const change = (name: string, value: string) => { setValues(current => ({ ...current, [name]: value })); setStatus(\"\"); };\n return <section data-product-pattern=\"form-panel\" aria-labelledby={id + \"-heading\"} className=\"min-w-0 rounded-lg border border-border bg-background\">\n  <header className=\"space-y-2 border-b border-border p-5\"><h2 id={id + \"-heading\"} className=\"text-lg font-semibold\">{title}</h2><p className=\"text-sm text-muted-foreground\">{description}</p></header>\n  <form noValidate onSubmit={async event => {\n   event.preventDefault(); if (lock.current) return;\n   const invalid: Record<string, string> = {};\n   for (const field of fields) { const value = values[field.name] || \"\"; const message = field.required && (field.type === \"checkbox\" ? value !== \"true\" : !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) invalid[field.name] = message; }\n   setErrors(invalid); setError(\"\"); setStatus(\"\"); if (Object.keys(invalid).length) return;\n   lock.current = true; setBusy(true); const submitted = { ...values };\n   try { await onSave(submitted); setBaseline(submitted); setStatus(\"Changes saved.\"); } catch (cause) { setError(cause instanceof Error ? cause.message : \"Could not save. Your draft is still here.\"); } finally { lock.current = false; setBusy(false); }\n  }}>\n   <div className=\"space-y-5 p-5\">{(error || Object.keys(errors).length > 0) && <div role=\"alert\" ref={alert} tabIndex={-1} className=\"rounded-md border border-destructive p-3 text-sm\"><p>{error || \"Review these fields.\"}</p>{Object.entries(errors).map(([name, message]) => <a key={name} href={\"#\" + id + \"-field-\" + name} className=\"block underline\">{message}</a>)}</div>}\n   {fields.map(field => { const common = { id: id + \"-field-\" + field.name, name: field.name, disabled: busy, required: field.required, \"aria-invalid\": Boolean(errors[field.name]), \"aria-describedby\": field.help || errors[field.name] ? id + \"-field-\" + field.name + \"-help\" : undefined }; return <div key={field.name} className=\"space-y-2\"><Label htmlFor={common.id}>{field.label}{field.required ? \" (required)\" : \"\"}</Label>\n    {field.type === \"textarea\" ? <textarea {...common} rows={4} className=\"w-full rounded-md border border-border bg-background p-3 text-sm\" value={values[field.name] || \"\"} onChange={e => change(field.name, e.target.value)} /> : field.type === \"select\" ? <select {...common} className=\"h-11 w-full rounded-md border border-border bg-background px-3 text-sm\" value={values[field.name] || \"\"} onChange={e => change(field.name, e.target.value)}><option value=\"\">Choose an option</option>{field.options?.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select> : field.type === \"checkbox\" ? <input {...common} type=\"checkbox\" className=\"block size-5 accent-primary\" checked={values[field.name] === \"true\"} onChange={e => change(field.name, String(e.target.checked))} /> : <Input {...common} className=\"h-11\" type={field.type || \"text\"} value={values[field.name] || \"\"} onChange={e => change(field.name, e.target.value)} />}\n    {(field.help || errors[field.name]) && <p id={common.id + \"-help\"} className=\"text-sm text-muted-foreground\">{errors[field.name] || field.help}</p>}</div>; })}</div>\n   <footer className=\"flex flex-wrap items-center justify-between gap-3 border-t border-border p-4\"><p role=\"status\" className=\"text-sm text-muted-foreground\">{status || (dirty ? \"Unsaved changes\" : \"\")}</p><div className=\"flex flex-wrap gap-2\"><Button type=\"button\" variant=\"outline\" disabled={busy || !dirty} onClick={() => setDiscard(true)}>Reset changes</Button><Button type=\"submit\" disabled={busy}>{busy ? \"Saving…\" : saveLabel}</Button></div></footer>\n  </form>\n  <AlertDialog open={discard} onOpenChange={setDiscard}><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Discard unsaved changes?</AlertDialogTitle><AlertDialogDescription>Restore the last saved values.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>Keep editing</AlertDialogCancel><AlertDialogAction onClick={() => { setValues({ ...baseline }); setErrors({}); setError(\"\"); setStatus(\"Changes reset.\"); }}>Discard changes</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog>\n </section>;\n}\n"
    }
  ]
}
