{
  "name": "speed-dial",
  "type": "registry:ui",
  "dependencies": [
    "cn",
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/speed-dial.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\n\nconst ROOT_NAME = \"SpeedDial\";\nconst TRIGGER_NAME = \"SpeedDialTrigger\";\nconst CONTENT_NAME = \"SpeedDialContent\";\nconst ITEM_NAME = \"SpeedDialItem\";\nconst ACTION_NAME = \"SpeedDialAction\";\nconst LABEL_NAME = \"SpeedDialLabel\";\n\nconst ACTION_SELECT = \"speedDial.actionSelect\";\nconst INTERACT_OUTSIDE = \"speedDial.interactOutside\";\nconst EVENT_OPTIONS = { bubbles: true, cancelable: true };\n\nconst DEFAULT_GAP = 8;\nconst DEFAULT_OFFSET = 8;\nconst DEFAULT_ITEM_DELAY = 50;\nconst DEFAULT_HOVER_CLOSE_DELAY = 100;\nconst DEFAULT_ANIMATION_DURATION = 200;\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\ntype ActivationMode = \"click\" | \"hover\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof SpeedDial>;\ntype ContentElement = React.ComponentRef<typeof SpeedDialContent>;\ntype TriggerElement = React.ComponentRef<typeof SpeedDialTrigger>;\ntype ActionElement = React.ComponentRef<typeof SpeedDialAction>;\n\ninterface InteractOutsideEvent extends CustomEvent {\n  detail: {\n    originalEvent: PointerEvent;\n  };\n}\n\nfunction getDataState(open: boolean): string {\n  return open ? \"open\" : \"closed\";\n}\n\nfunction getTransformOrigin(side: Side): string {\n  switch (side) {\n    case \"top\":\n      return \"bottom center\";\n    case \"bottom\":\n      return \"top center\";\n    case \"left\":\n      return \"right center\";\n    case \"right\":\n      return \"left center\";\n  }\n}\n\ninterface StoreState {\n  open: boolean;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n  notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nfunction useStore<T>(\n  selector: (state: StoreState) => T,\n  ogStore?: Store | null,\n): T {\n  const contextStore = React.useContext(StoreContext);\n\n  const store = ogStore ?? contextStore;\n\n  if (!store) {\n    throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface NodeData {\n  id: string;\n  ref: React.RefObject<HTMLElement | null>;\n  disabled: boolean;\n}\n\ninterface SpeedDialContextValue {\n  onNodeRegister: (node: NodeData) => void;\n  onNodeUnregister: (id: string) => void;\n  getNodes: () => NodeData[];\n  contentId: string;\n  rootRef: React.RefObject<RootElement | null>;\n  triggerRef: React.RefObject<TriggerElement | null>;\n  isPointerInsideReactTreeRef: React.RefObject<boolean>;\n  hoverCloseTimerRef: React.RefObject<number | null>;\n  side: Side;\n  activationMode: ActivationMode;\n  delay: number;\n  disabled: boolean;\n}\n\nconst SpeedDialContext = React.createContext<SpeedDialContextValue | null>(\n  null,\n);\n\nfunction useSpeedDialContext(consumerName: string) {\n  const context = React.useContext(SpeedDialContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface SpeedDialProps extends DivProps {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  side?: Side;\n  activationMode?: ActivationMode;\n  delay?: number;\n  disabled?: boolean;\n}\n\nfunction SpeedDial(props: SpeedDialProps) {\n  const {\n    open: openProp,\n    defaultOpen,\n    onOpenChange,\n    onPointerDownCapture: onPointerDownCaptureProp,\n    side = \"top\",\n    activationMode = \"click\",\n    delay = 250,\n    asChild,\n    disabled = false,\n    className,\n    ref,\n    ...rootProps\n  } = props;\n\n  const contentId = React.useId();\n\n  const rootRef = React.useRef<RootElement | null>(null);\n  const composedRefs = useComposedRefs(ref, rootRef);\n\n  const triggerRef = React.useRef<TriggerElement | null>(null);\n\n  const nodesRef = React.useRef<Map<string, NodeData>>(new Map());\n  const isPointerInsideReactTreeRef = React.useRef(false);\n  const hoverCloseTimerRef = React.useRef<number | null>(null);\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<StoreState>(() => ({\n    open: openProp ?? defaultOpen ?? false,\n  }));\n\n  const propsRef = useAsRef({\n    onOpenChange,\n    onPointerDownCapture: onPointerDownCaptureProp,\n  });\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      setState: (key, value) => {\n        if (Object.is(stateRef.current[key], value)) return;\n\n        if (key === \"open\" && typeof value === \"boolean\") {\n          stateRef.current.open = value;\n          propsRef.current.onOpenChange?.(value);\n        } else {\n          stateRef.current[key] = value;\n        }\n\n        store.notify();\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, propsRef]);\n\n  const getNodes = React.useCallback(() => {\n    return Array.from(nodesRef.current.values())\n      .filter((node) => node.ref.current)\n      .sort((a, b) => {\n        const elementA = a.ref.current;\n        const elementB = b.ref.current;\n        if (!elementA || !elementB) return 0;\n        const position = elementA.compareDocumentPosition(elementB);\n        if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n          return -1;\n        }\n        if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n          return 1;\n        }\n        return 0;\n      });\n  }, []);\n\n  const onNodeRegister = React.useCallback((node: NodeData) => {\n    nodesRef.current.set(node.id, node);\n  }, []);\n\n  const onNodeUnregister = React.useCallback((id: string) => {\n    nodesRef.current.delete(id);\n  }, []);\n\n  useIsomorphicLayoutEffect(() => {\n    if (openProp !== undefined) {\n      store.setState(\"open\", openProp);\n    }\n  }, [openProp]);\n\n  const open = useStore((state) => state.open, store);\n\n  const onPointerDownCapture = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      propsRef.current?.onPointerDownCapture?.(event);\n      if (event.defaultPrevented) return;\n\n      const target = event.target as HTMLElement;\n      const nodes = getNodes();\n      const isInteractiveElement = nodes.some((node) =>\n        node.ref.current?.contains(target),\n      );\n\n      isPointerInsideReactTreeRef.current = isInteractiveElement;\n    },\n    [propsRef, getNodes],\n  );\n\n  const contextValue = React.useMemo<SpeedDialContextValue>(\n    () => ({\n      getNodes,\n      onNodeRegister,\n      onNodeUnregister,\n      contentId,\n      rootRef,\n      triggerRef,\n      isPointerInsideReactTreeRef,\n      hoverCloseTimerRef,\n      side,\n      activationMode,\n      delay,\n      disabled,\n    }),\n    [\n      getNodes,\n      onNodeRegister,\n      onNodeUnregister,\n      contentId,\n      side,\n      activationMode,\n      delay,\n      disabled,\n    ],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <SpeedDialContext.Provider value={contextValue}>\n        <RootPrimitive\n          data-slot=\"speed-dial\"\n          data-state={getDataState(open)}\n          data-disabled={disabled}\n          {...rootProps}\n          ref={composedRefs}\n          className={cn(\"relative flex flex-col items-end\", className)}\n          onPointerDownCapture={onPointerDownCapture}\n        />\n      </SpeedDialContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\nfunction SpeedDialTrigger(props: React.ComponentProps<typeof Button>) {\n  const {\n    onClick: onClickProp,\n    onMouseEnter: onMouseEnterProp,\n    onMouseLeave: onMouseLeaveProp,\n    className,\n    disabled: disabledProp,\n    id,\n    ref,\n    ...triggerProps\n  } = props;\n\n  const store = useStoreContext(TRIGGER_NAME);\n\n  const {\n    onNodeRegister,\n    onNodeUnregister,\n    contentId,\n    hoverCloseTimerRef,\n    triggerRef,\n    activationMode,\n    delay,\n    disabled,\n  } = useSpeedDialContext(TRIGGER_NAME);\n\n  const open = useStore((state) => state.open);\n  const isDisabled = disabledProp || disabled;\n\n  const instanceId = React.useId();\n  const triggerId = id ?? instanceId;\n\n  const composedRef = useComposedRefs(ref, triggerRef);\n  const hoverOpenTimerRef = React.useRef<number | null>(null);\n\n  useIsomorphicLayoutEffect(() => {\n    onNodeRegister({\n      id: triggerId,\n      ref: triggerRef,\n      disabled: isDisabled,\n    });\n\n    return () => {\n      onNodeUnregister(triggerId);\n    };\n  }, [onNodeRegister, onNodeUnregister, triggerId, isDisabled]);\n\n  React.useEffect(() => {\n    return () => {\n      if (hoverOpenTimerRef.current) {\n        window.clearTimeout(hoverOpenTimerRef.current);\n      }\n      if (hoverCloseTimerRef.current) {\n        window.clearTimeout(hoverCloseTimerRef.current);\n      }\n    };\n  }, [hoverCloseTimerRef]);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<TriggerElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented) return;\n\n      if (hoverOpenTimerRef.current) {\n        window.clearTimeout(hoverOpenTimerRef.current);\n        hoverOpenTimerRef.current = null;\n      }\n      if (hoverCloseTimerRef.current) {\n        window.clearTimeout(hoverCloseTimerRef.current);\n        hoverCloseTimerRef.current = null;\n      }\n\n      store.setState(\"open\", !open);\n    },\n    [onClickProp, store, open, hoverCloseTimerRef],\n  );\n\n  const onMouseEnter = React.useCallback(\n    (event: React.MouseEvent<TriggerElement>) => {\n      onMouseEnterProp?.(event);\n      if (event.defaultPrevented || activationMode !== \"hover\" || isDisabled)\n        return;\n\n      if (hoverCloseTimerRef.current) {\n        window.clearTimeout(hoverCloseTimerRef.current);\n        hoverCloseTimerRef.current = null;\n      }\n\n      if (hoverOpenTimerRef.current) {\n        window.clearTimeout(hoverOpenTimerRef.current);\n      }\n\n      hoverOpenTimerRef.current = window.setTimeout(() => {\n        store.setState(\"open\", true);\n      }, delay);\n    },\n    [\n      onMouseEnterProp,\n      activationMode,\n      isDisabled,\n      store,\n      delay,\n      hoverCloseTimerRef,\n    ],\n  );\n\n  const onMouseLeave = React.useCallback(\n    (event: React.MouseEvent<TriggerElement>) => {\n      onMouseLeaveProp?.(event);\n      if (event.defaultPrevented || activationMode !== \"hover\" || isDisabled)\n        return;\n\n      if (hoverOpenTimerRef.current) {\n        window.clearTimeout(hoverOpenTimerRef.current);\n        hoverOpenTimerRef.current = null;\n      }\n\n      hoverCloseTimerRef.current = window.setTimeout(() => {\n        store.setState(\"open\", false);\n      }, DEFAULT_HOVER_CLOSE_DELAY);\n    },\n    [onMouseLeaveProp, activationMode, isDisabled, store, hoverCloseTimerRef],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      role=\"button\"\n      id={triggerId}\n      aria-controls={contentId}\n      aria-expanded={open}\n      aria-haspopup=\"menu\"\n      data-slot=\"speed-dial-trigger\"\n      data-state={getDataState(open)}\n      disabled={isDisabled}\n      size=\"icon\"\n      {...triggerProps}\n      ref={composedRef}\n      className={cn(\"size-11 rounded-full\", className)}\n      onClick={onClick}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n    />\n  );\n}\n\ninterface SpeedDialItemImplContextValue {\n  delay: number;\n  open: boolean;\n}\n\nconst SpeedDialItemImplContext =\n  React.createContext<SpeedDialItemImplContextValue | null>(null);\n\nfunction useSpeedDialItemImplContext() {\n  return React.useContext(SpeedDialItemImplContext);\n}\n\ninterface SpeedDialItemImplProps {\n  delay: number;\n  open: boolean;\n  children: React.ReactNode;\n}\n\nconst SpeedDialItemImpl = React.memo(function SpeedDialItemImpl({\n  delay,\n  open,\n  children,\n}: SpeedDialItemImplProps) {\n  const contextValue = React.useMemo<SpeedDialItemImplContextValue>(\n    () => ({ delay, open }),\n    [delay, open],\n  );\n\n  return (\n    <SpeedDialItemImplContext.Provider value={contextValue}>\n      {children}\n    </SpeedDialItemImplContext.Provider>\n  );\n});\n\nconst speedDialContentVariants = cva(\n  \"absolute z-50 flex gap-[var(--speed-dial-gap)] data-[state=closed]:pointer-events-none\",\n  {\n    variants: {\n      side: {\n        top: \"flex-col-reverse items-end\",\n        bottom: \"flex-col items-end\",\n        left: \"flex-row-reverse items-center\",\n        right: \"flex-row items-center\",\n      },\n    },\n    defaultVariants: {\n      side: \"top\",\n    },\n  },\n);\n\ninterface SpeedDialContentProps\n  extends DivProps, VariantProps<typeof speedDialContentVariants> {\n  offset?: number;\n  gap?: number;\n  forceMount?: boolean;\n  onEscapeKeyDown?: (event: KeyboardEvent) => void;\n  onInteractOutside?: (event: InteractOutsideEvent) => void;\n}\n\nfunction SpeedDialContent(props: SpeedDialContentProps) {\n  const {\n    offset = DEFAULT_OFFSET,\n    gap = DEFAULT_GAP,\n    forceMount = false,\n    onEscapeKeyDown,\n    onInteractOutside,\n    onMouseEnter: onMouseEnterProp,\n    onMouseLeave: onMouseLeaveProp,\n    asChild,\n    className,\n    children,\n    style,\n    ref,\n    ...contentProps\n  } = props;\n\n  const store = useStoreContext(CONTENT_NAME);\n  const open = useStore((state) => state.open);\n\n  const {\n    contentId,\n    side,\n    getNodes,\n    rootRef,\n    triggerRef,\n    isPointerInsideReactTreeRef,\n    hoverCloseTimerRef,\n    activationMode,\n  } = useSpeedDialContext(CONTENT_NAME);\n\n  const contentRef = React.useRef<ContentElement | null>(null);\n  const composedRef = useComposedRefs(ref, contentRef);\n\n  const propsRef = useAsRef({\n    onMouseEnter: onMouseEnterProp,\n    onMouseLeave: onMouseLeaveProp,\n    onEscapeKeyDown,\n    onInteractOutside,\n  });\n\n  const orientation =\n    side === \"top\" || side === \"bottom\" ? \"vertical\" : \"horizontal\";\n\n  const transformOrigin = React.useMemo(() => getTransformOrigin(side), [side]);\n\n  const ownerDocument =\n    contentRef.current?.ownerDocument ?? globalThis?.document;\n\n  const mounted = React.useSyncExternalStore(\n    () => () => {},\n    () => true,\n    () => false,\n  );\n\n  const [renderState, setRenderState] = React.useState({\n    shouldRender: false,\n    animating: false,\n  });\n  const [position, setPosition] = React.useState<React.CSSProperties>({});\n\n  const openRafRef = React.useRef<number | null>(null);\n  const unmountTimerRef = React.useRef<number | null>(null);\n\n  React.useEffect(() => {\n    if (open) {\n      if (unmountTimerRef.current) {\n        clearTimeout(unmountTimerRef.current);\n        unmountTimerRef.current = null;\n      }\n\n      setRenderState((prev) => ({ ...prev, shouldRender: true }));\n\n      if (openRafRef.current) cancelAnimationFrame(openRafRef.current);\n      openRafRef.current = requestAnimationFrame(() => {\n        setRenderState((prev) => ({ ...prev, animating: true }));\n        openRafRef.current = null;\n      });\n\n      return () => {\n        if (openRafRef.current) {\n          cancelAnimationFrame(openRafRef.current);\n          openRafRef.current = null;\n        }\n      };\n    } else {\n      setRenderState((prev) => ({ ...prev, animating: false }));\n\n      if (!forceMount) {\n        const childCount = React.Children.count(children);\n        const animationDuration = DEFAULT_ANIMATION_DURATION;\n        const longestDelay = (childCount - 1) * DEFAULT_ITEM_DELAY;\n        const totalDuration = longestDelay + animationDuration;\n\n        unmountTimerRef.current = window.setTimeout(() => {\n          setRenderState((prev) => ({ ...prev, shouldRender: false }));\n        }, totalDuration);\n\n        return () => {\n          if (unmountTimerRef.current) {\n            clearTimeout(unmountTimerRef.current);\n            unmountTimerRef.current = null;\n          }\n        };\n      }\n    }\n  }, [open, forceMount, children]);\n\n  const updatePosition = React.useCallback(() => {\n    if (!triggerRef.current || !open) return;\n\n    const newPosition: React.CSSProperties = {};\n\n    switch (side) {\n      case \"top\":\n        newPosition.bottom = `calc(100% + ${offset}px)`;\n        newPosition.right = \"0\";\n        break;\n      case \"bottom\":\n        newPosition.top = `calc(100% + ${offset}px)`;\n        newPosition.right = \"0\";\n        break;\n      case \"left\":\n        newPosition.right = `calc(100% + ${offset}px)`;\n        newPosition.top = \"0\";\n        break;\n      case \"right\":\n        newPosition.left = `calc(100% + ${offset}px)`;\n        newPosition.top = \"0\";\n        break;\n    }\n\n    setPosition((prev) => {\n      const hasChanged = Object.keys(newPosition).some((key) => {\n        const k = key as keyof typeof newPosition;\n        return prev[k] !== newPosition[k];\n      });\n      return hasChanged ? newPosition : prev;\n    });\n  }, [triggerRef, open, side, offset]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (!open) return;\n    updatePosition();\n  }, [open, updatePosition]);\n\n  React.useEffect(() => {\n    if (!open) return;\n\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        propsRef.current?.onEscapeKeyDown?.(event);\n        if (event.defaultPrevented) return;\n\n        store.setState(\"open\", false);\n      }\n\n      if (event.key === \"Tab\") {\n        const focusableElements = getNodes()\n          .filter((node) => !node.disabled)\n          .map((node) => node.ref.current);\n\n        if (focusableElements.length === 0) return;\n\n        const firstElement = focusableElements[0];\n        const lastElement = focusableElements[focusableElements.length - 1];\n        const activeElement = ownerDocument.activeElement;\n\n        if (event.shiftKey) {\n          if (activeElement === firstElement) {\n            store.setState(\"open\", false);\n          }\n        } else {\n          if (activeElement === lastElement) {\n            store.setState(\"open\", false);\n          }\n        }\n      }\n    };\n\n    ownerDocument.addEventListener(\"keydown\", onKeyDown);\n    return () => ownerDocument.removeEventListener(\"keydown\", onKeyDown);\n  }, [open, propsRef, ownerDocument, store, getNodes]);\n\n  const onClickRef = React.useRef<() => void>(() => {});\n\n  React.useEffect(() => {\n    if (!open) return;\n\n    isPointerInsideReactTreeRef.current = false;\n\n    const onPointerDown = (event: PointerEvent) => {\n      if (event.target && !isPointerInsideReactTreeRef.current) {\n        const target = event.target as HTMLElement;\n        const isOutside = !rootRef.current?.contains(target);\n\n        function onDismiss() {\n          if (isOutside) {\n            const interactEvent = new CustomEvent(INTERACT_OUTSIDE, {\n              ...EVENT_OPTIONS,\n              detail: { originalEvent: event },\n            }) as InteractOutsideEvent;\n\n            propsRef.current?.onInteractOutside?.(interactEvent);\n            if (interactEvent.defaultPrevented) return;\n          }\n\n          store.setState(\"open\", false);\n        }\n\n        if (event.pointerType === \"touch\") {\n          ownerDocument.removeEventListener(\"click\", onClickRef.current);\n          onClickRef.current = onDismiss;\n          ownerDocument.addEventListener(\"click\", onClickRef.current, {\n            once: true,\n          });\n        } else {\n          onDismiss();\n        }\n      } else {\n        ownerDocument.removeEventListener(\"click\", onClickRef.current);\n      }\n      isPointerInsideReactTreeRef.current = false;\n    };\n\n    const timerId = window.setTimeout(() => {\n      ownerDocument.addEventListener(\"pointerdown\", onPointerDown);\n    }, 0);\n\n    return () => {\n      window.clearTimeout(timerId);\n      ownerDocument.removeEventListener(\"pointerdown\", onPointerDown);\n      ownerDocument.removeEventListener(\"click\", onClickRef.current);\n    };\n  }, [\n    open,\n    rootRef,\n    isPointerInsideReactTreeRef,\n    propsRef,\n    ownerDocument,\n    store,\n  ]);\n\n  const onMouseEnter = React.useCallback(\n    (event: React.MouseEvent<ContentElement>) => {\n      propsRef.current?.onMouseEnter?.(event);\n      if (event.defaultPrevented || activationMode !== \"hover\") return;\n\n      if (hoverCloseTimerRef.current) {\n        window.clearTimeout(hoverCloseTimerRef.current);\n        hoverCloseTimerRef.current = null;\n      }\n    },\n    [propsRef, hoverCloseTimerRef, activationMode],\n  );\n\n  const onMouseLeave = React.useCallback(\n    (event: React.MouseEvent<ContentElement>) => {\n      propsRef.current?.onMouseLeave?.(event);\n      if (event.defaultPrevented || activationMode !== \"hover\") return;\n\n      hoverCloseTimerRef.current = window.setTimeout(() => {\n        store.setState(\"open\", false);\n      }, DEFAULT_HOVER_CLOSE_DELAY);\n    },\n    [propsRef, hoverCloseTimerRef, store, activationMode],\n  );\n\n  const contentStyle = React.useMemo<React.CSSProperties>(\n    () => ({\n      \"--speed-dial-gap\": `${gap}px`,\n      \"--speed-dial-offset\": `${offset}px`,\n      \"--speed-dial-transform-origin\": transformOrigin,\n      ...position,\n      ...style,\n    }),\n    [gap, offset, transformOrigin, position, style],\n  );\n\n  const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  const shouldMount = forceMount || renderState.shouldRender;\n\n  if (!mounted || !shouldMount) return null;\n\n  return (\n    <ContentPrimitive\n      id={contentId}\n      role=\"menu\"\n      aria-orientation={orientation}\n      data-slot=\"speed-dial-content\"\n      data-state={getDataState(renderState.animating)}\n      data-orientation={orientation}\n      data-side={side}\n      {...contentProps}\n      ref={composedRef}\n      className={cn(speedDialContentVariants({ side, className }))}\n      style={contentStyle}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n    >\n      {(() => {\n        const totalChildren = React.Children.count(children);\n        return React.Children.map(children, (child, index) => {\n          if (!React.isValidElement(child)) return child;\n\n          const delay = renderState.animating\n            ? index * DEFAULT_ITEM_DELAY\n            : (totalChildren - index - 1) * DEFAULT_ITEM_DELAY;\n\n          return (\n            <SpeedDialItemImpl\n              key={child.key ?? index}\n              delay={delay}\n              open={renderState.animating}\n            >\n              {child}\n            </SpeedDialItemImpl>\n          );\n        });\n      })()}\n    </ContentPrimitive>\n  );\n}\n\nconst speedDialItemVariants = cva(\n  \"flex items-center gap-2 transition-all [transition-delay:var(--speed-dial-delay)] [transition-duration:var(--speed-dial-animation-duration)] data-[state=closed]:opacity-0 data-[state=open]:translate-x-0 data-[state=open]:translate-y-0 data-[state=open]:opacity-100\",\n  {\n    variants: {\n      side: {\n        top: \"justify-end\",\n        bottom: \"justify-end\",\n        left: \"flex-row-reverse justify-start\",\n        right: \"justify-start\",\n      },\n    },\n    compoundVariants: [\n      {\n        side: \"top\",\n        className: \"data-[state=closed]:translate-y-2\",\n      },\n      {\n        side: \"bottom\",\n        className: \"data-[state=closed]:-translate-y-2\",\n      },\n      {\n        side: \"left\",\n        className: \"data-[state=closed]:translate-x-2\",\n      },\n      {\n        side: \"right\",\n        className: \"data-[state=closed]:-translate-x-2\",\n      },\n    ],\n    defaultVariants: {\n      side: \"top\",\n    },\n  },\n);\n\ninterface SpeedDialItemContextValue {\n  actionId: string;\n  labelId: string;\n}\n\nconst SpeedDialItemContext =\n  React.createContext<SpeedDialItemContextValue | null>(null);\n\nfunction useSpeedDialItemContext(consumerName: string) {\n  const context = React.useContext(SpeedDialItemContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n  }\n  return context;\n}\n\nfunction SpeedDialItem(props: DivProps) {\n  const { asChild, className, style, children, ...itemProps } = props;\n\n  const { side } = useSpeedDialContext(ITEM_NAME);\n  const itemImplContext = useSpeedDialItemImplContext();\n  const delay = itemImplContext?.delay ?? 0;\n  const open = itemImplContext?.open ?? false;\n\n  const actionId = React.useId();\n  const labelId = React.useId();\n\n  const contextValue = React.useMemo<SpeedDialItemContextValue>(\n    () => ({ actionId, labelId }),\n    [actionId, labelId],\n  );\n\n  const itemStyle = React.useMemo<React.CSSProperties>(\n    () => ({\n      \"--speed-dial-animation-duration\": `${DEFAULT_ANIMATION_DURATION}ms`,\n      \"--speed-dial-delay\": `${delay}ms`,\n      ...style,\n    }),\n    [delay, style],\n  );\n\n  const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SpeedDialItemContext.Provider value={contextValue}>\n      <ItemPrimitive\n        role=\"none\"\n        data-slot=\"speed-dial-item\"\n        data-state={getDataState(open)}\n        data-side={side}\n        {...itemProps}\n        className={cn(speedDialItemVariants({ side, className }))}\n        style={itemStyle}\n      >\n        {children}\n      </ItemPrimitive>\n    </SpeedDialItemContext.Provider>\n  );\n}\n\ninterface SpeedDialActionProps extends Omit<\n  React.ComponentProps<typeof Button>,\n  \"onSelect\"\n> {\n  onSelect?: (event: Event) => void;\n}\n\nfunction SpeedDialAction(props: SpeedDialActionProps) {\n  const {\n    onSelect,\n    onClick: onClickProp,\n    className,\n    disabled,\n    id,\n    ref,\n    ...actionProps\n  } = props;\n\n  const propsRef = useAsRef({\n    onClick: onClickProp,\n    onSelect,\n  });\n\n  const store = useStoreContext(ACTION_NAME);\n\n  const { onNodeRegister, onNodeUnregister } = useSpeedDialContext(ACTION_NAME);\n  const { actionId: itemActionId, labelId } =\n    useSpeedDialItemContext(ACTION_NAME);\n\n  const actionId = id ?? itemActionId;\n\n  const actionRef = React.useRef<ActionElement | null>(null);\n  const composedRefs = useComposedRefs(ref, actionRef);\n\n  useIsomorphicLayoutEffect(() => {\n    onNodeRegister({\n      id: actionId,\n      ref: actionRef,\n      disabled: !!disabled,\n    });\n\n    return () => {\n      onNodeUnregister(actionId);\n    };\n  }, [onNodeRegister, onNodeUnregister, actionId, disabled]);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<ActionElement>) => {\n      propsRef.current?.onClick?.(event);\n      if (event.defaultPrevented) return;\n\n      const action = actionRef.current;\n      if (!action) return;\n\n      const actionSelectEvent = new CustomEvent(ACTION_SELECT, EVENT_OPTIONS);\n\n      action.addEventListener(\n        ACTION_SELECT,\n        (event) => propsRef.current?.onSelect?.(event),\n        {\n          once: true,\n        },\n      );\n\n      action.dispatchEvent(actionSelectEvent);\n      if (actionSelectEvent.defaultPrevented) return;\n\n      store.setState(\"open\", false);\n    },\n    [propsRef, store],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      role=\"menuitem\"\n      id={actionId}\n      aria-labelledby={labelId}\n      data-slot=\"speed-dial-action\"\n      variant=\"outline\"\n      size=\"icon\"\n      disabled={disabled}\n      ref={composedRefs}\n      {...actionProps}\n      className={cn(\n        \"size-11 shrink-0 rounded-full bg-accent shadow-md\",\n        className,\n      )}\n      onClick={onClick}\n    />\n  );\n}\n\nfunction SpeedDialLabel({ asChild, className, ...props }: DivProps) {\n  const { labelId } = useSpeedDialItemContext(LABEL_NAME);\n\n  const LabelPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <LabelPrimitive\n      id={labelId}\n      data-slot=\"speed-dial-label\"\n      {...props}\n      className={cn(\n        \"pointer-events-none rounded-md bg-popover px-2 py-1 text-sm whitespace-nowrap text-popover-foreground shadow-md\",\n        className,\n      )}\n    />\n  );\n}\n\nexport {\n  SpeedDial,\n  SpeedDialAction,\n  SpeedDialContent,\n  SpeedDialItem,\n  SpeedDialLabel,\n  type SpeedDialProps,\n  SpeedDialTrigger,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}