{
  "schemaVersion": 1,
  "target": "react",
  "name": "button",
  "version": "0.2.0",
  "category": "core",
  "title": "Button",
  "description": "A button with primary, secondary, ghost, and destructive variants, a 100ms press scale, and a loading state that keeps its width.",
  "tags": [
    "action",
    "form",
    "loading"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [],
  "docsPath": "/components/button",
  "props": [
    {
      "name": "variant",
      "type": "\"primary\" | \"secondary\" | \"ghost\" | \"destructive\"",
      "default": "\"primary\"",
      "description": "Visual style."
    },
    {
      "name": "size",
      "type": "\"sm\" | \"md\" | \"lg\"",
      "default": "\"md\"",
      "description": "Height and padding."
    },
    {
      "name": "loading",
      "type": "boolean",
      "default": "false",
      "description": "Fades the label out and shows a centered spinner in its place, so the width stays the same. Sets aria-busy and ignores clicks; the button stays focusable and keeps its accessible name."
    },
    {
      "name": "...props",
      "type": "ComponentProps<\"button\">",
      "description": "Any native button attribute. type defaults to \"button\"."
    }
  ],
  "accessibility": [
    "Renders a native <button>, so Enter and Space work without extra code.",
    "Visible focus ring on keyboard focus only (focus-visible).",
    "While loading, focus is kept and aria-busy is announced instead of disabling the button.",
    "The press scale, label fade, and spinner rotation stop when reduced motion is preferred; the spinner is then a static arc."
  ],
  "states": [
    "default",
    "hover",
    "pressed",
    "focus-visible",
    "loading",
    "disabled"
  ],
  "files": [
    {
      "path": "ui/button.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, MouseEvent } from \"react\";\n\nfunction cx(...classes: (string | false | null | undefined)[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n\nconst base = cx(\n  \"relative isolate inline-flex select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium\",\n  // Hover colors ease over 150ms; the press scale answers faster, in 100ms.\n  \"transition-[background-color,border-color,color,box-shadow,scale,opacity] duration-150 ease-out active:duration-100\",\n  \"motion-safe:active:scale-[0.97] motion-reduce:transition-none\",\n  \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500\",\n  \"disabled:pointer-events-none disabled:opacity-45 aria-busy:cursor-progress\",\n  \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n);\n\nconst variants = {\n  primary: cx(\n    \"bg-zinc-950 text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.12),0_1px_2px_rgb(0_0_0/0.2)] hover:bg-zinc-800\",\n    \"dark:bg-white dark:text-black dark:shadow-[inset_0_-1px_0_rgb(0_0_0/0.12)] dark:hover:bg-zinc-200\",\n  ),\n  secondary: cx(\n    \"border border-zinc-950/12 bg-white text-zinc-950 shadow-[0_1px_2px_rgb(0_0_0/0.04)] hover:border-zinc-950/20 hover:bg-zinc-950/[0.03]\",\n    \"dark:border-white/15 dark:bg-black dark:text-white dark:shadow-none dark:hover:border-white/25 dark:hover:bg-white/[0.06]\",\n  ),\n  ghost: cx(\n    \"text-zinc-600 hover:bg-zinc-950/[0.04] hover:text-zinc-950\",\n    \"dark:text-zinc-400 dark:hover:bg-white/[0.06] dark:hover:text-white\",\n  ),\n  destructive:\n    \"bg-red-600 text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.16),0_1px_2px_rgb(0_0_0/0.2)] hover:bg-red-700 dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.16)]\",\n} as const;\n\n// A leading icon pulls the start padding in by 2px so the content looks optically centered.\nconst sizes = {\n  sm: \"h-8 px-3 text-sm has-[>[data-slot=label]>svg:first-child]:pl-2.5\",\n  md: \"h-9 px-3.5 text-sm has-[>[data-slot=label]>svg:first-child]:pl-3\",\n  lg: \"h-10 px-4 text-[15px] has-[>[data-slot=label]>svg:first-child]:pl-3.5\",\n} as const;\n\nexport type ButtonVariant = keyof typeof variants;\nexport type ButtonSize = keyof typeof sizes;\n\nexport interface ButtonProps extends ComponentProps<\"button\"> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  /**\n   * Swaps the label for a spinner and ignores clicks while keeping the button focusable.\n   * The label stays in the layout (only faded), so the button keeps its width and accessible name.\n   */\n  loading?: boolean;\n}\n\nexport function Button({\n  variant = \"primary\",\n  size = \"md\",\n  loading = false,\n  type = \"button\",\n  className,\n  onClick,\n  children,\n  ...props\n}: ButtonProps) {\n  function handleClick(event: MouseEvent<HTMLButtonElement>) {\n    if (loading) {\n      event.preventDefault();\n      return;\n    }\n    onClick?.(event);\n  }\n\n  return (\n    <button\n      type={type}\n      aria-busy={loading || undefined}\n      data-variant={variant}\n      data-loading={loading || undefined}\n      className={cx(base, variants[variant], sizes[size], className)}\n      onClick={handleClick}\n      {...props}\n    >\n      <span\n        data-slot=\"label\"\n        className={cx(\n          \"inline-flex items-center gap-[inherit]\",\n          \"transition-[opacity,scale] duration-150 ease-out motion-reduce:transition-none\",\n          loading && \"opacity-0 motion-safe:scale-[0.96]\",\n        )}\n      >\n        {children}\n      </span>\n      {loading ? <Spinner /> : null}\n    </button>\n  );\n}\n\nfunction Spinner() {\n  return (\n    <span aria-hidden=\"true\" className=\"absolute inset-0 flex items-center justify-center\">\n      <svg\n        viewBox=\"0 0 16 16\"\n        fill=\"none\"\n        className={cx(\n          \"motion-safe:animate-spin\",\n          \"transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\",\n          \"starting:opacity-0 motion-safe:starting:scale-90\",\n        )}\n      >\n        <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"1.5\" />\n        <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n      </svg>\n    </span>\n  );\n}\n"
    }
  ],
  "example": {
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button } from \"@/components/seamless/ui/button\";\n\nexport default function ButtonExample() {\n  const [saving, setSaving] = useState(false);\n\n  function save() {\n    setSaving(true);\n    window.setTimeout(() => setSaving(false), 1600);\n  }\n\n  return (\n    <div className=\"flex flex-wrap items-center justify-center gap-2.5\">\n      <Button>Deploy</Button>\n      <Button variant=\"secondary\">\n        <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\">\n          <path d=\"M8 3.5v9M3.5 8h9\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n        </svg>\n        New agent\n      </Button>\n      <Button variant=\"ghost\">Cancel</Button>\n      <Button variant=\"destructive\">Delete</Button>\n      <Button variant=\"secondary\" loading={saving} onClick={save}>\n        Save changes\n      </Button>\n    </div>\n  );\n}\n"
  }
}
