{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"settings-block","type":"registry:block","title":"Workspace settings","description":"A labelled settings form with local persistence, draft recovery and honest save errors.","registryDependencies":["https://mizu.user64bit.world/r/button.json","https://mizu.user64bit.world/r/select-field.json","https://mizu.user64bit.world/r/status.json","https://mizu.user64bit.world/r/switch.json","https://mizu.user64bit.world/r/text-field.json"],"meta":{"client":true},"files":[{"path":"registry/blocks/SettingsBlock.tsx","type":"registry:component","target":"@ui/mizu/blocks/SettingsBlock.tsx","content":"\"use client\";\n\nimport { useState, useSyncExternalStore } from \"react\";\nimport { Button } from \"../button\";\nimport { SelectField } from \"../select-field\";\nimport { Status } from \"../status\";\nimport { Switch } from \"../switch\";\nimport { TextField } from \"../text-field\";\nimport \"./blocks.css\";\n\ntype Settings = {\n  name: string;\n  email: string;\n  visibility: \"private\" | \"team\" | \"public\";\n  summaries: boolean;\n};\nconst defaults: Settings = {\n  name: \"Harbour studio\",\n  email: \"studio@example.com\",\n  visibility: \"team\",\n  summaries: true,\n};\nconst storageKey = \"mizu-settings-block-v1\";\nconst changeEvent = \"mizu-settings-block-change\";\nfunction subscribe(callback: () => void) {\n  const storage = (event: StorageEvent) => {\n    if (event.key === storageKey || event.key === null) callback();\n  };\n  window.addEventListener(\"storage\", storage);\n  window.addEventListener(changeEvent, callback);\n  return () => {\n    window.removeEventListener(\"storage\", storage);\n    window.removeEventListener(changeEvent, callback);\n  };\n}\nfunction snapshot() {\n  try {\n    return localStorage.getItem(storageKey);\n  } catch {\n    return \"unavailable\";\n  }\n}\nfunction parseSettings(raw: string | null): Settings | null {\n  if (!raw) return null;\n  const parsed: unknown = JSON.parse(raw);\n  if (!parsed || typeof parsed !== \"object\")\n    throw new Error(\"Invalid settings\");\n  const value = parsed as Partial<Settings>;\n  if (\n    typeof value.name !== \"string\" ||\n    !value.name.trim() ||\n    value.name.length > 60 ||\n    typeof value.email !== \"string\" ||\n    value.email.length > 254 ||\n    !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.email) ||\n    ![\"private\", \"team\", \"public\"].includes(value.visibility ?? \"\") ||\n    typeof value.summaries !== \"boolean\"\n  )\n    throw new Error(\"Invalid settings\");\n  return {\n    name: value.name,\n    email: value.email,\n    visibility: value.visibility as Settings[\"visibility\"],\n    summaries: value.summaries,\n  };\n}\n\nexport function SettingsBlock() {\n  const stored = useSyncExternalStore(subscribe, snapshot, () => null);\n  const [draft, setDraft] = useState<Settings | null>(null);\n  const [error, setError] = useState(\"\");\n  const [notice, setNotice] = useState(\"\");\n  let saved: Settings | null = null;\n  let restoreError = \"\";\n  try {\n    saved = parseSettings(stored);\n  } catch {\n    restoreError =\n      \"Saved settings could not be read. Your draft is safe; save again to replace the local copy.\";\n  }\n  const current = draft ?? saved ?? defaults;\n  const dirty = JSON.stringify(current) !== JSON.stringify(saved ?? defaults);\n  function edit(next: Partial<Settings>) {\n    setDraft({ ...current, ...next });\n    setError(\"\");\n    setNotice(\"\");\n  }\n\n  return (\n    <section\n      className=\"mizu-block mizu-block-settings\"\n      aria-label=\"Workspace settings\"\n    >\n      <div className=\"mizu-block-settings-intro\">\n        <h3>{current.name || \"Your workspace\"}</h3>\n        <p>Give the workspace a name and decide how it meets the world.</p>\n        <Status\n          tone={\n            error ? \"error\" : dirty ? \"warning\" : saved ? \"success\" : \"neutral\"\n          }\n        >\n          {error\n            ? \"Save failed\"\n            : dirty\n              ? \"Unsaved changes\"\n              : saved\n                ? \"Saved on this device\"\n                : \"No local save yet\"}\n        </Status>\n        <p className=\"mizu-field-hint\">\n          This sample stores settings only in this browser. The contact address\n          receives no messages.\n        </p>\n      </div>\n      <form\n        className=\"mizu-block-settings-form\"\n        onSubmit={(event) => {\n          event.preventDefault();\n          const next = {\n            ...current,\n            name: current.name.trim(),\n            email: current.email.trim(),\n          };\n          if (!next.name) {\n            setError(\"Enter a workspace name before saving.\");\n            return;\n          }\n          try {\n            localStorage.setItem(storageKey, JSON.stringify(next));\n            window.dispatchEvent(new Event(changeEvent));\n            setDraft(null);\n            setError(\"\");\n            setNotice(\"Settings saved on this device.\");\n          } catch {\n            setError(\n              \"This browser could not save your settings. Keep this page open and try saving again.\",\n            );\n          }\n        }}\n        onReset={() => {\n          setDraft(null);\n          setError(\"\");\n          setNotice(\"Draft discarded. Your saved settings are unchanged.\");\n        }}\n      >\n        <TextField\n          label=\"Workspace name\"\n          name=\"workspaceName\"\n          value={current.name}\n          onChange={(event) => edit({ name: event.target.value })}\n          required\n          maxLength={60}\n          autoComplete=\"organization\"\n        />\n        <TextField\n          label=\"Contact email\"\n          name=\"contactEmail\"\n          value={current.email}\n          onChange={(event) => edit({ email: event.target.value })}\n          type=\"email\"\n          required\n          maxLength={254}\n          autoComplete=\"email\"\n        />\n        <SelectField\n          label=\"Workspace visibility\"\n          name=\"visibility\"\n          value={current.visibility}\n          onChange={(event) =>\n            edit({ visibility: event.target.value as Settings[\"visibility\"] })\n          }\n        >\n          <option value=\"private\">Private — only you</option>\n          <option value=\"team\">Team — invited people</option>\n          <option value=\"public\">Public — anyone with the link</option>\n        </SelectField>\n        <Switch\n          label=\"Weekly summary preference\"\n          name=\"summaries\"\n          checked={current.summaries}\n          onChange={(event) => edit({ summaries: event.target.checked })}\n        />\n        <div className=\"mizu-block-actions\">\n          <Button type=\"submit\" arrow={false}>\n            Save settings\n          </Button>\n          <Button type=\"reset\" variant=\"ghost\" arrow={false} disabled={!draft}>\n            Discard changes\n          </Button>\n          <button\n            type=\"button\"\n            className=\"mizu-block-text-action\"\n            onClick={() => {\n              setDraft({ ...defaults });\n              setError(\"\");\n              setNotice(\"Defaults restored to the draft. Save to keep them.\");\n            }}\n          >\n            Restore defaults\n          </button>\n        </div>\n        {(error || restoreError) && (\n          <p className=\"mizu-field-error\" role=\"alert\">\n            {error || restoreError}\n          </p>\n        )}\n        <p className=\"mizu-field-hint\" role=\"status\">\n          {notice}\n        </p>\n      </form>\n    </section>\n  );\n}\n"},{"path":"registry/blocks/blocks.css","type":"registry:file","target":"@ui/mizu/blocks/blocks.css","content":".mizu-block {\n  min-width: 0;\n  color: var(--mizu-paper);\n  background: var(--mizu-ink);\n}\n.mizu-block h3 {\n  margin: 0 0 12px;\n  font: 600 24px / 1.2 var(--mizu-font-display);\n  letter-spacing: -0.025em;\n}\n.mizu-block p {\n  max-width: 65ch;\n}\n.mizu-block-settings {\n  display: grid;\n  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);\n  gap: 48px;\n}\n.mizu-block-settings-intro > p:first-of-type {\n  margin: 0 0 24px;\n  color: var(--mizu-muted);\n  line-height: 1.6;\n}\n.mizu-block-settings-intro > .mizu-field-hint {\n  margin-top: 24px;\n}\n.mizu-block-settings-form {\n  display: grid;\n  gap: 24px;\n}\n.mizu-block-settings-form > p {\n  margin: 0;\n}\n.mizu-block-settings-form > p:empty {\n  position: absolute;\n}\n.mizu-block-actions,\n.mizu-block-toolbar {\n  display: flex;\n  align-items: end;\n  justify-content: space-between;\n  gap: 16px;\n  flex-wrap: wrap;\n}\n.mizu-block-actions {\n  align-items: stretch;\n  justify-content: flex-start;\n}\n.mizu-block-text-action,\n.mizu-block-download {\n  display: inline-flex;\n  align-items: center;\n  min-height: 44px;\n  padding: 8px 0;\n  border: 0;\n  background: transparent;\n  color: var(--mizu-accent);\n  font: 11px var(--mizu-font-mono);\n  text-decoration: underline;\n  text-underline-offset: 4px;\n  cursor: pointer;\n}\n.mizu-block-text-action:hover,\n.mizu-block-download:hover {\n  color: var(--mizu-paper);\n}\n.mizu-block-text-action:focus-visible,\n.mizu-block-download:focus-visible,\n.mizu-block-file-name:focus-visible,\n.mizu-block-file-detail pre:focus-visible {\n  outline: 2px solid var(--mizu-accent);\n  outline-offset: 4px;\n}\n.mizu-block-toolbar > .mizu-search {\n  flex: 1 1 240px;\n}\n.mizu-block-toolbar > .mizu-field {\n  flex: 0 1 200px;\n}\n.mizu-block-readout {\n  margin: 16px 0;\n  color: var(--mizu-muted);\n  font: 11px var(--mizu-font-mono);\n  font-variant-numeric: tabular-nums;\n}\n.mizu-block-file-workspace {\n  display: grid;\n  grid-template-columns: minmax(0, 1.5fr) minmax(240px, 1fr);\n  gap: 32px;\n  align-items: start;\n}\n.mizu-block-file-name {\n  min-height: 44px;\n  padding: 0;\n  border: 0;\n  background: none;\n  color: var(--mizu-paper);\n  font: inherit;\n  text-align: left;\n  text-decoration: underline;\n  text-decoration-color: transparent;\n  text-underline-offset: 4px;\n  overflow-wrap: anywhere;\n  cursor: pointer;\n  transition: text-decoration-color var(--mizu-duration-fast);\n}\n.mizu-block-file-name:hover {\n  text-decoration-color: var(--mizu-line-bright);\n}\n.mizu-block-file-name[aria-pressed=\"true\"] {\n  color: var(--mizu-accent);\n}\n.mizu-block-file-detail {\n  min-width: 0;\n  padding: 24px;\n  border: 1px solid var(--mizu-line);\n  background: var(--mizu-ink-2);\n}\n.mizu-block-file-detail h3 {\n  font-size: 20px;\n  overflow-wrap: anywhere;\n}\n.mizu-block-file-detail pre {\n  max-height: 240px;\n  margin: 20px 0 8px;\n  overflow: auto;\n  white-space: pre-wrap;\n  overflow-wrap: anywhere;\n  color: var(--mizu-paper);\n  font: 11px / 1.7 var(--mizu-font-mono);\n  tab-size: 2;\n}\n.mizu-block-analytics {\n  display: grid;\n  gap: 32px;\n}\n.mizu-block-analytics-controls {\n  display: grid;\n  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);\n  gap: 48px;\n  align-items: start;\n}\n.mizu-block-analytics-summary {\n  display: grid;\n  grid-template-columns: repeat(2, minmax(0, 1fr));\n  gap: 24px;\n  padding-top: 28px;\n}\n@media (max-width: 800px) {\n  .mizu-block-settings,\n  .mizu-block-file-workspace,\n  .mizu-block-analytics-controls {\n    grid-template-columns: minmax(0, 1fr);\n    gap: 28px;\n  }\n  .mizu-block-analytics-summary {\n    padding-top: 0;\n  }\n}\n@media (max-width: 420px) {\n  .mizu-block-analytics-summary {\n    grid-template-columns: minmax(0, 1fr);\n  }\n}\n"}]}