import * as React from "react"
import { IconLoader2 } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/registry/lib/utils"
const spinnerVariants = cva("inline-flex shrink-0 items-center justify-center [&_svg]:!size-full", {
variants: {
size: {
xs: "size-3",
sm: "size-3.5",
default: "size-4",
lg: "size-5",
xl: "size-6",
},
},
defaultVariants: { size: "default" },
})
type SpinnerProps = Omit<React.ComponentProps<"span">, "children"> &
VariantProps<typeof spinnerVariants> & {
/** What is loading, for screen readers: "Loading photo". Adds
* `role="status"`. Leave it off when the spinner sits inside something
* that already says it is busy (a Button with `loading`), and it is
* hidden from them instead. */
label?: string
}
// @use-when an indeterminate "still working" mark that is not a button: a photo
// slot that is loading, a preview pane waiting on a result, a menu item running
// an action. Button already draws this same mark for its `loading` state.
function Spinner({ size, label, className, ...props }: SpinnerProps) {
return (
<span
data-slot="spinner"
role={label ? "status" : undefined}
aria-hidden={label ? undefined : true}
className={cn(spinnerVariants({ size }), className)}
{...props}
>
{/* motion-exempt: an indeterminate spinner is the only "still working" cue,
so it keeps spinning under reduced motion. Stopping it reads as frozen
rather than as calm. */}
<IconLoader2 className="animate-spin" />
{label ? <span className="sr-only">{label}</span> : null}
</span>
)
}
// The box steps with the icon ramp (12 / 14 / 16 / 20 / 24). The wrapper owns
// the size and the glyph fills it with `!size-full`, because a parent that sizes
// its icons (`[&_svg]:size-3.5` on a Button) would otherwise outrank a class on
// the glyph itself.
export { Spinner, spinnerVariants }
export type { SpinnerProps }