{
  "name": "loading-indicator",
  "layer": "application",
  "type": "component",
  "title": "Loading indicator components",
  "description": "Free and open-source React loading indicator components built for modern applications and websites. These loading indicators are built using React Aria and styled with Tailwind CSS.",
  "files": [
    {
      "path": "components/application/loading-indicator/loading-indicator.tsx",
      "target": "components/application/loading-indicator/loading-indicator.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport { cx, sortCx } from \"@/utils/cx\";\n\nexport const styles = sortCx({\n    sizes: {\n        xs: {\n            root: \"gap-2\",\n            label: \"text-sm font-medium\",\n            spinner: \"size-4\",\n        },\n        sm: {\n            root: \"gap-4\",\n            label: \"text-sm font-medium\",\n            spinner: \"size-8\",\n        },\n        md: {\n            root: \"gap-4\",\n            label: \"text-sm font-medium\",\n            spinner: \"size-12\",\n        },\n        lg: {\n            root: \"gap-4\",\n            label: \"text-lg font-medium\",\n            spinner: \"size-14\",\n        },\n        xl: {\n            root: \"gap-5\",\n            label: \"text-lg font-medium\",\n            spinner: \"size-16\",\n        },\n    },\n});\n\nexport interface LoadingIndicatorProps {\n    /**\n     * The visual style of the loading indicator.\n     * @default 'line-simple'\n     */\n    type?: \"line-simple\" | \"line-spinner\" | \"dot-circle\";\n    /**\n     * The size of the loading indicator. `xs` is a 16px spinner meant to sit inline with text\n     * (see `inline`) rather than stand alone as a page/section loading state.\n     *\n     * To show a spinner inside a `Button`, use the button's own `isLoading` prop instead of\n     * this component — it replaces the button's content with a correctly sized, correctly\n     * coloured spinner and manages `aria-disabled` for you.\n     *\n     * @default 'sm'\n     */\n    size?: keyof typeof styles.sizes;\n    /**\n     * Optional text label. Sits below the indicator by default; pass `inline` to place it\n     * beside the indicator instead.\n     */\n    label?: string;\n    /**\n     * Lays the spinner and label out side by side (row) instead of stacked (column). Pairs\n     * naturally with `size=\"xs\"` for a loading state inline with surrounding text.\n     * @default false\n     */\n    inline?: boolean;\n}\n\nexport const LoadingIndicator = ({ type = \"line-simple\", size = \"sm\", label, inline = false }: LoadingIndicatorProps) => {\n    const renderSpinner = () => {\n        if (type === \"line-spinner\") {\n            return (\n                <svg aria-hidden=\"true\" className={cx(\"animate-spin\", styles.sizes[size].spinner)} viewBox=\"0 0 32 32\" fill=\"none\">\n                    <circle\n                        className=\"stroke-fg-brand-primary\"\n                        cx=\"16\"\n                        cy=\"16\"\n                        r=\"14\"\n                        fill=\"none\"\n                        strokeWidth=\"4\"\n                        strokeDashoffset=\"40\"\n                        strokeDasharray=\"100\"\n                        strokeLinecap=\"round\"\n                    />\n                </svg>\n            );\n        }\n\n        if (type === \"dot-circle\") {\n            return (\n                <svg aria-hidden=\"true\" className={cx(\"text-fg-brand-primary animate-spin\", styles.sizes[size].spinner)} viewBox=\"0 0 36 36\" fill=\"none\">\n                    <path\n                        d=\"M34 18C34 15.8989 33.5861 13.8183 32.7821 11.8771C31.978 9.93586 30.7994 8.17203 29.3137 6.68629C27.828 5.20055 26.0641 4.022 24.1229 3.21793C22.1817 2.41385 20.1011 2 18 2C15.8988 2 13.8183 2.41385 11.8771 3.21793C9.93585 4.022 8.17203 5.20055 6.68629 6.68629C5.20055 8.17203 4.022 9.93586 3.21793 11.8771C2.41385 13.8183 2 15.8989 2 18\"\n                        stroke=\"url(#paint0)\"\n                        strokeWidth=\"4\"\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                        strokeDasharray=\"0.1 8\"\n                    />\n                    <path\n                        d=\"M3.21793 24.1229C4.022 26.0641 5.20055 27.828 6.68629 29.3137C8.17203 30.7994 9.93585 31.978 11.8771 32.7821C13.8183 33.5861 15.8988 34 18 34C20.1011 34 22.1817 33.5861 24.1229 32.7821C26.0641 31.978 27.828 30.7994 29.3137 29.3137C30.7994 27.828 31.978 26.0641 32.7821 24.1229\"\n                        stroke=\"url(#paint1)\"\n                        strokeWidth=\"4\"\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                        strokeDasharray=\"0.1 8\"\n                    />\n                    <defs>\n                        <linearGradient id=\"paint0\" x1=\"34\" y1=\"18\" x2=\"2\" y2=\"18\" gradientUnits=\"userSpaceOnUse\">\n                            <stop stopColor=\"currentColor\" />\n                            <stop offset=\"1\" stopColor=\"currentColor\" stopOpacity=\"0.5\" />\n                        </linearGradient>\n                        <linearGradient id=\"paint1\" x1=\"33\" y1=\"23.5\" x2=\"3\" y2=\"24\" gradientUnits=\"userSpaceOnUse\">\n                            <stop stopOpacity=\"0\" stopColor=\"currentColor\" />\n                            <stop offset=\"1\" stopColor=\"currentColor\" stopOpacity=\"0.48\" />\n                        </linearGradient>\n                    </defs>\n                </svg>\n            );\n        }\n\n        // Default case: type === \"line-simple\"\n        return (\n            <svg aria-hidden=\"true\" className={cx(\"animate-spin\", styles.sizes[size].spinner)} viewBox=\"0 0 32 32\" fill=\"none\">\n                <circle className=\"text-bg-tertiary\" cx=\"16\" cy=\"16\" r=\"14\" stroke=\"currentColor\" strokeWidth=\"4\" />\n                <circle\n                    className=\"stroke-fg-brand-primary\"\n                    cx=\"16\"\n                    cy=\"16\"\n                    r=\"14\"\n                    fill=\"none\"\n                    strokeWidth=\"4\"\n                    strokeDashoffset=\"75\"\n                    strokeDasharray=\"100\"\n                    strokeLinecap=\"round\"\n                />\n            </svg>\n        );\n    };\n\n    return (\n        <div\n            role=\"status\"\n            aria-live=\"polite\"\n            aria-label={label ?? \"Loading\"}\n            className={cx(\"flex items-center justify-center\", inline ? \"flex-row\" : \"flex-col\", styles.sizes[size].root)}\n        >\n            {renderSpinner()}\n            {label && <span className={cx(\"text-secondary\", styles.sizes[size].label)}>{label}</span>}\n        </div>\n    );\n};\n",
      "dependencies": []
    }
  ],
  "registryDependencies": [
    "cx"
  ],
  "optionalRegistryDependencies": [],
  "dependencies": [],
  "cssVars": [],
  "examples": [
    "dot-circle",
    "dot-circle-with-label",
    "inline-xs",
    "line-simple",
    "line-simple-with-label",
    "line-spinner",
    "line-spinner-with-label",
    "loading-indicator-example",
    "sizes"
  ],
  "docs": "/components/loading-indicators",
  "intent": "Communicate that content is being fetched or a background action is in progress, with no determinate percentage.",
  "avoid_when": [
    "real progress can be reported (use progress-indicators, which is more informative)"
  ],
  "composes_with": [
    "empty-state",
    "table"
  ],
  "a11y_contract": [
    "the root carries `role=\"status\" aria-live=\"polite\"` with a default aria-label of \"Loading\" (override via the `label` prop) so assistive tech announces the state change without stealing focus; the spinner graphic itself is aria-hidden"
  ],
  "token_contract": [
    "text-bg-tertiary",
    "text-fg-brand-primary",
    "text-secondary"
  ],
  "changelog": [
    {
      "version": "0.2.0",
      "changes": [
        "Fixes and additive props across metrics, modals, notifications, pagination, table, file-upload, loading-indicator, progress-steps, alerts, app-navigation, and `CloseButton`, addressing item 2.1, 2.4, 2.9, 2.12, 2.18, 2.19 and part of 2.5 from `docs/spec/feedback/2026-09-11-agent-feedback-map.md`."
      ]
    }
  ],
  "platforms": [
    "react",
    "next"
  ]
}
