{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "speed-dial",
  "title": "Speed Dial",
  "description": "A floating button that fans out three to six related actions as a menu, with arrow-key navigation, Escape and outside-press to close.",
  "files": [
    {
      "path": "registry/tealmac/speed-dial.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PlusIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface SpeedDialAction {\n  id: string\n  label: string\n  icon: React.ReactNode\n  onSelect?: () => void\n}\n\nexport interface SpeedDialProps {\n  actions: SpeedDialAction[]\n  /** Name of the main button, for example \"Create\". */\n  label: string\n  /** Icon of the main button. It turns 45 degrees when open. */\n  icon?: React.ReactNode\n  /** The direction the actions fan out. `up` suits a button at the bottom. */\n  direction?: \"up\" | \"down\"\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** `fixed` pins it to the bottom end corner. */\n  position?: \"fixed\" | \"static\"\n  className?: string\n}\n\n/**\n * A floating button that opens a short menu of related actions. The button is\n * a menu button (`aria-haspopup=\"menu\"`); the actions are menu items with\n * arrow-key movement, and Escape closes the menu and returns focus.\n */\nexport function SpeedDial({\n  actions,\n  label,\n  icon = <PlusIcon />,\n  direction = \"up\",\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  position = \"static\",\n  className,\n}: SpeedDialProps) {\n  const [state, setState] = React.useState(defaultOpen)\n  const open = openProp ?? state\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const buttonRef = React.useRef<HTMLButtonElement>(null)\n  const menuId = React.useId()\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      setState(next)\n      onOpenChange?.(next)\n    },\n    [onOpenChange]\n  )\n\n  React.useEffect(() => {\n    if (!open) return\n    const onPointer = (event: PointerEvent) => {\n      if (!rootRef.current?.contains(event.target as Node)) setOpen(false)\n    }\n    document.addEventListener(\"pointerdown\", onPointer)\n    return () => document.removeEventListener(\"pointerdown\", onPointer)\n  }, [open, setOpen])\n\n  const items = () =>\n    Array.from(\n      rootRef.current?.querySelectorAll<HTMLElement>('[role=\"menuitem\"]') ?? []\n    )\n\n  const focusItem = (index: number) => {\n    const list = items()\n    if (!list.length) return\n    list[(index + list.length) % list.length]?.focus()\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    if (event.key === \"Escape\" && open) {\n      event.preventDefault()\n      setOpen(false)\n      buttonRef.current?.focus()\n      return\n    }\n    const forward = direction === \"up\" ? \"ArrowUp\" : \"ArrowDown\"\n    const back = direction === \"up\" ? \"ArrowDown\" : \"ArrowUp\"\n    const at = items().indexOf(document.activeElement as HTMLElement)\n    if (event.key === forward || event.key === back) {\n      event.preventDefault()\n      if (!open) {\n        setOpen(true)\n        requestAnimationFrame(() => focusItem(event.key === forward ? 0 : -1))\n      } else if (at === -1) {\n        focusItem(event.key === forward ? 0 : -1)\n      } else {\n        focusItem(at + (event.key === forward ? 1 : -1))\n      }\n    } else if (open && event.key === \"Home\") {\n      event.preventDefault()\n      focusItem(0)\n    } else if (open && event.key === \"End\") {\n      event.preventDefault()\n      focusItem(-1)\n    }\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"presentation\"\n      data-slot=\"speed-dial\"\n      data-open={open || undefined}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"flex items-center gap-3\",\n        direction === \"up\" ? \"flex-col-reverse\" : \"flex-col\",\n        position === \"fixed\" &&\n          \"fixed end-4 bottom-[max(1rem,env(safe-area-inset-bottom))] z-40\",\n        className\n      )}\n    >\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        aria-label={label}\n        aria-haspopup=\"menu\"\n        aria-expanded={open}\n        aria-controls={menuId}\n        onClick={() => setOpen(!open)}\n        className=\"bg-primary text-primary-foreground focus-visible:ring-ring grid size-14 place-items-center rounded-2xl shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-offset-2 [&_svg]:size-6\"\n      >\n        <span\n          className={cn(\n            \"transition-transform duration-200 motion-reduce:transition-none\",\n            open && \"rotate-45\"\n          )}\n        >\n          {icon}\n        </span>\n      </button>\n      <div\n        id={menuId}\n        role=\"menu\"\n        aria-label={label}\n        hidden={!open}\n        className={cn(\n          \"flex gap-3\",\n          direction === \"up\" ? \"flex-col-reverse\" : \"flex-col\"\n        )}\n      >\n        {actions.map((action, index) => (\n          <button\n            key={action.id}\n            type=\"button\"\n            role=\"menuitem\"\n            tabIndex={-1}\n            onClick={() => {\n              setOpen(false)\n              buttonRef.current?.focus()\n              action.onSelect?.()\n            }}\n            style={{ transitionDelay: `${index * 30}ms` }}\n            className=\"bg-card text-foreground hover:bg-accent focus-visible:ring-ring animate-in fade-in-0 slide-in-from-bottom-2 inline-flex h-11 items-center gap-2 self-end rounded-full border ps-3 pe-4 text-sm font-medium shadow-md outline-none focus-visible:ring-2 motion-reduce:animate-none [&_svg]:size-5\"\n          >\n            {action.icon}\n            {action.label}\n          </button>\n        ))}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}