{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "speed-dial",
  "title": "Speed Dial",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/speed-dial/speed-dial.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  Input,\n  OnDestroy,\n  TemplateRef,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiFabComponent } from '@/ui/fab/fab.component'\nimport { UiPopoverComponent, UiPopoverContentComponent } from '@/ui/popover/popover.component'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\n\nexport interface SpeedDialAction {\n  /** Icon template (React passes a LucideIcon component): `<ng-template #mail><svg ...></svg></ng-template>`. */\n  icon: TemplateRef<unknown>\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 SpeedDialVariant = 'default' | 'secondary' | 'destructive' | 'outline'\nexport type SpeedDialPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-center' | 'inline'\n\nconst sideMap: Record<SpeedDialDirection, 'top' | 'bottom' | 'left' | 'right'> = {\n  up: 'top',\n  down: 'bottom',\n  left: 'left',\n  right: 'right',\n}\n\nconst ACTION_CLASS =\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\n/**\n * Angular port of UIPKGE SpeedDial (React: Popover + Fab). The trigger is a wrapper div\n * around the Fab (Radix `PopoverTrigger asChild`); actions render in a transparent\n * popover panel on the side given by `direction`, 12px from the FAB, each fading in with\n * a 40ms stagger. `trigger=\"hover\"` opens on mouse enter and closes 150ms after the\n * pointer leaves both the FAB and the actions. Same props and defaults as React.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-speed-dial, [ui-speed-dial]',\n  standalone: true,\n  imports: [UiPopoverComponent, UiPopoverContentComponent, UiFabComponent, UiRenderTemplateDirective],\n  // React renders no wrapper around the Popover root.\n  host: { class: 'contents' },\n  template: `\n    <ui-popover #popover [open]=\"open()\" (openChange)=\"handleOpenChange($event)\">\n      <!-- Radix PopoverTrigger asChild: trigger props merged onto the wrapper div. -->\n      <div\n        #triggerEl\n        data-uipkge=\"\"\n        data-slot=\"speed-dial\"\n        aria-haspopup=\"dialog\"\n        [attr.aria-expanded]=\"open()\"\n        [attr.aria-controls]=\"popover.contentId\"\n        [attr.data-state]=\"open() ? 'open' : 'closed'\"\n        [class]=\"rootClass\"\n        (mouseenter)=\"onTriggerEnter()\"\n        (mouseleave)=\"onTriggerLeave()\"\n        (click)=\"popover.toggle()\"\n      >\n        <button\n          ui-fab\n          [variant]=\"variant\"\n          [position]=\"position\"\n          [absolute]=\"absolute\"\n          [disabled]=\"disabled\"\n          [ariaLabel]=\"label || 'Quick actions'\"\n          [attr.aria-expanded]=\"open()\"\n          aria-haspopup=\"menu\"\n          [class]=\"fabClass\"\n          (click)=\"onFabClick($event)\"\n        >\n          @if (icon) {\n            <ng-container [uiRenderTemplate]=\"icon\" />\n          } @else {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"lucide lucide-plus\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M5 12h14\" />\n              <path d=\"M12 5v14\" />\n            </svg>\n          }\n        </button>\n      </div>\n      <ui-popover-content [side]=\"side\" align=\"center\" [sideOffset]=\"12\" [class]=\"contentClass\">\n        <div [class]=\"listClass\" (mouseenter)=\"onContentEnter()\" (mouseleave)=\"onContentLeave()\">\n          @for (action of actions; track $index; let i = $index) {\n            <button\n              type=\"button\"\n              data-slot=\"speed-dial-action\"\n              [disabled]=\"action.disabled\"\n              [attr.aria-label]=\"action.label\"\n              [style.animation-delay.ms]=\"i * 40\"\n              [class]=\"actionClass(action)\"\n              (click)=\"runAction(action)\"\n            >\n              <ng-container [uiRenderTemplate]=\"action.icon\" />\n              <span class=\"sr-only\">{{ action.label }}</span>\n            </button>\n          }\n        </div>\n      </ui-popover-content>\n    </ui-popover>\n  `,\n})\nexport class UiSpeedDialComponent implements AfterViewInit, OnDestroy {\n  @ViewChild('popover', { static: true }) popover!: UiPopoverComponent\n  @ViewChild('triggerEl', { static: true }) triggerEl!: ElementRef<HTMLElement>\n\n  @Input() actions: SpeedDialAction[] = []\n  /** Main FAB icon template (defaults to a plus). */\n  @Input() icon?: TemplateRef<unknown>\n  /** Accessible label for the main FAB. */\n  @Input() label?: string\n  @Input() direction: SpeedDialDirection = 'up'\n  @Input() trigger: SpeedDialTrigger = 'click'\n  /** Close the dial after an action is triggered. */\n  @Input({ transform: booleanAttribute }) closeOnAction = true\n  @Input() variant: SpeedDialVariant = 'default'\n  @Input() position: SpeedDialPosition = 'bottom-right'\n  @Input({ transform: booleanAttribute }) absolute = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input('class') className?: string\n\n  readonly open = signal(false)\n  private hoverTimer: ReturnType<typeof setTimeout> | null = null\n\n  get side() {\n    return sideMap[this.direction]\n  }\n\n  get rootClass(): string {\n    return cn(this.className)\n  }\n\n  get fabClass(): string {\n    return cn(\n      'transition-[color,background-color,box-shadow,transform,scale,translate,rotate] duration-200',\n      this.open() && 'rotate-45',\n    )\n  }\n\n  get contentClass(): string {\n    return cn('w-auto border-0 bg-transparent p-0 shadow-none', this.trigger === 'hover' && 'pointer-events-auto')\n  }\n\n  get listClass(): string {\n    return this.direction === 'up' || this.direction === 'down'\n      ? 'flex flex-col items-center gap-3'\n      : 'flex flex-row items-center gap-3'\n  }\n\n  actionClass(action: SpeedDialAction): string {\n    return cn(ACTION_CLASS, action.className)\n  }\n\n  private clearHoverTimer(): void {\n    if (this.hoverTimer) {\n      clearTimeout(this.hoverTimer)\n      this.hoverTimer = null\n    }\n  }\n\n  private closeLater(): void {\n    this.clearHoverTimer()\n    this.hoverTimer = setTimeout(() => this.open.set(false), 150)\n  }\n\n  onTriggerEnter(): void {\n    if (this.disabled || this.trigger !== 'hover') return\n    this.clearHoverTimer()\n    this.open.set(true)\n  }\n\n  onTriggerLeave(): void {\n    if (this.trigger !== 'hover') return\n    this.closeLater()\n  }\n\n  onContentEnter(): void {\n    if (this.trigger !== 'hover') return\n    this.clearHoverTimer()\n  }\n\n  onContentLeave(): void {\n    if (this.trigger !== 'hover') return\n    this.closeLater()\n  }\n\n  /** In hover mode a click on the FAB must not toggle the popover (React stops propagation). */\n  onFabClick(event: MouseEvent): void {\n    if (this.trigger === 'hover') event.stopPropagation()\n  }\n\n  /** The trigger is a wrapper div, so a disabled Fab alone does not block the popover. */\n  handleOpenChange(next: boolean): void {\n    this.open.set(this.disabled ? false : next)\n  }\n\n  runAction(action: SpeedDialAction): void {\n    if (action.disabled) return\n    action.handler?.()\n    if (this.closeOnAction) this.open.set(false)\n  }\n\n  ngAfterViewInit(): void {\n    // The popover anchors to (and returns focus to) the wrapper div.\n    this.popover.trigger = this.triggerEl.nativeElement\n  }\n\n  ngOnDestroy(): void {\n    this.clearHoverTimer()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/speed-dial/speed-dial.component.ts"
    },
    {
      "path": "packages/registry-angular/components/speed-dial/index.ts",
      "content": "export {\n  UiSpeedDialComponent,\n  type SpeedDialAction,\n  type SpeedDialDirection,\n  type SpeedDialTrigger,\n  type SpeedDialVariant,\n  type SpeedDialPosition,\n} from './speed-dial.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/speed-dial/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/fab.json",
    "https://uipkge.dev/r/angular/popover.json",
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Expanding floating action button (Fab + Popover) that reveals secondary icon actions. Click or hover triggers, four expansion directions, 40ms staggered entrance, configurable FAB icon template, close-on-action, disabled actions.",
  "categories": [
    "control",
    "navigation"
  ]
}