{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loaders",
  "title": "Loaders",
  "description": "Five tiny CSS loading indicators — dots, pulse, orbit, bars and spinner.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/loaders.tsx",
      "content": "import { cn } from \"@/lib/utils\";\n\ntype LoaderSize = \"sm\" | \"md\" | \"lg\";\n\ninterface LoaderProps extends React.HTMLAttributes<HTMLSpanElement> {\n  /** Overall size: sm = 16px, md = 24px, lg = 32px */\n  size?: LoaderSize;\n  /** Text read by screen readers (visually hidden) */\n  label?: string;\n}\n\nconst box: Record<LoaderSize, string> = {\n  sm: \"size-4\",\n  md: \"size-6\",\n  lg: \"size-8\",\n};\n\n// Under reduced motion every loader swaps its movement for a slow fade.\nconst calm = \"motion-reduce:animate-pulse motion-reduce:[animation-duration:2s]!\";\n\n// Shared shell: a status region with a visually hidden label.\nfunction Shell({\n  label,\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement> & { label: string }) {\n  return (\n    <span\n      role=\"status\"\n      {...props}\n      className={cn(\"inline-flex shrink-0 items-center justify-center\", className)}\n    >\n      <span className=\"sr-only\">{label}</span>\n      {children}\n    </span>\n  );\n}\n\n/**\n * Three dots that hop in turn. Every loader in this file draws with\n * `currentColor`, so colour it with a text utility (`text-brand`).\n */\nexport function DotsLoader({\n  size = \"md\",\n  label = \"Loading\",\n  className,\n  ...props\n}: LoaderProps) {\n  const dot = { sm: \"text-[6px]\", md: \"text-[8px]\", lg: \"text-[10px]\" }[size];\n  return (\n    <Shell\n      label={label}\n      className={cn(\"items-end gap-[0.4em]\", dot, className)}\n      {...props}\n    >\n      {[0, 1, 2].map((i) => (\n        // The tall wrapper hops; its height sets how far the dot travels.\n        <span\n          key={i}\n          aria-hidden\n          className={cn(\"flex h-[2.4em] items-end motion-safe:animate-bounce\", calm)}\n          style={{\n            animationDuration: \"0.9s\",\n            animationDelay: `${i * 0.15 - 0.9}s`,\n          }}\n        >\n          <span className=\"size-[1em] rounded-full bg-current\" />\n        </span>\n      ))}\n    </Shell>\n  );\n}\n\n/** A solid dot with a ring that ripples outwards. */\nexport function PulseLoader({\n  size = \"md\",\n  label = \"Loading\",\n  className,\n  ...props\n}: LoaderProps) {\n  return (\n    <Shell label={label} className={cn(\"relative\", box[size], className)} {...props}>\n      <span\n        aria-hidden\n        className=\"absolute inset-1/4 rounded-full bg-current opacity-50 motion-safe:animate-ping motion-reduce:hidden\"\n      />\n      <span aria-hidden className={cn(\"size-1/2 rounded-full bg-current\", calm)} />\n    </Shell>\n  );\n}\n\n/** Two dots circling a faint track in opposite directions. */\nexport function OrbitLoader({\n  size = \"md\",\n  label = \"Loading\",\n  className,\n  ...props\n}: LoaderProps) {\n  const dot = \"absolute top-0 left-1/2 size-1/4 -translate-x-1/2 rounded-full bg-current\";\n  return (\n    <Shell label={label} className={cn(\"relative\", box[size], className)} {...props}>\n      <span\n        aria-hidden\n        className=\"absolute inset-[12%] rounded-full border-2 border-current opacity-20\"\n      />\n      <span\n        aria-hidden\n        className={cn(\"absolute inset-0 motion-safe:animate-spin\", calm)}\n        style={{ animationDuration: \"1.2s\" }}\n      >\n        <span className={dot} />\n      </span>\n      <span\n        aria-hidden\n        className={cn(\"absolute inset-0 rotate-180 motion-safe:animate-spin\", calm)}\n        style={{ animationDuration: \"1.8s\", animationDirection: \"reverse\" }}\n      >\n        <span className={cn(dot, \"opacity-60\")} />\n      </span>\n    </Shell>\n  );\n}\n\n/** Four bars rising and falling from a shared baseline, like a level meter. */\nexport function BarsLoader({\n  size = \"md\",\n  label = \"Loading\",\n  className,\n  ...props\n}: LoaderProps) {\n  return (\n    <Shell\n      label={label}\n      className={cn(\"items-stretch gap-[12%] overflow-hidden\", box[size], className)}\n      {...props}\n    >\n      {[-0.2, -0.6, -0.4, 0].map((delay) => (\n        // The shell clips each bar's foot; the bounce changes how much shows.\n        <span\n          key={delay}\n          aria-hidden\n          className={cn(\n            \"relative top-1/2 h-full flex-1 rounded-full bg-current motion-safe:animate-bounce motion-reduce:top-1/4\",\n            calm\n          )}\n          style={{ animationDuration: \"0.8s\", animationDelay: `${delay}s` }}\n        />\n      ))}\n    </Shell>\n  );\n}\n\n/** A rounded arc turning over a faint track. */\nexport function SpinnerLoader({\n  size = \"md\",\n  label = \"Loading\",\n  className,\n  ...props\n}: LoaderProps) {\n  return (\n    <Shell label={label} className={cn(box[size], className)} {...props}>\n      <svg\n        aria-hidden\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"3\"\n        className={cn(\"size-full motion-safe:animate-spin\", calm)}\n        style={{ animationDuration: \"0.8s\" }}\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"9.5\" opacity=\"0.2\" />\n        <path d=\"M21.5 12A9.5 9.5 0 0 0 12 2.5\" strokeLinecap=\"round\" />\n      </svg>\n    </Shell>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/loaders.tsx"
    }
  ],
  "type": "registry:ui"
}