{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "account-settings",
  "title": "Account Settings",
  "description": "Composable account-management sections for a Nexus ID consumer: profile and contact presentation, linked sign-in methods, passkeys, active sessions and organisation memberships. They accept data and callbacks only; consumers keep authentication, confirmation and policy decisions.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/alert-dialog",
    "@nx-ui/avatar",
    "@nx-ui/badge",
    "@nx-ui/button",
    "@nx-ui/card",
    "@nx-ui/settings",
    "@nx-ui/skeleton",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/account-settings.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  CheckCircle,\n  Fingerprint01,\n  Link01,\n  LogOut01,\n  Mail01,\n  Monitor01,\n  Plus,\n  ShieldTick,\n  Trash01,\n  Users01,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { SettingsRow, SettingsSection } from \"@/components/blocks/settings\"\n\n/** Data snapshots for a consumer-owned Nexus Account surface. No auth client or\n * product policy is embedded here: pass values from nx-id and wire callbacks at\n * the application boundary. */\nexport type AccountPerson = {\n  name: string\n  email: string\n  image?: string | null\n  emailVerified?: boolean\n}\n\nexport type LinkedIdentity = {\n  id: string\n  provider: string\n  providerLabel?: string\n  email?: string | null\n  connectedAt?: string | Date | null\n  /** Do not offer disconnect for the only recovery method. */\n  canDisconnect?: boolean\n}\n\nexport type AccountPasskey = {\n  id: string\n  name: string\n  createdAt?: string | Date | null\n  lastUsedAt?: string | Date | null\n}\n\nexport type AccountSession = {\n  id: string\n  label: string\n  detail?: string\n  lastActiveAt?: string | Date | null\n  current?: boolean\n}\n\nexport type AccountMembership = {\n  id: string\n  name: string\n  slug?: string\n  role: string\n  image?: string | null\n  active?: boolean\n  /** Set false when the server has determined the user cannot leave. */\n  canLeave?: boolean\n}\n\nexport type AccountActionState =\n  | { tone: \"success\"; message: string }\n  | { tone: \"error\"; message: string }\n  | null\n\nfunction initials(name: string) {\n  return name\n    .split(\" \")\n    .map((part) => part[0])\n    .join(\"\")\n    .slice(0, 2)\n    .toUpperCase()\n}\n\nfunction date(value?: string | Date | null) {\n  if (!value) return \"Never\"\n  const parsed = typeof value === \"string\" ? new Date(value) : value\n  if (Number.isNaN(parsed.getTime())) return \"Unknown\"\n  return new Intl.DateTimeFormat(\"en-GB\", { dateStyle: \"medium\" }).format(parsed)\n}\n\n/** Inline success/error acknowledgement for async consumer callbacks. */\nfunction AccountActionNotice({\n  state,\n  className,\n}: {\n  state?: AccountActionState\n  className?: string\n}) {\n  if (!state) return null\n  return (\n    <p\n      role={state.tone === \"error\" ? \"alert\" : \"status\"}\n      className={cn(\n        \"rounded-lg border px-3 py-2 text-sm\",\n        state.tone === \"success\"\n          ? \"border-utility-green-200 bg-utility-green-50 text-utility-green-700\"\n          : \"border-utility-red-200 bg-utility-red-50 text-utility-red-700\",\n        className\n      )}\n    >\n      {state.message}\n    </p>\n  )\n}\n\n/**\n * The notice band inside a section card. Renders nothing at all when there is\n * no state — an empty padded wrapper would leave a hole above the first row.\n */\nfunction AccountNoticeBand({ state }: { state?: AccountActionState }) {\n  if (!state) return null\n  return (\n    <div className=\"border-b border-border px-6 py-4\">\n      <AccountActionNotice state={state} />\n    </div>\n  )\n}\n\n/** The leading mark on a list row — keeps every row's text on one baseline. */\nfunction AccountRowIcon({\n  icon: Icon,\n}: {\n  icon: React.ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>\n}) {\n  return (\n    <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg border border-border bg-background text-muted-foreground\">\n      <Icon className=\"size-4.5\" aria-hidden />\n    </span>\n  )\n}\n\n/**\n * One row in an account list: media + title (with an optional badge) and meta\n * on the left, actions pinned right. Every account section uses it, so the\n * rows line up across cards.\n */\nfunction AccountRow({\n  media,\n  title,\n  badge,\n  meta,\n  actions,\n}: {\n  media?: React.ReactNode\n  title: React.ReactNode\n  badge?: React.ReactNode\n  meta?: React.ReactNode\n  actions?: React.ReactNode\n}) {\n  return (\n    <div\n      data-slot=\"account-row\"\n      className=\"flex flex-wrap items-center gap-x-4 gap-y-3 px-6 py-4\"\n    >\n      <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n        {media}\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <span className=\"truncate text-sm font-semibold text-foreground\">\n              {title}\n            </span>\n            {badge}\n          </div>\n          {meta ? (\n            <span className=\"truncate text-sm text-muted-foreground\">\n              {meta}\n            </span>\n          ) : null}\n        </div>\n      </div>\n      {actions ? (\n        <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>\n      ) : null}\n    </div>\n  )\n}\n\nfunction AccountRowsSkeleton({ rows = 2 }: { rows?: number }) {\n  return (\n    <div className=\"flex flex-col gap-3 p-6\">\n      {Array.from({ length: rows }, (_, index) => (\n        <Skeleton key={index} className=\"h-14 w-full\" />\n      ))}\n    </div>\n  )\n}\n\n/** The destructive row action, styled the same in every account section. */\nfunction AccountRowAction({\n  icon: Icon,\n  label,\n  disabled,\n  onClick,\n}: {\n  icon: React.ComponentType<{ className?: string }>\n  label: string\n  disabled?: boolean\n  onClick: () => void\n}) {\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      disabled={disabled}\n      className=\"text-destructive hover:bg-destructive/10 hover:text-destructive\"\n      onClick={onClick}\n    >\n      <Icon />\n      {label}\n    </Button>\n  )\n}\n\n/** Empty/error/confirmation-ready state for account sections without imposing\n * product wording or side effects on consumers. */\nfunction AccountState({\n  icon: Icon = ShieldTick,\n  title,\n  description,\n  action,\n  className,\n}: {\n  icon?: React.ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>\n  title: string\n  description: string\n  action?: React.ReactNode\n  className?: string\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center gap-3 px-6 py-12 text-center\",\n        className\n      )}\n    >\n      <span className=\"flex size-10 items-center justify-center rounded-lg border border-border bg-background text-muted-foreground\">\n        <Icon className=\"size-5\" aria-hidden />\n      </span>\n      <div className=\"flex flex-col gap-1\">\n        <p className=\"text-sm font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-balance text-muted-foreground\">\n          {description}\n        </p>\n      </div>\n      {action ? <div className=\"mt-1\">{action}</div> : null}\n    </div>\n  )\n}\n\n/** A reusable controlled destructive-action confirmation. The consumer owns\n * whether it opens and what the async confirmation does. */\nfunction AccountConfirmation({\n  open,\n  onOpenChange,\n  title,\n  description,\n  confirmLabel = \"Confirm\",\n  pending = false,\n  onConfirm,\n}: {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  title: string\n  description: string\n  confirmLabel?: string\n  pending?: boolean\n  onConfirm: () => void\n}) {\n  return (\n    <AlertDialog open={open} onOpenChange={onOpenChange}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{description}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>\n          <AlertDialogAction\n            variant=\"destructive\"\n            disabled={pending}\n            onClick={(event) => {\n              event.preventDefault()\n              onConfirm()\n            }}\n          >\n            {pending ? \"Working…\" : confirmLabel}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n\n/** Top-of-account summary: identity plus plain counts supplied by the consumer. */\nfunction AccountOverview({\n  person,\n  securityLabel,\n  memberships,\n  className,\n}: {\n  person: AccountPerson\n  /** e.g. \"Two-factor authentication is on\" — calculate policy server-side. */\n  securityLabel: string\n  memberships?: number | null\n  className?: string\n}) {\n  return (\n    <Card data-slot=\"account-overview\" className={cn(\"gap-0 p-0\", className)}>\n      <CardContent className=\"flex flex-col gap-5 p-6 sm:flex-row sm:items-center sm:justify-between\">\n        <div className=\"flex min-w-0 items-center gap-4\">\n          <Avatar size=\"lg\">\n            {person.image ? <AvatarImage src={person.image} alt={person.name} /> : null}\n            <AvatarFallback>{initials(person.name)}</AvatarFallback>\n          </Avatar>\n          <div className=\"min-w-0\">\n            <h1 className=\"truncate text-lg font-semibold text-foreground\">{person.name}</h1>\n            <p className=\"truncate text-sm text-muted-foreground\">{person.email}</p>\n          </div>\n        </div>\n        <div className=\"flex flex-wrap gap-2 sm:justify-end\">\n          <Badge variant={person.emailVerified ? \"success\" : \"warning\"}>\n            <Mail01 />\n            {person.emailVerified ? \"Email verified\" : \"Email needs verification\"}\n          </Badge>\n          <Badge variant=\"gray\"><ShieldTick />{securityLabel}</Badge>\n          <Badge variant=\"gray\">\n            <Users01 />\n            {memberships == null\n              ? \"Loading organisations\"\n              : `${memberships} ${memberships === 1 ? \"organisation\" : \"organisations\"}`}\n          </Badge>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n\n/** Identity/profile settings. Consumers keep inputs, upload handling and save\n * mutations; this block simply provides the consistent account layout. */\nfunction AccountProfileSection({\n  person,\n  children,\n  onEditProfile,\n  className,\n}: {\n  person: AccountPerson\n  /** Optional consumer-controlled fields, upload control or contact rows. */\n  children?: React.ReactNode\n  onEditProfile?: () => void\n  className?: string\n}) {\n  return (\n    <SettingsSection\n      title=\"Profile\"\n      description=\"Your name and photo appear across Nexus products and organisation membership lists.\"\n      className={className}\n      actions={\n        onEditProfile ? (\n          <Button type=\"button\" size=\"sm\" onClick={onEditProfile}>\n            Edit profile\n          </Button>\n        ) : null\n      }\n    >\n      <SettingsRow label=\"Account holder\">\n        <div className=\"flex items-center gap-3\">\n          <Avatar>\n            {person.image ? (\n              <AvatarImage src={person.image} alt={person.name} />\n            ) : null}\n            <AvatarFallback>{initials(person.name)}</AvatarFallback>\n          </Avatar>\n          <span className=\"text-sm font-medium text-foreground\">\n            {person.name}\n          </span>\n        </div>\n      </SettingsRow>\n      <SettingsRow\n        label=\"Email address\"\n        description={person.emailVerified ? \"Verified\" : \"Verification required\"}\n      >\n        <span className=\"text-sm text-foreground\">{person.email}</span>\n      </SettingsRow>\n      {children}\n    </SettingsSection>\n  )\n}\n\nfunction LinkedAccountsSection({\n  accounts,\n  loading = false,\n  state,\n  onLink,\n  onDisconnect,\n  className,\n}: {\n  accounts: LinkedIdentity[]\n  loading?: boolean\n  state?: AccountActionState\n  onLink?: () => void\n  /** Called only after the consumer's own confirmation gate, if required. */\n  onDisconnect?: (account: LinkedIdentity) => void\n  className?: string\n}) {\n  const linkAction = onLink ? (\n    <Button type=\"button\" size=\"sm\" onClick={onLink}>\n      <Plus />\n      Link account\n    </Button>\n  ) : null\n\n  return (\n    <SettingsSection\n      title=\"Linked accounts\"\n      description=\"Sign-in methods connected to this Nexus Account.\"\n      className={className}\n      actions={linkAction}\n    >\n      <AccountNoticeBand state={state} />\n      {loading ? (\n        <AccountRowsSkeleton />\n      ) : accounts.length === 0 ? (\n        <AccountState\n          icon={Link01}\n          title=\"No linked accounts\"\n          description=\"Add Google, Microsoft or another approved identity provider to create another sign-in method.\"\n          action={linkAction}\n        />\n      ) : (\n        <div className=\"divide-y divide-border\">\n          {accounts.map((account) => (\n            <AccountRow\n              key={account.id}\n              media={<AccountRowIcon icon={Link01} />}\n              title={account.providerLabel ?? account.provider}\n              meta={account.email ?? `Linked ${date(account.connectedAt)}`}\n              actions={\n                onDisconnect ? (\n                  <AccountRowAction\n                    icon={Trash01}\n                    label=\"Disconnect\"\n                    disabled={account.canDisconnect === false}\n                    onClick={() => onDisconnect(account)}\n                  />\n                ) : null\n              }\n            />\n          ))}\n        </div>\n      )}\n    </SettingsSection>\n  )\n}\n\nfunction PasskeysSection({\n  passkeys,\n  loading = false,\n  state,\n  onCreate,\n  onDelete,\n  className,\n}: {\n  passkeys: AccountPasskey[]\n  loading?: boolean\n  state?: AccountActionState\n  onCreate?: () => void\n  onDelete?: (passkey: AccountPasskey) => void\n  className?: string\n}) {\n  const createAction = onCreate ? (\n    <Button type=\"button\" size=\"sm\" onClick={onCreate}>\n      <Plus />\n      Add passkey\n    </Button>\n  ) : null\n\n  return (\n    <SettingsSection\n      title=\"Passkeys\"\n      description=\"Use a device biometric or screen lock instead of a password.\"\n      className={className}\n      actions={createAction}\n    >\n      <AccountNoticeBand state={state} />\n      {loading ? (\n        <AccountRowsSkeleton rows={1} />\n      ) : passkeys.length === 0 ? (\n        <AccountState\n          icon={Fingerprint01}\n          title=\"No passkeys\"\n          description=\"Add a passkey from a device you use regularly for faster, phishing-resistant sign-in.\"\n          action={createAction}\n        />\n      ) : (\n        <div className=\"divide-y divide-border\">\n          {passkeys.map((passkey) => (\n            <AccountRow\n              key={passkey.id}\n              media={<AccountRowIcon icon={Fingerprint01} />}\n              title={passkey.name}\n              meta={`Added ${date(passkey.createdAt)} · Last used ${date(passkey.lastUsedAt)}`}\n              actions={\n                onDelete ? (\n                  <AccountRowAction\n                    icon={Trash01}\n                    label=\"Remove\"\n                    onClick={() => onDelete(passkey)}\n                  />\n                ) : null\n              }\n            />\n          ))}\n        </div>\n      )}\n    </SettingsSection>\n  )\n}\n\nfunction SessionsSection({\n  sessions,\n  loading = false,\n  state,\n  onRevoke,\n  onRevokeOtherSessions,\n  className,\n}: {\n  sessions: AccountSession[]\n  loading?: boolean\n  state?: AccountActionState\n  onRevoke?: (session: AccountSession) => void\n  onRevokeOtherSessions?: () => void\n  className?: string\n}) {\n  return (\n    <SettingsSection\n      title=\"Sessions\"\n      description=\"Devices currently signed in to your Nexus Account.\"\n      className={className}\n      actions={\n        onRevokeOtherSessions ? (\n          <Button\n            type=\"button\"\n            variant=\"secondary\"\n            size=\"sm\"\n            onClick={onRevokeOtherSessions}\n          >\n            <LogOut01 />\n            Sign out other sessions\n          </Button>\n        ) : null\n      }\n    >\n      <AccountNoticeBand state={state} />\n      {loading ? (\n        <AccountRowsSkeleton />\n      ) : sessions.length === 0 ? (\n        <AccountState\n          icon={Monitor01}\n          title=\"No active sessions\"\n          description=\"Your signed-in devices will appear here.\"\n        />\n      ) : (\n        <div className=\"divide-y divide-border\">\n          {sessions.map((session) => (\n            <AccountRow\n              key={session.id}\n              media={<AccountRowIcon icon={Monitor01} />}\n              title={session.label}\n              badge={\n                session.current ? (\n                  <Badge variant=\"success\">This device</Badge>\n                ) : null\n              }\n              meta={`${session.detail ? `${session.detail} · ` : \"\"}Last active ${date(session.lastActiveAt)}`}\n              actions={\n                onRevoke && !session.current ? (\n                  <AccountRowAction\n                    icon={LogOut01}\n                    label=\"Sign out\"\n                    onClick={() => onRevoke(session)}\n                  />\n                ) : null\n              }\n            />\n          ))}\n        </div>\n      )}\n    </SettingsSection>\n  )\n}\n\nfunction OrganisationMembershipsSection({\n  memberships,\n  loading = false,\n  state,\n  onSetActive,\n  onLeave,\n  className,\n}: {\n  memberships: AccountMembership[]\n  loading?: boolean\n  state?: AccountActionState\n  onSetActive?: (membership: AccountMembership) => void\n  onLeave?: (membership: AccountMembership) => void\n  className?: string\n}) {\n  return (\n    <SettingsSection\n      title=\"Organisations\"\n      description=\"Workspaces you can access with this Nexus Account.\"\n      className={className}\n    >\n      <AccountNoticeBand state={state} />\n      {loading ? (\n        <AccountRowsSkeleton rows={1} />\n      ) : memberships.length === 0 ? (\n        <AccountState\n          icon={Users01}\n          title=\"No organisations yet\"\n          description=\"When you accept an organisation invitation, the workspace appears here.\"\n        />\n      ) : (\n        <div className=\"divide-y divide-border\">\n          {memberships.map((membership) => {\n            const canSetActive = !!onSetActive && !membership.active\n            return (\n            <AccountRow\n              key={membership.id}\n              media={\n                <Avatar>\n                  {membership.image ? (\n                    <AvatarImage src={membership.image} alt={membership.name} />\n                  ) : null}\n                  <AvatarFallback>{initials(membership.name)}</AvatarFallback>\n                </Avatar>\n              }\n              title={membership.name}\n              badge={\n                membership.active ? (\n                  <Badge variant=\"success\">\n                    <CheckCircle />\n                    Active\n                  </Badge>\n                ) : null\n              }\n              meta={`${membership.role}${membership.slug ? ` · ${membership.slug}` : \"\"}`}\n              actions={\n                canSetActive || onLeave ? (\n                  <>\n                    {canSetActive ? (\n                      <Button\n                        type=\"button\"\n                        variant=\"secondary\"\n                        size=\"sm\"\n                        onClick={() => onSetActive?.(membership)}\n                      >\n                        Set active\n                      </Button>\n                    ) : null}\n                    {onLeave ? (\n                      <AccountRowAction\n                        icon={LogOut01}\n                        label=\"Leave\"\n                        disabled={membership.canLeave === false}\n                        onClick={() => onLeave(membership)}\n                      />\n                    ) : null}\n                  </>\n                ) : null\n              }\n            />\n            )\n          })}\n        </div>\n      )}\n    </SettingsSection>\n  )\n}\n\nexport {\n  AccountActionNotice,\n  AccountConfirmation,\n  AccountOverview,\n  AccountProfileSection,\n  AccountState,\n  LinkedAccountsSection,\n  OrganisationMembershipsSection,\n  PasskeysSection,\n  SessionsSection,\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}