{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "settings-section",
  "title": "Settings section",
  "description": "Settings as titled cards: fields, switches that read as sentences, a choice of retention, a save bar that floats up once something changed, and a danger zone that confirms a delete in place.",
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/button.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/field.json",
    "https://sproutboat.com/ikada/r/inline-confirm.json",
    "https://sproutboat.com/ikada/r/input.json",
    "https://sproutboat.com/ikada/r/radio-group.json",
    "https://sproutboat.com/ikada/r/select.json",
    "https://sproutboat.com/ikada/r/switch.json"
  ],
  "files": [
    {
      "path": "registry/ikada/blocks/settings-section/settings-section.tsx",
      "content": "\"use client\";\n\n/**\n * Settings as titled cards: fields, switches that read as sentences, a choice of retention, a save\n * bar that floats up once something changed, and a danger zone that confirms a delete in place.\n */\nimport { useState } from \"react\";\nimport type { FormEvent, ReactNode } from \"react\";\nimport { Button } from \"@/components/ikada/button\";\nimport { Field } from \"@/components/ikada/field\";\nimport { InlineConfirm } from \"@/components/ikada/inline-confirm\";\nimport { Input } from \"@/components/ikada/input\";\nimport { RadioGroup } from \"@/components/ikada/radio-group\";\nimport { Select } from \"@/components/ikada/select\";\nimport { Switch } from \"@/components/ikada/switch\";\nimport { cn } from \"@/components/ikada/lib/cn\";\n\nexport interface ProjectSettings {\n  name: string;\n  slug: string;\n  region: string;\n  notifyFailedDeploys: boolean;\n  weeklySummary: boolean;\n  retentionDays: string;\n}\n\nexport interface SettingsSectionProps {\n  defaultValues?: ProjectSettings;\n  regions?: { value: string; label: string }[];\n  /** Persist the changes. Return a promise to keep Save busy until it settles; throw to keep the form dirty. */\n  onSave?: (values: ProjectSettings) => void | Promise<void>;\n  /** Called once the name has been typed and Delete pressed. */\n  onDelete?: (slug: string) => void;\n  className?: string;\n}\n\n/** Sample data: a small project on the free plan. */\nexport const sampleSettings: ProjectSettings = {\n  name: \"Field notes\",\n  slug: \"field-notes\",\n  region: \"eu-central\",\n  notifyFailedDeploys: true,\n  weeklySummary: false,\n  retentionDays: \"3\",\n};\n\nexport const sampleRegions = [\n  { value: \"eu-central\", label: \"Frankfurt (eu-central)\" },\n  { value: \"eu-west\", label: \"Amsterdam (eu-west)\" },\n  { value: \"us-east\", label: \"Virginia (us-east)\" },\n];\n\nconst RETENTION = [\n  { value: \"1\", label: \"1 day\", description: \"Enough for a live tail and today's errors.\" },\n  { value: \"3\", label: \"3 days\", description: \"The free plan's limit.\" },\n  { value: \"14\", label: \"14 days\", description: \"On paid plans.\", disabled: true },\n];\n\n/** A titled card; the heading names the section for screen readers. */\nfunction Section({\n  id,\n  title,\n  description,\n  action,\n  tone,\n  children,\n}: {\n  id: string;\n  title: string;\n  description?: ReactNode;\n  action?: ReactNode;\n  tone?: \"danger\";\n  children?: ReactNode;\n}) {\n  return (\n    <section\n      aria-labelledby={`${id}-title`}\n      className={cn(\n        \"flex min-w-0 flex-col gap-4 rounded-xl bg-surface p-4 shadow-card @lg:p-5\",\n        tone === \"danger\" && \"shadow-[0_0_0_1px_var(--sb-danger-soft),var(--sb-shadow-card)]\",\n      )}\n    >\n      <div className=\"flex flex-wrap items-start justify-between gap-x-4 gap-y-3\">\n        <div className=\"min-w-0 flex-1 basis-60\">\n          <h2\n            id={`${id}-title`}\n            className={cn(\"text-sb-md font-semibold [overflow-wrap:anywhere]\", tone === \"danger\" && \"text-danger-fg\")}\n          >\n            {title}\n          </h2>\n          {description ? (\n            <p className=\"mt-0.5 text-sb-base text-fg-muted [overflow-wrap:anywhere]\">{description}</p>\n          ) : null}\n        </div>\n        {action ? <div className=\"flex shrink-0 flex-wrap items-center gap-2\">{action}</div> : null}\n      </div>\n      {children}\n    </section>\n  );\n}\n\n/** A setting that reads as a sentence, with its switch at the end of the line. */\nfunction SwitchRow({\n  label,\n  description,\n  checked,\n  onCheckedChange,\n}: {\n  label: string;\n  description: string;\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n}) {\n  return (\n    <label className=\"flex cursor-pointer items-start justify-between gap-4 py-3 first:pt-0 last:pb-0\">\n      <span className=\"min-w-0\">\n        <span className=\"block font-medium [overflow-wrap:anywhere]\">{label}</span>\n        <span className=\"block text-sb-sm text-fg-muted [overflow-wrap:anywhere]\">{description}</span>\n      </span>\n      <Switch checked={checked} onCheckedChange={onCheckedChange} className=\"mt-0.5\" />\n    </label>\n  );\n}\n\nfunction DangerZone({ slug, onDelete }: { slug: string; onDelete?: (slug: string) => void }) {\n  const [open, setOpen] = useState(false);\n  const [typed, setTyped] = useState(\"\");\n  const [deleted, setDeleted] = useState(false);\n  const matches = typed.trim() === slug;\n\n  function submit(event: FormEvent) {\n    event.preventDefault();\n    if (!matches) return;\n    onDelete?.(slug);\n    setDeleted(true);\n  }\n\n  if (deleted) {\n    return (\n      <Section id=\"settings-danger\" tone=\"danger\" title=\"Delete project\" description={`${slug} was deleted.`}>\n        <div>\n          <Button size=\"sm\" variant=\"ghost\" onClick={() => (setDeleted(false), setOpen(false), setTyped(\"\"))}>\n            Reset the sample\n          </Button>\n        </div>\n      </Section>\n    );\n  }\n\n  return (\n    <Section\n      id=\"settings-danger\"\n      tone=\"danger\"\n      title=\"Delete project\"\n      description=\"Removes every sprout in it, their versions, logs and domains. Storage bound to them is kept.\"\n      action={\n        open ? null : (\n          <Button variant=\"danger\" onClick={() => setOpen(true)}>\n            Delete project\n          </Button>\n        )\n      }\n    >\n      {open ? (\n        <form onSubmit={submit} noValidate className=\"flex flex-col gap-3 rounded-lg bg-danger-soft/50 p-3 @lg:p-4\">\n          <Field label={`Type ${slug} to confirm`} description=\"This cannot be undone.\">\n            <Input\n              mono\n              autoFocus\n              autoComplete=\"off\"\n              spellCheck={false}\n              value={typed}\n              onChange={(event) => setTyped(event.target.value)}\n            />\n          </Field>\n          <div className=\"flex flex-wrap justify-end gap-2\">\n            <Button type=\"button\" variant=\"ghost\" onClick={() => (setOpen(false), setTyped(\"\"))}>\n              Cancel\n            </Button>\n            <Button type=\"submit\" variant=\"danger\" disabled={!matches}>\n              Delete {slug}\n            </Button>\n          </div>\n        </form>\n      ) : null}\n    </Section>\n  );\n}\n\nexport function SettingsSection({\n  defaultValues = sampleSettings,\n  regions = sampleRegions,\n  onSave,\n  onDelete,\n  className,\n}: SettingsSectionProps) {\n  const [saved, setSaved] = useState(defaultValues);\n  const [values, setValues] = useState(defaultValues);\n  const [saving, setSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const set = <K extends keyof ProjectSettings>(key: K, value: ProjectSettings[K]) =>\n    setValues((v) => ({ ...v, [key]: value }));\n  const dirty = (Object.keys(values) as (keyof ProjectSettings)[]).some((k) => values[k] !== saved[k]);\n  const nameError = values.name.trim() ? undefined : \"A project needs a name.\";\n\n  async function save(confirm: () => void) {\n    if (!dirty || nameError) return;\n    setSaving(true);\n    setError(null);\n    try {\n      await onSave?.(values);\n      setSaved(values);\n      confirm();\n    } catch {\n      setError(\"Could not save. Try again in a moment.\");\n    } finally {\n      setSaving(false);\n    }\n  }\n\n  return (\n    <div className={cn(\"@container flex flex-col gap-4 text-fg\", className)}>\n      <Section id=\"settings-general\" title=\"General\" description=\"The name is shown in the dashboard and in emails.\">\n        <div className=\"grid grid-cols-[minmax(0,1fr)] gap-4 @xl:grid-cols-[repeat(2,minmax(0,1fr))]\">\n          <Field label=\"Name\" error={nameError}>\n            <Input value={values.name} onChange={(event) => set(\"name\", event.target.value)} autoComplete=\"off\" />\n          </Field>\n          <Field label=\"URL name\" description=\"Part of every sprout's hostname, so it cannot change.\">\n            <Input value={values.slug} mono readOnly disabled />\n          </Field>\n          <Field label=\"Region\" description=\"New sprouts run here.\">\n            <Select\n              value={values.region}\n              options={regions}\n              onValueChange={(next) => typeof next === \"string\" && set(\"region\", next)}\n            />\n          </Field>\n        </div>\n      </Section>\n\n      <Section id=\"settings-notify\" title=\"Notifications\" description=\"Sent to every member who has not opted out.\">\n        <div className=\"flex flex-col divide-y divide-hairline\">\n          <SwitchRow\n            label=\"Email when a deploy fails\"\n            description=\"With the build log's last lines and a link to retry.\"\n            checked={values.notifyFailedDeploys}\n            onCheckedChange={(checked) => set(\"notifyFailedDeploys\", checked)}\n          />\n          <SwitchRow\n            label=\"Weekly usage summary\"\n            description=\"Requests, CPU time and storage, every Monday.\"\n            checked={values.weeklySummary}\n            onCheckedChange={(checked) => set(\"weeklySummary\", checked)}\n          />\n        </div>\n      </Section>\n\n      <Section\n        id=\"settings-logs\"\n        title=\"Log retention\"\n        description=\"How long request and console logs stay searchable.\"\n      >\n        <RadioGroup\n          aria-label=\"Keep logs for\"\n          options={RETENTION}\n          value={values.retentionDays}\n          onValueChange={(next) => typeof next === \"string\" && set(\"retentionDays\", next)}\n        />\n      </Section>\n\n      {/* The save bar keeps its place so nothing jumps; once something changed it sticks to the bottom and floats up. */}\n      <div\n        data-dirty={dirty ? \"\" : undefined}\n        className={cn(\n          \"bottom-3 z-(--sb-z-sticky) flex flex-wrap items-center data-dirty:sticky gap-x-3 gap-y-2 rounded-xl px-4 py-2.5\",\n          \"[transition:background-color_var(--sb-duration-hover)_var(--ease-hover),box-shadow_var(--sb-duration-hover)_var(--ease-hover),translate_var(--sb-duration-spring-snappy)_var(--sb-spring-snappy)]\",\n          \"bg-well shadow-tray\",\n          \"data-dirty:-translate-y-(--sb-offset-press) data-dirty:bg-raised data-dirty:shadow-float\",\n        )}\n      >\n        <p className=\"me-auto text-sb-sm text-fg-muted\" aria-live=\"polite\">\n          {error ? (\n            <span className=\"text-danger-fg\">{error}</span>\n          ) : dirty ? (\n            \"You have unsaved changes.\"\n          ) : (\n            \"Everything is saved.\"\n          )}\n        </p>\n        <div className=\"ms-auto flex items-center gap-2\">\n          <Button\n            size=\"sm\"\n            variant=\"ghost\"\n            disabled={!dirty || saving}\n            onClick={() => (setValues(saved), setError(null))}\n          >\n            Discard\n          </Button>\n          <InlineConfirm message=\"Saved\">\n            {({ confirm }) => (\n              <Button\n                size=\"sm\"\n                variant=\"primary\"\n                disabled={!dirty || Boolean(nameError)}\n                loading={saving}\n                onClick={() => void save(confirm)}\n              >\n                Save changes\n              </Button>\n            )}\n          </InlineConfirm>\n        </div>\n      </div>\n\n      <DangerZone slug={values.slug} onDelete={onDelete} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ikada/blocks/settings-section/settings-section.tsx"
    }
  ],
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}