{"name":"button","type":"registry:component","title":"Button","description":"An atomic button component built with Lit and styled using Tailwind CSS. Supports multiple variants and sizes for versatile use in web applications.","categories":["ui","button","web-component"],"author":"Lloyd Richards <lloyd.d.richards@gmail.com>","dependencies":["lucide-static"],"files":[{"path":"registry/ui/button/button.ts","type":"registry:ui","content":"import { cva, type VariantProps } from \"class-variance-authority\";\nimport { html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { TW } from \"@/lib/tailwindMixin\";\n\nexport const buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"w-full h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"w-full h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"w-full h-10 rounded-md px-6 has-[>svg]:px-4\",\n        \"icon-sm\": \"size-8\",\n        icon: \"size-9\",\n        \"icon-lg\": \"size-15\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\ntype ButtonVariants = VariantProps<typeof buttonVariants>;\n\nexport type ButtonProperties = {\n  variant?: ButtonVariants[\"variant\"];\n  size?: ButtonVariants[\"size\"];\n  type: \"button\" | \"submit\" | \"reset\";\n  disabled: boolean;\n  ariaLabel: string | null;\n  ariaDescribedby: string | null;\n  ariaLabelledby: string | null;\n};\n\n@customElement(\"ui-button\")\nexport class Button extends TW(LitElement) implements ButtonVariants {\n  static formAssociated = true;\n  private internals: ElementInternals;\n\n  @property({ type: String }) variant: ButtonVariants[\"variant\"] = \"default\";\n  @property({ type: String }) size: ButtonVariants[\"size\"] = \"default\";\n  @property({ type: String }) type: \"button\" | \"submit\" | \"reset\" = \"button\";\n  @property({ type: Boolean }) disabled = false;\n\n  @property({ type: String, attribute: \"aria-label\" }) accessor ariaLabel:\n    | string\n    | null = null;\n  @property({ type: String, attribute: \"aria-describedby\" })\n  accessor ariaDescribedby: string | null = null;\n  @property({ type: String, attribute: \"aria-labelledby\" })\n  accessor ariaLabelledby: string | null = null;\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  private get isDisabled() {\n    return this.disabled;\n  }\n\n  private get buttonClasses() {\n    return buttonVariants({ variant: this.variant, size: this.size });\n  }\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (changedProperties.has(\"disabled\")) {\n      this.setAttribute(\"aria-disabled\", String(this.isDisabled));\n    }\n  }\n\n  private handleClick(e: Event) {\n    if (this.type === \"submit\" && this.internals.form) {\n      e.preventDefault();\n      this.internals.form.requestSubmit();\n    } else if (this.type === \"reset\" && this.internals.form) {\n      e.preventDefault();\n      this.internals.form.reset();\n    }\n  }\n\n  override render() {\n    return html`\n      <button\n        type=${this.type}\n        class=${this.buttonClasses}\n        ?disabled=${this.isDisabled}\n        aria-label=${this.ariaLabel || nothing}\n        aria-describedby=${this.ariaDescribedby || nothing}\n        aria-labelledby=${this.ariaLabelledby || nothing}\n        @click=${this.handleClick}\n      >\n        <slot></slot>\n      </button>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    \"ui-button\": Button;\n  }\n}\n"},{"path":"registry/ui/button/button.stories.ts","type":"registry:ui","content":"import \"./button\";\nimport type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport type { TemplateResult } from \"lit\";\nimport { html } from \"lit\";\nimport type { UnsafeSVGDirective } from \"lit/directives/unsafe-svg.js\";\nimport { unsafeSVG } from \"lit/directives/unsafe-svg.js\";\nimport type { DirectiveResult } from \"lit-html/directive.js\";\nimport { Search } from \"lucide-static\";\nimport type { ButtonProperties } from \"./button\";\n\ntype ButtonArgs = ButtonProperties & {\n  children?:\n    | string\n    | TemplateResult\n    | DirectiveResult<typeof UnsafeSVGDirective>;\n};\n\n/**\n * Displays a button or a component that looks like a button.\n */\nconst meta: Meta<ButtonArgs> = {\n  title: \"ui/Button\",\n  component: \"ui-button\",\n  tags: [\"autodocs\"],\n  argTypes: {\n    variant: {\n      control: \"select\",\n      options: [\n        \"default\",\n        \"destructive\",\n        \"outline\",\n        \"secondary\",\n        \"ghost\",\n        \"link\",\n      ],\n    },\n    size: {\n      control: \"select\",\n      options: [\"default\", \"sm\", \"lg\", \"icon-sm\", \"icon\", \"icon-lg\"],\n      if: { arg: \"variant\", neq: \"link\" },\n    },\n    type: {\n      control: \"select\",\n      options: [\"button\", \"submit\", \"reset\"],\n    },\n    disabled: {\n      control: \"boolean\",\n    },\n    ariaLabel: {\n      control: \"text\",\n    },\n    ariaDescribedby: {\n      control: \"text\",\n    },\n    ariaLabelledby: {\n      control: \"text\",\n    },\n    children: {\n      control: \"text\",\n    },\n  },\n  parameters: {\n    layout: \"centered\",\n  },\n  args: {\n    variant: \"default\",\n    size: \"default\",\n    type: \"button\",\n    disabled: false,\n    children: \"Button\",\n  },\n  render: (args) =>\n    html`<ui-button\n      .variant=${args.variant}\n      .size=${args.size}\n      .type=${args.type}\n      .disabled=${args.disabled}\n      .ariaLabel=${args.ariaLabel}\n      .ariaDescribedby=${args.ariaDescribedby}\n      .ariaLabelledby=${args.ariaLabelledby}\n    >\n      ${args.children}\n    </ui-button>`,\n};\n\nexport default meta;\n\ntype Story = StoryObj<ButtonArgs>;\n\n/**\n * The default form of the button, used for primary actions and commands.\n */\nexport const Default: Story = {};\n\n/**\n * Use the `outline` button to reduce emphasis on secondary actions, such as\n * canceling or dismissing a dialog.\n */\nexport const Outline: Story = {\n  args: {\n    variant: \"outline\",\n  },\n};\n\n/**\n * Use the `ghost` button is minimalistic and subtle, for less intrusive\n * actions.\n */\nexport const Ghost: Story = {\n  args: {\n    variant: \"ghost\",\n  },\n};\n\n/**\n * Use the `secondary` button to call for less emphasized actions, styled to\n * complement the primary button while being less conspicuous.\n */\nexport const Secondary: Story = {\n  args: {\n    variant: \"secondary\",\n  },\n};\n\n/**\n * Use the `destructive` button to indicate errors, alerts, or the need for\n * immediate attention.\n */\nexport const Destructive: Story = {\n  args: {\n    variant: \"destructive\",\n  },\n};\n\n/**\n * Use the `link` button to reduce emphasis on tertiary actions, such as\n * hyperlink or navigation, providing a text-only interactive element.\n */\nexport const Link: Story = {\n  args: {\n    variant: \"link\",\n  },\n};\n\n/**\n * Use the `sm` size for a smaller button, suitable for interfaces needing\n * compact elements without sacrificing usability.\n */\nexport const Small: Story = {\n  args: {\n    size: \"sm\",\n  },\n};\n\n/**\n * Use the `lg` size for a larger button, offering better visibility and\n * easier interaction for users.\n */\nexport const Large: Story = {\n  args: {\n    size: \"lg\",\n  },\n};\n\n/**\n * Use the \"icon\" size for a button with only an icon.\n */\nexport const Icon: Story = {\n  args: {\n    variant: \"secondary\",\n    size: \"icon\",\n    ariaLabel: \"Icon button\",\n    children: unsafeSVG(Search),\n  },\n};\n\n/**\n * Use the `icon-sm` size for a smaller icon-only button.\n */\nexport const IconSmall: Story = {\n  args: {\n    variant: \"secondary\",\n    size: \"icon-sm\",\n    ariaLabel: \"Small icon button\",\n    children: unsafeSVG(Search),\n  },\n};\n\n/**\n * Use the `icon-lg` size for a larger icon-only button.\n */\nexport const IconLarge: Story = {\n  args: {\n    variant: \"secondary\",\n    size: \"icon-lg\",\n    ariaLabel: \"Large icon button\",\n    children: unsafeSVG(Search),\n  },\n};\n\n/**\n * Add the `disabled` prop to prevent interactions with the button.\n */\nexport const Disabled: Story = {\n  args: {\n    disabled: true,\n  },\n};\n"}]}