# Animated Generate Button

> AI generate button: swaps to a spinner with a breathing glow while running; works controlled or uncontrolled.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/animated-generate-button
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/animated-generate-button.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/animated-generate-button-demo.tsx"
import { AnimatedGenerateButton } from "@/components/animated-generate-button"

export default function AnimatedGenerateButtonDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-6 p-8">
      <AnimatedGenerateButton />
      <AnimatedGenerateButton idleLabel="Write with AI" generatingLabel="Thinking…" duration={4000} />
      <AnimatedGenerateButton generating generatingLabel="Rendering…" />
    </div>
  )
}
```

## Source

```tsx title="components/animated-generate-button.tsx"
"use client"

import * as React from "react"
import { Loader2, Sparkles } from "lucide-react"

import { cn } from "@/lib/utils"

const css = `@keyframes generate-breathe{0%,100%{opacity:.35;transform:scale(1)}50%{opacity:.85;transform:scale(1.06)}}@media (prefers-reduced-motion:reduce){.generate-halo{animation:none!important}}`

export interface AnimatedGenerateButtonProps extends React.ComponentProps<"button"> {
  /** Controlled generating state. Leave undefined to let the button simulate a run on click. */
  generating?: boolean
  /** Simulated run length in milliseconds when uncontrolled */
  duration?: number
  idleLabel?: string
  generatingLabel?: string
}

export function AnimatedGenerateButton({
  generating,
  duration = 2500,
  idleLabel = "Generate",
  generatingLabel = "Generating…",
  className,
  onClick,
  ...props
}: AnimatedGenerateButtonProps) {
  const [internal, setInternal] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
  const busy = generating ?? internal

  React.useEffect(
    () => () => {
      if (timer.current) clearTimeout(timer.current)
    },
    []
  )

  return (
    <>
      <style href="animated-generate-button" precedence="default">
        {css}
      </style>
      <button
        type="button"
        aria-busy={busy}
        className={cn(
          "group relative isolate inline-flex h-12 min-w-44 items-center justify-center gap-2 rounded-xl px-7 text-sm font-semibold text-white",
          "bg-gradient-to-r from-violet-600 to-indigo-600 shadow-[inset_0_1px_0_rgba(255,255,255,0.3)]",
          "transition-transform duration-200 active:scale-[0.97]",
          "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
          "disabled:pointer-events-none disabled:opacity-50",
          busy && "cursor-progress",
          className
        )}
        onClick={(event) => {
          onClick?.(event)
          if (generating !== undefined || busy) return
          setInternal(true)
          timer.current = setTimeout(() => setInternal(false), duration)
        }}
        {...props}
      >
        <span
          aria-hidden
          className={cn(
            "generate-halo pointer-events-none absolute -inset-1 -z-10 rounded-2xl bg-gradient-to-r from-violet-500 to-indigo-500 blur-lg transition-opacity duration-300",
            busy ? "opacity-70" : "opacity-0 group-hover:opacity-50"
          )}
          style={busy ? { animation: "generate-breathe 1.6s ease-in-out infinite" } : undefined}
        />
        {busy ? <Loader2 className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
        <span>{busy ? generatingLabel : idleLabel}</span>
      </button>
    </>
  )
}
```
