{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-spinner",
  "title": "Loading Spinner",
  "description": "Loading Spinner from the Get IT Done design system.",
  "dependencies": [
    "class-variance-authority@^0.7.0",
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/loading-spinner.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Loader } from 'lucide-react';\nimport { forwardRef, HTMLAttributes } from 'react';\nimport { cn } from '@/lib/utils';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n/**\n * FIGMA SPEC NOTE (LOADSPIN-FIGMA-01): the Component Library file \"🚧 GetITDone-Basic\"\n * (bzBnRLfRJNknGg9UYJIHb0) has no dedicated \"Loading Spinner\" / \"Progress indicator\" component\n * set — confirmed by walking every top-level page (👋🏻 Welcome, ↳ Avatars 18:1350, ↳ Button\n * 1:1183, ↳ Select 18:0, ↳ Tooltips 1052:485, ↳ CardMetrics 185:279, ↳ Logos 1083:118533, Icons\n * 3463:407484, Misc icons 1025:31781, Utilities 1054:557). The only loading-adjacent assets are\n * three generic icon glyphs (\"loading-01/02/03\", nodes 3463:405201/405204/405207) living in the\n * shared icon set — plain iconography, not a bound component with anatomy/variants/states. Per\n * figma-design-sync, a spec must not be fabricated where none exists, so this file does not\n * invent a Figma-driven redesign. Instead it aligns the EXISTING ring-spinner visual to design\n * tokens and to Material's own canonical \"circular progress indicator\" anatomy (required reading\n * per material-design-3-expressive): a low-emphasis static track + a high-emphasis `primary`\n * active indicator — plus this repo's established cva/size/tone/a11y patterns (see Avatar,\n * Skeleton).\n *\n * TOKEN ALIGNMENT (closes the raw-palette gap): the previous implementation hardcoded\n * `border-slate-200 border-t-slate-600` — a raw Tailwind palette pair with no dark-mode value at\n * all (same root-cause class as Skeleton's pre-fix `bg-slate-200`, see SKEL-TOK-01/TABLE-MAT-02\n * in skeleton.tsx). `border-slate-200` rendered a fixed light-gray track and `border-t-slate-600`\n * a fixed dark-gray active arc in EVERY theme and BOTH modes, so in dark mode (and in several of\n * the 18 color themes) the spinner's only real signal — the moving arc — read as a barely-there\n * dark-on-dark smudge. Replaced with:\n * - track: `border-border-default` (`--semantic-border-default`) — the same low-contrast,\n *   intentionally-subtle divider token Chip/Card use for non-state-bearing edges (already\n *   documented in chip.tsx as ~1.2:1 in light mode \"by design\" for a de-emphasized boundary);\n *   adaptive in both modes (light rgb(226 232 240) / dark rgb(51 65 85)).\n * - active indicator (`border-t-*`, the segment that visibly rotates and is the ONLY perceivable\n *   \"it's loading\" signal — must clear WCAG 1.4.11's 3:1 non-text minimum): `tone=\"default\"`\n *   uses `general-brand-default` (rgb(37 99 235), unchanged across modes) — verified ≥3:1 against\n *   a typical page background in both light (~5.17:1) and dark (~3.91:1, against\n *   `semantic-background-default` rgb(2 6 23)). `tone=\"inverse\"` (for a spinner drawn on a solid\n *   brand-color surface, e.g. inside a filled primary Button while loading) uses\n *   `general-brand-on`, verified ≥3:1 against `general-brand-default` in both modes (light\n *   ~4.94:1, dark ~3.46:1). `tone=\"destructive\"` (a retry-after-error loading state) uses\n *   `general-error-default`, verified ≥3:1 against page background in both modes (light ~4.83:1,\n *   dark ~5.36:1). All three are the same adaptive, dark-mode-safe `general-*` role tokens\n *   already vetted elsewhere in this file set (Avatar, Skeleton, Alert) — no raw hex/palette\n *   value is introduced.\n *\n * STATE-SET NOTE: LoadingSpinner is a non-interactive, purely visual/status indicator — like\n * Skeleton, it is never a focusable or clickable control, so hover/focus-visible/active/selected\n * have no real interaction target to attach to and are intentionally NOT added (mirrors\n * Skeleton's identical reasoning in skeleton.tsx). Adding those data-/pseudo-classes without a\n * genuine interaction would be dead, feature-specific branching hidden in a shared primitive.\n * \"invalid\" is a form-validation concept with no analogue on a decorative progress indicator.\n * \"disabled\" is likewise incoherent for an indeterminate spinner (dimming something that keeps\n * spinning sends a contradictory signal); the one real \"off\" state this component supports is\n * `prefers-reduced-motion`, covered by `motion-reduce:animate-none` below — the same fallback\n * Skeleton/Tooltip/Select/DropdownMenu already carry for continuously, automatically animating\n * primitives, so reduced-motion users are not shown perpetual motion with no way to opt out. The\n * track + active-indicator arc stay fully visible (communicating \"still loading\") with only the\n * rotation removed.\n */\nconst loadingSpinnerVariants = cva(\n  'inline-block shrink-0 animate-spin motion-reduce:animate-none rounded-full border-solid border-border-default',\n  {\n    variants: {\n      size: {\n        sm: 'h-4 w-4 border-2',\n        default: 'h-12 w-12 border-4',\n        lg: 'h-16 w-16 border-4',\n        xl: 'h-24 w-24 border-8',\n      },\n      preset: { default: '', zero: '' },\n      tone: {\n        default: 'border-t-general-brand-default',\n        inverse: 'border-t-general-brand-on',\n        destructive: 'border-t-general-error-default',\n      },\n    },\n    // Product preset (DesignSystemProvider `preset`): `zero` renders the small ring at 14px.\n    compoundVariants: [{ preset: 'zero', size: 'sm', className: 'h-icon-xs w-icon-xs' }],\n    defaultVariants: {\n      size: 'default',\n      tone: 'default',\n      preset: 'default',\n    },\n  },\n);\n\nconst glyphSize = { sm: 'h-4 w-4', default: 'h-12 w-12', lg: 'h-16 w-16', xl: 'h-24 w-24' } as const;\nconst glyphTone = {\n  default: 'text-general-brand-default',\n  inverse: 'text-general-brand-on',\n  destructive: 'text-general-error-default',\n} as const;\n\n// Eight dots on a circle, the leading ones fully opaque (Figma Loading indicator Style=Dot circle).\nconst DotCircle = ({ className }: { className?: string }) => (\n  <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\" className={className} fill=\"currentColor\">\n    {[0, 1, 2, 3, 4, 5, 6, 7].map((index) => (\n      <circle\n        key={index}\n        cx={12 + 9 * Math.cos((index * Math.PI) / 4)}\n        cy={12 + 9 * Math.sin((index * Math.PI) / 4)}\n        r={2}\n        opacity={0.2 + index * 0.1}\n      />\n    ))}\n  </svg>\n);\n\ninterface LoadingSpinnerProps extends HTMLAttributes<HTMLDivElement>, Omit<VariantProps<typeof loadingSpinnerVariants>, 'preset'> {\n  className?: string;\n  /**\n   * Accessible name announced to assistive tech for as long as the spinner is on screen.\n   * Defaults to \"Loading\". Ignored if an explicit `aria-label` is also passed.\n   */\n  label?: string;\n  // `ring` is the established spinner. `line` (radial ticks) and `dots` (dot circle) are the other Figma styles.\n  variant?: 'ring' | 'line' | 'dots';\n  // Visible caption under the indicator (Figma Supporting text).\n  supportingText?: string;\n  // Where the supporting text sits: under the indicator (default) or inline after it on one line.\n  layout?: 'stacked' | 'inline';\n}\n\nconst LoadingSpinner = forwardRef<HTMLDivElement, LoadingSpinnerProps>((props, ref) => {\n  const {\n    className,\n    size,\n    tone,\n    label: labelProp,\n    role = 'status',\n    'aria-live': ariaLive = 'polite',\n    'aria-label': ariaLabel,\n    variant = 'ring',\n    supportingText,\n    layout = 'stacked',\n    ...rest\n  } = props;\n  const l = useLabels();\n  const zero = usePreset() === 'zero';\n  const label = labelProp ?? l.loading;\n  if (variant !== 'ring' || supportingText) {\n    const glyphClass = cn(glyphSize[size ?? 'default'], glyphTone[tone ?? 'default'], 'shrink-0 animate-spin motion-reduce:animate-none');\n    return (\n      <div\n        ref={ref}\n        role={role}\n        aria-live={ariaLive}\n        aria-label={ariaLabel ?? label}\n        data-variant={variant}\n        data-layout={layout}\n        className={cn('inline-flex items-center', layout === 'inline' ? 'flex-row gap-2' : 'flex-col gap-3', className)}\n        {...rest}\n      >\n        {variant === 'dots' ? (\n          <DotCircle className={glyphClass} />\n        ) : variant === 'line' ? (\n          <Loader aria-hidden=\"true\" className={glyphClass} />\n        ) : (\n          <span aria-hidden=\"true\" className={cn(loadingSpinnerVariants({ size, tone, preset: zero ? 'zero' : 'default' }))} />\n        )}\n        {supportingText ? <span className={cn('typography-ui text-text-subtle', layout === 'inline' && 'whitespace-nowrap')}>{supportingText}</span> : null}\n      </div>\n    );\n  }\n  return (\n    <div\n      ref={ref}\n      role={role}\n      aria-live={ariaLive}\n      aria-label={ariaLabel ?? label}\n      className={cn(loadingSpinnerVariants({ size, tone, preset: zero ? 'zero' : 'default' }), className)}\n      {...rest}\n    />\n  );\n});\n\nLoadingSpinner.displayName = 'LoadingSpinner';\n\nexport { LoadingSpinner, loadingSpinnerVariants, type LoadingSpinnerProps };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}