{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "speed-dial",
  "title": "Speed Dial",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/speed-dial/SpeedDial.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Component } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface SpeedDialAction {\n    icon: Component<Record<string, any>>\n    label: string\n    handler?: () => void\n    disabled?: boolean\n    class?: string\n  }\n\n  export type SpeedDialDirection = 'up' | 'down' | 'left' | 'right'\n  export type SpeedDialTrigger = 'click' | 'hover'\n  export type SpeedDialPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-center' | 'inline'\n\n  export interface SpeedDialProps extends HTMLAttributes<HTMLDivElement> {\n    actions: SpeedDialAction[]\n    /** Main FAB icon. Defaults to Plus. */\n    icon?: Component<Record<string, any>>\n    /** Accessible label for the main FAB. */\n    label?: string\n    direction?: SpeedDialDirection\n    trigger?: SpeedDialTrigger\n    /** Close the dial after an action is triggered. */\n    closeOnAction?: boolean\n    variant?: 'default' | 'secondary' | 'destructive' | 'outline'\n    position?: SpeedDialPosition\n    /** Use absolute instead of fixed positioning (for contained dials). */\n    absolute?: boolean\n    disabled?: boolean\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Plus } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    actions,\n    icon,\n    label,\n    direction = 'up',\n    trigger = 'click',\n    closeOnAction = true,\n    variant = 'default',\n    position = 'bottom-right',\n    absolute = false,\n    disabled = false,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: SpeedDialProps = $props()\n\n  let open = $state(false)\n  let rootEl = $state<HTMLDivElement | null>(null)\n  let hoverTimer: ReturnType<typeof setTimeout> | null = null\n\n  $effect(() => {\n    ref = rootEl\n  })\n\n  function clearHoverTimer() {\n    if (hoverTimer) {\n      clearTimeout(hoverTimer)\n      hoverTimer = null\n    }\n  }\n\n  function onTriggerEnter() {\n    if (disabled || trigger !== 'hover') return\n    clearHoverTimer()\n    open = true\n  }\n\n  function scheduleClose() {\n    if (trigger !== 'hover') return\n    clearHoverTimer()\n    hoverTimer = setTimeout(() => (open = false), 150)\n  }\n\n  function onTriggerClick() {\n    if (disabled || trigger !== 'click') return\n    open = !open\n  }\n\n  function runAction(action: SpeedDialAction) {\n    if (action.disabled) return\n    action.handler?.()\n    if (closeOnAction) open = false\n  }\n\n  // Click-trigger parity with popover: dismiss on outside click / Escape.\n  $effect(() => {\n    if (!open || trigger !== 'click') return\n    function onPointerDown(event: PointerEvent) {\n      if (rootEl && !rootEl.contains(event.target as Node)) open = false\n    }\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === 'Escape') open = false\n    }\n    document.addEventListener('pointerdown', onPointerDown)\n    document.addEventListener('keydown', onKeyDown)\n    return () => {\n      document.removeEventListener('pointerdown', onPointerDown)\n      document.removeEventListener('keydown', onKeyDown)\n      clearHoverTimer()\n    }\n  })\n\n  const positionClass = $derived.by(() => {\n    if (position === 'inline') return 'relative inline-flex'\n    const corner = {\n      'bottom-right': 'bottom-6 right-6',\n      'bottom-left': 'bottom-6 left-6',\n      'top-right': 'top-6 right-6',\n      'top-left': 'top-6 left-6',\n      'bottom-center': 'bottom-6 left-1/2 -translate-x-1/2',\n      inline: '',\n    }[position]\n    return cn(absolute ? 'absolute' : 'fixed', corner, 'z-50')\n  })\n\n  const panelClass = $derived.by(() => {\n    switch (direction) {\n      case 'up':\n        return 'bottom-full left-1/2 mb-3 -translate-x-1/2 flex-col'\n      case 'down':\n        return 'top-full left-1/2 mt-3 -translate-x-1/2 flex-col'\n      case 'left':\n        return 'right-full top-1/2 mr-3 -translate-y-1/2 flex-row'\n      case 'right':\n        return 'left-full top-1/2 ml-3 -translate-y-1/2 flex-row'\n    }\n  })\n\n  // FAB trigger styling mirrors fabVariants (default size) 1:1; inlined\n  // because fab has no Svelte port yet — keep in sync if fab changes.\n  const fabVariantClass = {\n    default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n    secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n    destructive: 'bg-destructive text-white hover:bg-destructive/90',\n    outline:\n      'border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n  } as const\n\n  const TriggerIcon = $derived(icon ?? Plus)\n</script>\n\n<!-- Hover handlers only mirror the trigger's hover intent; all actions are real buttons. -->\n<!-- svelte-ignore a11y_no_static_element_interactions -->\n<div\n  bind:this={rootEl}\n  data-uipkge=\"\"\n  data-slot=\"speed-dial\"\n  data-state={open ? 'open' : 'closed'}\n  data-direction={direction}\n  class={cn(positionClass, className)}\n  onmouseenter={onTriggerEnter}\n  onmouseleave={scheduleClose}\n  {...restProps}\n>\n  <button\n    type=\"button\"\n    data-uipkge=\"\"\n    data-slot=\"speed-dial-trigger\"\n    data-variant={variant}\n    disabled={disabled}\n    aria-label={label || 'Quick actions'}\n    aria-expanded={open}\n    aria-haspopup=\"menu\"\n    class={cn(\n      \"inline-flex items-center justify-center gap-2 rounded-full font-medium shadow-lg outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] active:scale-95 touch-manipulation [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6 disabled:pointer-events-none disabled:opacity-50\",\n      fabVariantClass[variant],\n      'size-14',\n      'transition-[color,background-color,box-shadow,transform,scale,translate,rotate] duration-200',\n      open && 'rotate-45',\n    )}\n    onclick={onTriggerClick}\n  >\n    <TriggerIcon />\n  </button>\n\n  {#if open}\n    <!-- svelte-ignore a11y_no_static_element_interactions -->\n    <div\n      data-slot=\"speed-dial-content\"\n      class={cn('absolute z-50 flex items-center gap-3', panelClass)}\n      onmouseenter={clearHoverTimer}\n      onmouseleave={scheduleClose}\n    >\n      {#each actions as action, i (action.label)}\n        {@const ActionIcon = action.icon}\n        <button\n          type=\"button\"\n          data-slot=\"speed-dial-action\"\n          disabled={action.disabled}\n          aria-label={action.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            action.class,\n          )}\n          onclick={() => runAction(action)}\n        >\n          <ActionIcon />\n          <span class=\"sr-only\">{action.label}</span>\n        </button>\n      {/each}\n    </div>\n  {/if}\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/speed-dial/SpeedDial.svelte"
    },
    {
      "path": "packages/registry-svelte/components/speed-dial/index.ts",
      "content": "export {\n  default as SpeedDial,\n  type SpeedDialAction,\n  type SpeedDialDirection,\n  type SpeedDialPosition,\n  type SpeedDialProps,\n  type SpeedDialTrigger,\n} from './SpeedDial.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/speed-dial/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "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. Self-contained (FAB trigger styling and popover positioning hand-rolled).",
  "categories": [
    "control",
    "navigation"
  ]
}