{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "save-status",
  "title": "Save Status",
  "description": "The small inline \"Saving… / Saved 2 minutes ago / Couldn't save · Retry\" indicator that sits beside an autosaving surface. Use it wherever edits persist in the background instead of behind a Save button: a document, note, or rich-text editor, a settings or profile page that saves on blur, a draft post or email composer, a form with debounced autosave, a spreadsheet-style inline-edit table, or a builder/canvas. Common asks it answers: \"autosave indicator\", \"saving spinner next to the title\", \"all changes saved\", \"draft saved status\", \"last saved timestamp\", \"Google-Docs-style save state\", \"how to show saving/saved/error\". shadcn/ui ships nothing for this — you'd hand-roll the state wording, the spinner, and the announcement each time. You pass one `status` prop (idle | saving | saved | error): idle renders nothing visible, so you can render it unconditionally and just mirror your mutation state (react-query isPending/isError, a useActionState, or your own flag). Pass `savedAt` and the \"Saved\" text is followed by a live relative timestamp that keeps itself fresh — it composes the time-ago component rather than freezing a string that goes stale while the tab sits open. Pass `onRetry` and the error state grows a Retry button. Accessibility is the fiddly part it gets right: the wording lives in an always-mounted role=status region so the very first transition is actually announced, while the ticking timestamp and the Retry label sit outside it — inside, the timer would make the page announce \"Saved 3 minutes ago\" every minute unprompted. It stays polite rather than assertive, because aria-live is honoured at registration time and a failed autosave should not cut across someone mid-sentence. All labels are overridable for i18n. Styled with shadcn tokens (muted-foreground, destructive, ring) so it follows light/dark themes. Distinct from toast, which pops a transient message after an action, and from spinner or loading-button, which cover a single in-flight request: this one is the persistent status of a background save.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://pulld.pages.dev/r/time-ago.json"
  ],
  "files": [
    {
      "path": "registry/ui/save-status.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, CircleAlert, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { TimeAgo } from \"@/registry/ui/time-ago\"\n\ninterface SaveStatusProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Where the background save currently stands. `idle` renders nothing visible. */\n  status: \"idle\" | \"saving\" | \"saved\" | \"error\"\n  /** When the last successful save landed. Shown as a live \"2 minutes ago\" next to \"Saved\". */\n  savedAt?: Date | string | number\n  /** Shows a Retry button in the error state. Omit it and no button is rendered. */\n  onRetry?: () => void\n  /** Text for the in-flight state. */\n  savingLabel?: string\n  /** Text once the save has landed. */\n  savedLabel?: string\n  /** Text when the save failed. */\n  errorLabel?: string\n  /** Label of the retry button. */\n  retryLabel?: string\n}\n\n/**\n * The small inline \"Saving… / Saved 2 minutes ago / Couldn't save\" indicator\n * that sits beside an autosaving editor, form, or settings panel.\n *\n * The status text lives in its own always-mounted `role=\"status\"` region, so\n * the first transition is announced (a live region inserted together with its\n * text is not reliably read out) and only the state wording is announced.\n *\n * The timestamp deliberately sits *outside* that region: it re-renders on a\n * timer, and inside a live region that would spontaneously announce \"Saved 3\n * minutes ago\" every minute with nothing having happened. Same for the Retry\n * button, whose label would otherwise be read on every status change.\n *\n * The region stays `polite` even for failures. `aria-live` is read when the\n * region is registered, so flipping it to `assertive` on error is unreliable —\n * and a failed autosave should not interrupt someone mid-sentence; it stays on\n * screen until it is resolved.\n */\nexport function SaveStatus({\n  status,\n  savedAt,\n  onRetry,\n  savingLabel = \"Saving…\",\n  savedLabel = \"Saved\",\n  errorLabel = \"Couldn't save\",\n  retryLabel = \"Retry\",\n  className,\n  ...props\n}: SaveStatusProps) {\n  const label =\n    status === \"saving\"\n      ? savingLabel\n      : status === \"saved\"\n        ? savedLabel\n        : status === \"error\"\n          ? errorLabel\n          : \"\"\n\n  return (\n    <div\n      className={cn(\n        \"inline-flex items-center gap-1.5 text-sm\",\n        status === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n        className\n      )}\n      {...props}\n    >\n      <span role=\"status\" className=\"inline-flex items-center gap-1.5\">\n        {status === \"saving\" ? (\n          <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden=\"true\" />\n        ) : null}\n        {status === \"saved\" ? (\n          <Check className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n        ) : null}\n        {status === \"error\" ? (\n          <CircleAlert className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n        ) : null}\n        {label}\n      </span>\n      {status === \"saved\" && savedAt !== undefined ? (\n        <TimeAgo date={savedAt} className=\"text-inherit\" />\n      ) : null}\n      {status === \"error\" && onRetry ? (\n        <button\n          type=\"button\"\n          onClick={onRetry}\n          className=\"rounded-sm font-medium underline underline-offset-2 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        >\n          {retryLabel}\n        </button>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
