{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "type": "registry:ui",
  "description": "Interactive button component with variants (default, destructive, outline, secondary, ghost, link), sizes, and composite ButtonGroup container for segmented toolbars and split buttons.",
  "categories": [
    "control"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/button/button.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { buttonVariants, type ButtonVariants } from './button.variants'\n\ntype Variant = NonNullable<ButtonVariants['variant']>\ntype Size = NonNullable<ButtonVariants['size']>\n\n/**\n * <uip-button> — the registry Button as a web component.\n *\n * Class strings are React's `buttonVariants` verbatim. Two shadow-DOM\n * adaptations (a light-DOM <svg> is not a descendant of the inner <button>):\n *  - icon sizing moves from `[&_svg]:…` to `::slotted(svg)` on the <slot>;\n *  - React's `has-[>svg]:px-*` can't see slotted icons, so the element sets\n *    `data-icon` on the inner button when a slotted <svg> is present and the\n *    size padding is re-expressed as `data-[icon]:px-*`.\n * `href` renders an <a> (the web-component stand-in for React's `asChild`).\n *\n * Inside a <uip-button-group> the group sets `data-group-position`\n * (first | middle | last | only) and `data-group-orientation` on the host; the\n * button then applies React ButtonGroup's child rules to its inner element\n * (rounding, `-ml-px` / `-mt-px`, divider border per variant, hover/focus z-index).\n *\n * Form-associated (like React's <button>, which extends ButtonHTMLAttributes):\n * `type` (`button` | `submit` | `reset`) drives the ancestor form — `submit`\n * calls `requestSubmit()`, `reset` calls `reset()`. A shadow-DOM <button> can't\n * submit the host's form on its own, so the click is forwarded through\n * ElementInternals. `name` + `value` are submitted as name=value only on the\n * submission this button triggers (set just before requestSubmit, cleared\n * after). `formaction` / `formmethod` / `formenctype` / `formnovalidate` /\n * `formtarget` (React's formAction …) temporarily override the form's\n * attributes for that submission. The native `form` attribute (form id) works\n * as-is via form association — no property needed.\n */\nconst activeFormSubmissions = new WeakMap<HTMLFormElement, { count: number; saved: Map<string, string | null> }>()\n\nexport class UipButton extends LitElement {\n  static formAssociated = true\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: inline-flex; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    size: { reflect: true },\n    type: { reflect: true },\n    name: { reflect: true },\n    value: {},\n    formAction: { attribute: 'formaction' },\n    formMethod: { attribute: 'formmethod' },\n    formEnctype: { attribute: 'formenctype' },\n    formNoValidate: { type: Boolean, attribute: 'formnovalidate' },\n    formTarget: { attribute: 'formtarget' },\n    disabled: { type: Boolean, reflect: true },\n    href: {},\n    // The focusable element is the inner <button>, so the host's aria-label\n    // must be forwarded or icon-only buttons have no accessible name.\n    accessibleLabel: { attribute: 'aria-label' },\n    // Popup-trigger state must reach the inner <button> too, or assistive tech\n    // never hears it when a uip-button is a menu / popover / collapsible trigger.\n    fwdExpanded: { attribute: 'aria-expanded' },\n    fwdHaspopup: { attribute: 'aria-haspopup' },\n    fwdDescription: { attribute: 'aria-description' },\n    fwdControls: { attribute: 'aria-controls' },\n    target: {},\n    groupPosition: { attribute: 'data-group-position' },\n    groupOrientation: { attribute: 'data-group-orientation' },\n    hasIcon: { state: true },\n  }\n\n  variant: Variant = 'default'\n  size: Size = 'default'\n  type: 'button' | 'submit' | 'reset' = 'button'\n  name?: string\n  value?: string\n  formAction?: string\n  formMethod?: string\n  formEnctype?: string\n  formNoValidate = false\n  formTarget?: string\n  disabled = false\n  href?: string\n  accessibleLabel?: string\n  fwdExpanded?: string\n  fwdHaspopup?: string\n  fwdDescription?: string\n  fwdControls?: string\n  target?: string\n  groupPosition?: 'first' | 'middle' | 'last' | 'only'\n  groupOrientation?: 'horizontal' | 'vertical'\n  private hasIcon = false\n\n  private internals = this.attachInternals()\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'button')\n  }\n\n  willUpdate() {\n    this.setAttribute('data-variant', this.variant)\n    this.setAttribute('data-size', this.size)\n  }\n\n  // aria-controls is an id in the host's tree; an id can't be referenced from\n  // inside the shadow root, so point the inner control at the element itself.\n  protected updated() {\n    const inner = this.renderRoot.querySelector<HTMLElement>('[part=base]')\n    if (!inner) return\n    const root = this.getRootNode() as Document | ShadowRoot\n    const target = this.fwdControls ? root.getElementById?.(this.fwdControls) : null\n    inner.ariaControlsElements = target ? [target] : null\n  }\n\n  private onSlotChange(e: Event) {\n    const slot = e.target as HTMLSlotElement\n    this.hasIcon = slot.assignedElements().some((el) => el.localName === 'svg')\n  }\n\n  formDisabledCallback(disabled: boolean) {\n    this.disabled = disabled\n  }\n\n  // A <button> inside a shadow root can't submit the host's form on its own,\n  // so submit/reset are forwarded through ElementInternals. The inner button\n  // keeps the real `type` for semantics/inspection; the outer form action\n  // happens here.\n  private onClick() {\n    if (this.disabled || this.href) return\n    const form = this.internals.form\n    if (!form) return\n    if (this.type === 'reset') {\n      form.reset()\n      return\n    }\n    if (this.type !== 'submit') return\n    // Native submit buttons send name=value only when they are the submitter.\n    // Emulate that: publish the value just for this submission, then clear it\n    // once the form data has been captured (formdata) or on the next tick as\n    // a fallback (submit prevented / no data).\n    if (this.name != null && this.value != null) this.internals.setFormValue(this.value)\n    else this.internals.setFormValue(null)\n    // form* overrides: stash the form's attributes, apply the button's, and\n    // restore after capture. Guard against rapid re-entrant clicks so initial\n    // attributes are never clobbered by in-flight overrides.\n    let entry = activeFormSubmissions.get(form)\n    if (!entry) {\n      const saved = new Map<string, string | null>()\n      const stash = (attr: string) => saved.set(attr, form.getAttribute(attr))\n      stash('action')\n      stash('method')\n      stash('enctype')\n      stash('target')\n      stash('novalidate')\n      entry = { count: 1, saved }\n      activeFormSubmissions.set(form, entry)\n    } else {\n      entry.count += 1\n    }\n\n    if (this.formAction != null) form.setAttribute('action', this.formAction)\n    if (this.formMethod != null) form.setAttribute('method', this.formMethod)\n    if (this.formEnctype != null) form.setAttribute('enctype', this.formEnctype)\n    if (this.formTarget != null) form.setAttribute('target', this.formTarget)\n    if (this.formNoValidate) form.setAttribute('novalidate', '')\n\n    let restored = false\n    const restore = () => {\n      if (restored) return\n      restored = true\n      const currentEntry = activeFormSubmissions.get(form)\n      if (currentEntry) {\n        currentEntry.count -= 1\n        if (currentEntry.count <= 0) {\n          activeFormSubmissions.delete(form)\n          for (const [attr, val] of currentEntry.saved) {\n            if (val == null) form.removeAttribute(attr)\n            else form.setAttribute(attr, val)\n          }\n        }\n      }\n      this.internals.setFormValue(null)\n      form.removeEventListener('formdata', restore)\n    }\n    form.addEventListener('formdata', restore, { once: true })\n    setTimeout(restore, 0)\n    form.requestSubmit()\n  }\n\n  // React ButtonGroup's `[&>[data-slot=button]…]` rules, applied from inside.\n  private groupClasses() {\n    const pos = this.groupPosition\n    if (!pos) return ''\n    const v = this.groupOrientation === 'vertical'\n    const rounding = {\n      first: v ? 'rounded-t-md rounded-b-none' : 'rounded-l-md rounded-r-none',\n      middle: 'rounded-none',\n      last: v ? 'rounded-t-none rounded-b-md' : 'rounded-l-none rounded-r-md',\n      only: 'rounded-md',\n    }[pos]\n    const joined = pos === 'middle' || pos === 'last'\n    const divider = joined\n      ? ({ default: 'border-primary-foreground/20', secondary: 'border-border', destructive: 'border-white/20' } as Record<string, string>)[\n          this.variant\n        ]\n      : undefined\n    return cn(\n      'relative hover:z-10 focus-visible:z-20',\n      rounding,\n      joined && (v ? '-mt-px' : '-ml-px'),\n      divider && [divider, v ? 'border-t' : 'border-l'],\n    )\n  }\n\n  render() {\n    const classes = cn(\n      buttonVariants({ variant: this.variant, size: this.size }),\n      // `grow`: fill the host when a layout stretches it (flex-col, w-full).\n      'grow data-[size=default]:data-[icon]:px-3 data-[size=sm]:data-[icon]:px-2.5 data-[size=lg]:data-[icon]:px-4 data-[size=xs]:data-[icon]:px-1.5',\n      this.groupClasses(),\n    )\n    const slot = html`<slot\n      class=\"[&::slotted(svg)]:pointer-events-none [&::slotted(svg)]:shrink-0 [&::slotted(svg:not([class*='size-']))]:size-4\"\n      @slotchange=${this.onSlotChange}\n    ></slot>`\n    if (this.href) {\n      return html`<a\n        part=\"base\"\n        class=${classes}\n        href=${this.disabled ? nothing : this.href}\n        target=${this.target ?? nothing}\n        aria-disabled=${this.disabled ? 'true' : nothing}\n        aria-expanded=${this.fwdExpanded ?? nothing}\n        aria-haspopup=${this.fwdHaspopup ?? nothing}\n        aria-description=${this.fwdDescription ?? nothing}\n        aria-label=${this.accessibleLabel ?? nothing}\n        data-size=${this.size}\n        ?data-icon=${this.hasIcon}\n        >${slot}</a\n      >`\n    }\n    return html`<button\n      part=\"base\"\n      class=${classes}\n      type=${this.type}\n      name=${this.name ?? nothing}\n      value=${this.value ?? nothing}\n      formaction=${this.formAction ?? nothing}\n      formmethod=${this.formMethod ?? nothing}\n      formenctype=${this.formEnctype ?? nothing}\n      ?formnovalidate=${this.formNoValidate}\n      formtarget=${this.formTarget ?? nothing}\n      ?disabled=${this.disabled}\n      aria-expanded=${this.fwdExpanded ?? nothing}\n      aria-haspopup=${this.fwdHaspopup ?? nothing}\n      aria-description=${this.fwdDescription ?? nothing}\n      aria-label=${this.accessibleLabel ?? nothing}\n      data-size=${this.size}\n      ?data-icon=${this.hasIcon}\n      @click=${this.onClick}\n    >\n      ${slot}\n    </button>`\n  }\n}\n\ncustomElements.get('uip-button') || customElements.define('uip-button', UipButton)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-button': UipButton\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/button/button.ts"
    },
    {
      "path": "packages/elements-lit/src/components/button/button.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so `Button.vue` can `import { buttonVariants } from\n * './button.variants'` without creating a circular dependency through the\n * index. See card.variants.ts for the same pattern + the SSR symptom that\n * motivated the split.\n */\nexport const buttonVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity,transform,scale] duration-150 active:scale-[0.97] active:duration-100 touch-manipulation disabled:cursor-not-allowed 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 shadow-xs hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-white shadow-xs 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 shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n        secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n        ghost: '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: 'h-9 px-4 py-2 has-[>svg]:px-3',\n        sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',\n        lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n        xs: 'h-7 rounded-md gap-1 px-2 has-[>svg]:px-1.5 text-xs',\n        icon: 'size-9',\n        'icon-sm': 'size-8',\n        'icon-lg': 'size-10',\n        // Square counterpart of `xs` (h-7). Dense data toolbars run at 28px and\n        // had no icon-only tier to match, so icon controls sat 4px taller than\n        // the buttons beside them. Icons inside carry their own size class.\n        'icon-xs': 'size-7',\n        // Row-action tier: the hover-revealed copy control inside a two-line\n        // table cell. Anything taller than the 20px text line it sits on makes\n        // the row grow when the control appears.\n        'icon-2xs': 'size-5',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n)\n\nexport type ButtonVariants = VariantProps<typeof buttonVariants>\n",
      "type": "registry:file",
      "target": "src/uip/components/button/button.variants.ts"
    },
    {
      "path": "packages/elements-lit/src/components/button-group/button-group.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\ntype Orientation = 'horizontal' | 'vertical'\nexport type ButtonGroupPosition = 'first' | 'middle' | 'last' | 'only'\n\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== null && v !== 'false' }\n\n/**\n * <uip-button-group> — the registry ButtonGroup as a web component.\n *\n * React joins its buttons with child selectors (`[&>[data-slot=button]]:rounded-none`,\n * `:first-child` → `rounded-l-md`, `:not(:first-child)` → `-ml-px` + a divider border).\n * Those can't reach the inner <button> inside uip-button's shadow root, so the group\n * tells each direct child where it sits instead: it sets\n * `data-group-position=\"first|middle|last|only\"` and `data-group-orientation` on every\n * element child (on `slotchange` and whenever `orientation` / `attached` change), and\n * uip-button maps them to React's exact rounding / border / negative-margin classes on\n * its inner button. Native <button> children are not styled (use uip-button).\n *\n * Props: `orientation` (horizontal | vertical, default horizontal), `attached`\n * (default true; `attached=\"false\"` spaces the buttons with React's gap instead).\n * The host has role=\"group\" — give it an `aria-label`.\n */\nexport class UipButtonGroup extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: inline-flex; }`]\n\n  static properties = {\n    orientation: { reflect: true },\n    attached: { converter: trueByDefault },\n  }\n\n  orientation: Orientation = 'horizontal'\n  attached = true\n\n  private internals = this.attachInternals()\n  private marked = new Set<Element>()\n\n  constructor() {\n    super()\n    new ThemeController(this)\n    this.internals.role = 'group'\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'button-group')\n  }\n\n  protected willUpdate() {\n    this.setAttribute('data-orientation', this.orientation)\n    this.toggleAttribute('data-attached', this.attached)\n  }\n\n  protected updated() {\n    this.markChildren()\n  }\n\n  private markChildren() {\n    const children = [...this.children]\n    for (const el of this.marked) {\n      if (children.includes(el)) continue\n      el.removeAttribute('data-group-position')\n      el.removeAttribute('data-group-orientation')\n    }\n    this.marked = new Set(this.attached ? children : [])\n    children.forEach((el, i) => {\n      if (!this.attached) {\n        el.removeAttribute('data-group-position')\n        el.removeAttribute('data-group-orientation')\n        return\n      }\n      const last = children.length - 1\n      const position: ButtonGroupPosition = last === 0 ? 'only' : i === 0 ? 'first' : i === last ? 'last' : 'middle'\n      el.setAttribute('data-group-position', position)\n      el.setAttribute('data-group-orientation', this.orientation === 'vertical' ? 'vertical' : 'horizontal')\n    })\n  }\n\n  render() {\n    const vertical = this.orientation === 'vertical'\n    return html`<div\n      part=\"base\"\n      data-slot=\"button-group\"\n      data-orientation=${vertical ? 'vertical' : 'horizontal'}\n      ?data-attached=${this.attached}\n      class=${cn(\n        'inline-flex items-center',\n        vertical ? 'flex-col items-stretch' : 'flex-row',\n        !this.attached && (vertical ? 'gap-1' : 'gap-1.5'),\n      )}\n    >\n      <slot @slotchange=${() => this.markChildren()}></slot>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-button-group') || customElements.define('uip-button-group', UipButtonGroup)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-button-group': UipButtonGroup\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/button-group/button-group.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
