{
  "schemaVersion": 1,
  "target": "react",
  "name": "animated-status",
  "version": "0.1.0",
  "category": "core",
  "title": "Animated Status",
  "description": "An inline status for async work whose icon and label transition between idle, working, success, and error.",
  "tags": [
    "status",
    "async",
    "loading",
    "feedback",
    "animation"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [],
  "docsPath": "/components/animated-status",
  "props": [
    {
      "name": "status",
      "type": "\"idle\" | \"working\" | \"success\" | \"error\"",
      "description": "Current state of the work."
    },
    {
      "name": "labels",
      "type": "Partial<Record<AnimatedStatusValue, ReactNode>>",
      "description": "Text for any status. Defaults: Ready, Working…, Done, Failed."
    },
    {
      "name": "...props",
      "type": "ComponentProps<\"span\">",
      "description": "Any native span attribute except role and children."
    }
  ],
  "accessibility": [
    "The wrapper has role=status, so screen readers politely announce the new label each time the status changes.",
    "aria-busy is set while the status is working.",
    "Every status has its own text label and its own icon shape; color is never the only signal.",
    "Icons are decorative (aria-hidden); the label carries the meaning.",
    "With reduced motion, the label swaps without fading or sliding and the working spinner stops rotating."
  ],
  "states": [
    "idle",
    "working",
    "success",
    "error"
  ],
  "files": [
    {
      "path": "ui/animated-status.tsx",
      "content": "import type { ComponentProps, ReactNode } from \"react\";\n\nexport type AnimatedStatusValue = \"idle\" | \"working\" | \"success\" | \"error\";\n\nconst defaultLabels: Record<AnimatedStatusValue, string> = {\n  idle: \"Ready\",\n  working: \"Working…\",\n  success: \"Done\",\n  error: \"Failed\",\n};\n\nconst tones: Record<AnimatedStatusValue, string> = {\n  idle: \"text-zinc-600 dark:text-zinc-400\",\n  working: \"text-sky-700 dark:text-sky-300\",\n  success: \"text-emerald-700 dark:text-emerald-300\",\n  error: \"text-red-700 dark:text-red-400\",\n};\n\nexport interface AnimatedStatusProps extends Omit<ComponentProps<\"span\">, \"children\" | \"role\"> {\n  status: AnimatedStatusValue;\n  /** Overrides the text shown for any status. Defaults: Ready, Working…, Done, Failed. */\n  labels?: Partial<Record<AnimatedStatusValue, ReactNode>>;\n}\n\n/**\n * An inline status for async work. The icon and label swap with a short CSS fade and slide\n * when `status` changes; screen readers announce the new label.\n */\nexport function AnimatedStatus({ status, labels, className, ...props }: AnimatedStatusProps) {\n  const label = labels?.[status] ?? defaultLabels[status];\n\n  return (\n    <span\n      role=\"status\"\n      aria-busy={status === \"working\" || undefined}\n      data-status={status}\n      className={[\"inline-flex items-center text-sm font-medium\", tones[status], className].filter(Boolean).join(\" \")}\n      {...props}\n    >\n      {/* Keyed by status so each change remounts the content and replays the @starting-style entry. */}\n      <span\n        key={status}\n        className={[\n          \"inline-flex items-center gap-1.5 whitespace-nowrap\",\n          \"transition-[opacity,translate] duration-200 ease-out starting:translate-y-1 starting:opacity-0\",\n          \"motion-reduce:transition-none\",\n        ].join(\" \")}\n      >\n        <StatusIcon status={status} />\n        <span>{label}</span>\n      </span>\n    </span>\n  );\n}\n\nfunction StatusIcon({ status }: { status: AnimatedStatusValue }) {\n  const common = { \"aria-hidden\": true, viewBox: \"0 0 16 16\", fill: \"none\", className: \"size-4 shrink-0\" } as const;\n\n  switch (status) {\n    case \"idle\":\n      return (\n        <svg {...common}>\n          <circle cx=\"8\" cy=\"8\" r=\"5.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeDasharray=\"2 2.12\" />\n        </svg>\n      );\n    case \"working\":\n      return (\n        <svg {...common} className=\"size-4 shrink-0 motion-safe:animate-spin\">\n          <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n          <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n        </svg>\n      );\n    case \"success\":\n      return (\n        <svg {...common}>\n          <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n          <path d=\"m5.5 8.25 1.75 1.75 3.25-3.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n        </svg>\n      );\n    case \"error\":\n      return (\n        <svg {...common}>\n          <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n          <path d=\"M8 4.75v3.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n          <circle cx=\"8\" cy=\"11\" r=\"0.875\" fill=\"currentColor\" />\n        </svg>\n      );\n  }\n}\n"
    }
  ],
  "example": {
    "content": "import { AnimatedStatus } from \"@/components/seamless/ui/animated-status\";\n\nexport default function AnimatedStatusExample() {\n  return (\n    <div className=\"grid gap-2\">\n      <AnimatedStatus status=\"idle\" />\n      <AnimatedStatus status=\"working\" labels={{ working: \"Saving…\" }} />\n      <AnimatedStatus status=\"success\" labels={{ success: \"Saved\" }} />\n      <AnimatedStatus status=\"error\" labels={{ error: \"Couldn’t save\" }} />\n    </div>\n  );\n}\n"
  }
}
