{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "speed-dial",
  "title": "Speed Dial",
  "type": "registry:ui",
  "description": "Expanding floating action button that reveals a list of secondary action buttons. Supports click or hover triggers, four expansion directions (up/down/left/right), staggered entrance animation, a configurable main FAB icon, and close-on-action. Built on the fab and popover primitives.",
  "categories": [
    "control",
    "navigation"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/speed-dial/speed-dial.ts",
      "content": "import { LitElement, css, html, nothing, type TemplateResult } from 'lit'\nimport { Plus, type IconNode } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\nimport { fabVariants, type FabVariants } from '../fab/fab.variants'\n\nexport interface SpeedDialAction {\n  icon: IconNode | string\n  label: string\n  handler?: () => void\n  disabled?: boolean\n  className?: string\n}\n\nexport type SpeedDialDirection = 'up' | 'down' | 'left' | 'right'\nexport type SpeedDialTrigger = 'click' | 'hover'\nexport type SpeedDialPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-center' | 'inline'\n\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\n/**\n * <uip-speed-dial> — Floating Action Button with expandable sub-actions.\n */\nexport class UipSpeedDial extends LitElement {\n  static styles = [tailwind, css`:host { display: inline-block; position: relative; }`]\n\n  static properties = {\n    actions: { type: Array },\n    label: { type: String },\n    direction: { type: String },\n    trigger: { type: String },\n    closeOnAction: { attribute: 'close-on-action', converter: trueByDefault },\n    variant: { type: String },\n    position: { type: String },\n    absolute: { type: Boolean },\n    disabled: { type: Boolean },\n    open: { type: Boolean, reflect: true },\n  }\n\n  actions: SpeedDialAction[] = []\n  label = 'Quick actions'\n  direction: SpeedDialDirection = 'up'\n  trigger: SpeedDialTrigger = 'click'\n  closeOnAction = true\n  variant: NonNullable<FabVariants['variant']> = 'default'\n  position: SpeedDialPosition = 'bottom-right'\n  absolute = false\n  disabled = false\n  open = false\n\n  private hoverTimer?: ReturnType<typeof setTimeout>\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', 'speed-dial')\n  }\n\n  private toggleOpen() {\n    if (this.disabled) return\n    this.open = !this.open\n    this.dispatchEvent(new CustomEvent('open-change', { detail: { open: this.open }, bubbles: true, composed: true }))\n  }\n\n  private onPointerEnter() {\n    if (this.disabled || this.trigger !== 'hover') return\n    if (this.hoverTimer) clearTimeout(this.hoverTimer)\n    this.open = true\n    this.dispatchEvent(new CustomEvent('open-change', { detail: { open: true }, bubbles: true, composed: true }))\n  }\n\n  private onPointerLeave() {\n    if (this.trigger !== 'hover') return\n    if (this.hoverTimer) clearTimeout(this.hoverTimer)\n    this.hoverTimer = setTimeout(() => {\n      this.open = false\n      this.dispatchEvent(new CustomEvent('open-change', { detail: { open: false }, bubbles: true, composed: true }))\n    }, 150)\n  }\n\n  private handleAction(action: SpeedDialAction) {\n    if (action.disabled) return\n    action.handler?.()\n    this.dispatchEvent(new CustomEvent('action', { detail: { action }, bubbles: true, composed: true }))\n    if (this.closeOnAction) {\n      this.open = false\n      this.dispatchEvent(new CustomEvent('open-change', { detail: { open: false }, bubbles: true, composed: true }))\n    }\n  }\n\n  private renderActionIcon(actIcon: IconNode | string): TemplateResult | typeof nothing {\n    if (Array.isArray(actIcon)) {\n      return icon(actIcon as IconNode, 'speed-dial-icon', 'size-5')\n    }\n    if (typeof actIcon === 'string') {\n      return html`<span class=\"size-5 [&_svg]:size-5 inline-flex items-center justify-center\" .innerHTML=${actIcon}></span>`\n    }\n    return nothing\n  }\n\n  render() {\n    const listDirectionClasses: Record<SpeedDialDirection, string> = {\n      up: 'flex-col-reverse bottom-full mb-3 left-1/2 -translate-x-1/2',\n      down: 'flex-col top-full mt-3 left-1/2 -translate-x-1/2',\n      left: 'flex-row-reverse right-full mr-3 top-1/2 -translate-y-1/2',\n      right: 'flex-row left-full ml-3 top-1/2 -translate-y-1/2',\n    }\n\n    return html`\n      <div\n        part=\"wrapper\"\n        class=${cn(\n          'relative inline-flex items-center justify-center',\n          this.position !== 'inline' && fabVariants({ position: this.position as FabVariants['position'] }),\n          this.absolute && this.position !== 'inline' && 'absolute',\n        )}\n        @mouseenter=${this.onPointerEnter}\n        @mouseleave=${this.onPointerLeave}\n      >\n        <!-- Actions container -->\n        ${this.open\n          ? html`\n              <div\n                part=\"actions\"\n                data-slot=\"speed-dial-actions\"\n                class=${cn(\n                  'absolute z-50 flex items-center gap-3 pointer-events-auto',\n                  listDirectionClasses[this.direction],\n                )}\n              >\n                ${this.actions.map((act, i) => html`\n                  <button\n                    type=\"button\"\n                    part=\"action-button\"\n                    data-slot=\"speed-dial-action\"\n                    ?disabled=${act.disabled}\n                    aria-label=${act.label}\n                    style=${`animation-delay: ${i * 40}ms;`}\n                    class=${cn(\n                      \"group/speed-dial-item bg-background text-foreground hover:bg-accent hover:text-accent-foreground motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 focus-visible:ring-ring/50 inline-flex size-12 items-center justify-center rounded-full border shadow-md transition-colors outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5\",\n                      act.className,\n                    )}\n                    @click=${() => this.handleAction(act)}\n                  >\n                    ${this.renderActionIcon(act.icon)}\n                    <span class=\"sr-only\">${act.label}</span>\n                  </button>\n                `)}\n              </div>\n            `\n          : nothing}\n\n        <!-- Main FAB trigger -->\n        <button\n          type=\"button\"\n          part=\"trigger\"\n          data-slot=\"speed-dial-trigger\"\n          aria-label=${this.label}\n          aria-expanded=${this.open ? 'true' : 'false'}\n          aria-haspopup=\"menu\"\n          ?disabled=${this.disabled}\n          class=${cn(\n            fabVariants({ variant: this.variant }),\n            'transition-[transform,background-color,color] duration-200 cursor-pointer',\n            this.open && 'rotate-45',\n          )}\n          @click=${() => this.toggleOpen()}\n        >\n          <slot name=\"icon\">${icon(Plus, 'plus', 'size-6')}</slot>\n        </button>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-speed-dial') || customElements.define('uip-speed-dial', UipSpeedDial)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-speed-dial': UipSpeedDial\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/speed-dial/speed-dial.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/fab.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
