Components
Loading preview...
Here is Button component
npx shadcn@latest add https://21st.dev/r/intentui/button7"use client"
import { IconCircleCheckFill, IconKey, IconLoader } from "@intentui/icons"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { ProgressBar } from "react-aria-components"
import { twMerge } from "tailwind-merge"
import type { VariantProps } from "tailwind-variants"
import { tv } from "tailwind-variants"
const loaderStyles = tv({
base: "relative",
variants: {
intent: {
current: "text-current",
primary: "text-primary",
secondary: "text-muted-fg",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
},
size: {
sm: "size-4",
md: "size-6",
lg: "size-8",
xl: "size-10",
},
},
defaultVariants: {
intent: "current",
size: "sm",
},
})
type LoaderVariantProps = VariantProps<typeof loaderStyles>
const Bars = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
<svg
className={twMerge("size-4", className)}
data-slot="icon"
viewBox="0 0 135 140"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
{...props}
>
<rect y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.5s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.5s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.25s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.25s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate
attributeName="height"
begin="0s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.25s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.25s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.5s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.5s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
</svg>
)
const Ring = (props: React.SVGProps<SVGSVGElement>) => <IconLoader {...props} />
const Spin = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
<svg
className={twMerge("size-4", className)}
data-slot="icon"
viewBox="0 0 2400 2400"
{...props}
>
<g strokeWidth="200" strokeLinecap="round" fill="none">
<line x1="1200" y1="600" x2="1200" y2="100" />
<line opacity="0.5" x1="1200" y1="2300" x2="1200" y2="1800" />
<line opacity="0.917" x1="900" y1="680.4" x2="650" y2="247.4" />
<line opacity="0.417" x1="1750" y1="2152.6" x2="1500" y2="1719.6" />
<line opacity="0.833" x1="680.4" y1="900" x2="247.4" y2="650" />
<line opacity="0.333" x1="2152.6" y1="1750" x2="1719.6" y2="1500" />
<line opacity="0.75" x1="600" y1="1200" x2="100" y2="1200" />
<line opacity="0.25" x1="2300" y1="1200" x2="1800" y2="1200" />
<line opacity="0.667" x1="680.4" y1="1500" x2="247.4" y2="1750" />
<line opacity="0.167" x1="2152.6" y1="650" x2="1719.6" y2="900" />
<line opacity="0.583" x1="900" y1="1719.6" x2="650" y2="2152.6" />
<line opacity="0.083" x1="1750" y1="247.4" x2="1500" y2="680.4" />
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
keyTimes="0;0.08333;0.16667;0.25;0.33333;0.41667;0.5;0.58333;0.66667;0.75;0.83333;0.91667"
values="0 1199 1199;30 1199 1199;60 1199 1199;90 1199 1199;120 1199 1199;150 1199 1199;180 1199 1199;210 1199 1199;240 1199 1199;270 1199 1199;300 1199 1199;330 1199 1199"
dur="0.83333s"
begin="0.08333s"
repeatCount="indefinite"
calcMode="discrete"
/>
</g>
</svg>
)
const LOADERS = {
bars: Bars,
ring: Ring,
spin: Spin,
}
const DEFAULT_SPINNER = "spin"
interface LoaderProps
extends Omit<React.ComponentPropsWithoutRef<"svg">, "display" | "opacity" | "intent">,
LoaderVariantProps {
variant?: keyof typeof LOADERS
percentage?: number
isIndeterminate?: boolean
formatOptions?: Intl.NumberFormatOptions
ref?: React.RefObject<SVGSVGElement>
}
const Loader = ({ isIndeterminate = true, ref, ...props }: LoaderProps) => {
const { className, variant = DEFAULT_SPINNER, intent, size, ...spinnerProps } = props
const LoaderPrimitive = LOADERS[variant in LOADERS ? variant : DEFAULT_SPINNER]
return (
<ProgressBar
data-slot="loader"
aria-label={props["aria-label"] ?? "Loading..."}
formatOptions={props.formatOptions}
isIndeterminate={isIndeterminate}
>
<LoaderPrimitive
role="presentation"
className={loaderStyles({
intent,
size,
className: twMerge([
["ring"].includes(variant) && "animate-spin",
variant === "spin" && "stroke-current",
className,
]),
})}
ref={ref}
{...spinnerProps}
/>
</ProgressBar>
)
}
// --- Main component using the inlined Loader ---
// Key changes:
// 1) `clickCount` increments on press — used as `key` to force remount of Loader
// 2) Handler attached to both onPress and onClick to ensure it fires
export default function Component() {
const [loading, setLoading] = useState<"idle" | "loading" | "success">("idle")
const [clickCount, setClickCount] = useState(0) // forces remount
const pressHandler = () => {
// force remount every click so SMIL animations restart
setClickCount((c) => c + 1)
setLoading("loading")
setTimeout(() => setLoading("success"), 3000)
setTimeout(() => setLoading("idle"), 6000)
}
return (
<Button
// attach both handlers (some button libs expect onPress, others onClick)
onPress={pressHandler}
onClick={pressHandler}
isPending={loading === "loading"}
className="w-52 justify-between px-4 py-2"
intent="primary"
>
{loading === "success" ? (
<IconCircleCheckFill />
) : loading === "loading" ? (
// key={clickCount} forces the Loader SVG to remount each click (restarts SMIL)
<Loader key={clickCount} variant="spin" />
) : (
<IconKey />
)}
{loading === "loading"
? "Generating Key..."
: loading === "success"
? "Key Generated!"
: "Generate API Key"}
</Button>
)
}