{"name":"toggle","type":"registry:component","title":"Toggle","description":"A two-state button that can be either on or off. Built with Lit and styled using Tailwind CSS. Supports multiple variants and sizes with form integration.","categories":["ui","toggle","button","web-component"],"author":"Lloyd Richards <lloyd.d.richards@gmail.com>","dependencies":["lucide-static"],"files":[{"path":"registry/ui/toggle/toggle.ts","type":"registry:ui","content":"import { cva, type VariantProps } from \"class-variance-authority\";\nimport { html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { TW } from \"@/registry/lib/tailwindMixin\";\n\nexport const toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-2 min-w-9\",\n        sm: \"h-8 px-1.5 min-w-8\",\n        lg: \"h-10 px-2.5 min-w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\ntype ToggleVariants = VariantProps<typeof toggleVariants>;\n\nexport interface ToggleChangeEvent extends CustomEvent {\n  detail: { pressed: boolean };\n}\n\nexport interface ToggleProperties {\n  variant?: ToggleVariants[\"variant\"];\n  size?: ToggleVariants[\"size\"];\n  disabled?: boolean;\n  pressed?: boolean;\n  defaultPressed?: boolean;\n}\n\n@customElement(\"ui-toggle\")\nexport class Toggle extends TW(LitElement) implements ToggleProperties {\n  static formAssociated = true;\n  private internals: ElementInternals;\n\n  @property({ type: String }) variant: ToggleVariants[\"variant\"] = \"default\";\n  @property({ type: String }) size: ToggleVariants[\"size\"] = \"default\";\n  @property({ type: Boolean }) disabled = false;\n\n  @property({ type: Boolean }) pressed: boolean | undefined = undefined;\n  @property({ type: Boolean, attribute: \"default-pressed\" })\n  defaultPressed = false;\n\n  @property({ type: String, attribute: \"aria-label\" }) accessor ariaLabel:\n    | string\n    | null = null;\n\n  @state() private _pressed = false;\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  override connectedCallback() {\n    super.connectedCallback();\n    if (this.pressed === undefined) {\n      this._pressed = this.defaultPressed;\n    }\n  }\n\n  private get isPressed(): boolean {\n    return this.pressed !== undefined ? this.pressed : this._pressed;\n  }\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (\n      changedProperties.has(\"pressed\") ||\n      changedProperties.has(\"_pressed\") ||\n      changedProperties.has(\"disabled\")\n    ) {\n      const state = this.isPressed ? \"on\" : \"off\";\n      this.setAttribute(\"data-state\", state);\n      this.setAttribute(\"aria-pressed\", String(this.isPressed));\n\n      if (this.disabled) {\n        this.setAttribute(\"data-disabled\", \"\");\n      } else {\n        this.removeAttribute(\"data-disabled\");\n      }\n\n      this.internals.setFormValue(this.isPressed ? \"true\" : \"false\");\n    }\n  }\n\n  private handleClick() {\n    if (this.disabled) return;\n\n    if (this.pressed === undefined) {\n      this._pressed = !this._pressed;\n    }\n\n    this.dispatchEvent(\n      new CustomEvent(\"change\", {\n        detail: {\n          pressed: this.pressed !== undefined ? !this.pressed : this._pressed,\n        },\n        bubbles: true,\n        composed: true,\n      }),\n    );\n  }\n\n  override render() {\n    return html`\n      <button\n        type=\"button\"\n        class=${toggleVariants({\n          variant: this.variant,\n          size: this.size,\n        })}\n        ?disabled=${this.disabled}\n        aria-pressed=${this.isPressed}\n        aria-label=${this.ariaLabel || nothing}\n        data-state=${this.isPressed ? \"on\" : \"off\"}\n        @click=${this.handleClick}\n      >\n        <slot></slot>\n      </button>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    \"ui-toggle\": Toggle;\n  }\n}\n"},{"path":"registry/ui/toggle/toggle.stories.ts","type":"registry:ui","content":"import \"./toggle\";\nimport type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport { html } from \"lit\";\nimport { unsafeSVG } from \"lit/directives/unsafe-svg.js\";\nimport { Bold, Italic } from \"lucide-static\";\nimport type { ToggleProperties } from \"./toggle\";\n\ntype ToggleArgs = ToggleProperties & {\n  icon?: \"bold\" | \"italic\";\n  withText?: boolean;\n  text?: string;\n};\n\n/**\n * A two-state button that can be either on or off.\n */\nconst meta: Meta<ToggleArgs> = {\n  title: \"ui/Toggle\",\n  component: \"ui-toggle\",\n  tags: [\"autodocs\"],\n  parameters: {\n    layout: \"centered\",\n  },\n  argTypes: {\n    variant: { control: \"select\", options: [\"default\", \"outline\"] },\n    size: { control: \"select\", options: [\"default\", \"sm\", \"lg\"] },\n    disabled: { control: \"boolean\" },\n    pressed: { control: \"boolean\" },\n    defaultPressed: { control: \"boolean\" },\n    icon: { control: \"select\", options: [\"bold\", \"italic\"] },\n    withText: { control: \"boolean\" },\n    text: { control: \"text\" },\n  },\n  args: {\n    variant: \"default\",\n    size: \"default\",\n    disabled: false,\n    defaultPressed: false,\n    icon: \"bold\",\n    withText: false,\n    text: \"Italic\",\n  },\n  render: (args) => {\n    const iconSvg = args.icon === \"bold\" ? Bold : Italic;\n    return html`\n      <ui-toggle\n        .variant=${args.variant}\n        .size=${args.size}\n        .disabled=${args.disabled || false}\n        .pressed=${args.pressed}\n        .defaultPressed=${args.defaultPressed || false}\n        aria-label=${`Toggle ${args.icon}`}\n      >\n        ${unsafeSVG(iconSvg)} ${args.withText ? args.text : \"\"}\n      </ui-toggle>\n    `;\n  },\n};\n\nexport default meta;\n\ntype Story = StoryObj<ToggleArgs>;\n\n/**\n * The default form of the toggle.\n */\nexport const Default: Story = {};\n\n/**\n * Use the `outline` variant for a distinct outline, emphasizing the boundary\n * of the selection circle for clearer visibility\n */\nexport const Outline: Story = {\n  args: {\n    variant: \"outline\",\n    icon: \"italic\",\n  },\n};\n\n/**\n * Use the text element to add a label to the toggle.\n */\nexport const WithText: Story = {\n  args: {\n    variant: \"outline\",\n    icon: \"italic\",\n    withText: true,\n  },\n};\n\n/**\n * Use the `sm` size for a smaller toggle, 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 toggle, offering better visibility and\n * easier interaction for users.\n */\nexport const Large: Story = {\n  args: {\n    size: \"lg\",\n  },\n};\n\n/**\n * Add the `disabled` prop to prevent interactions with the toggle.\n */\nexport const Disabled: Story = {\n  args: {\n    disabled: true,\n  },\n};\n"}]}