{ "name": "speed-dial", "type": "registry:ui", "files": [ { "path": "ui/speed-dial.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/base/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/base/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/base/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\n extends Omit, \"children\">,\n useRender.ComponentProps<\"div\"> {}\n\ntype RootElement = HTMLDivElement;\ntype ContentElement = HTMLDivElement;\ntype TriggerElement = HTMLButtonElement;\ntype ActionElement = HTMLButtonElement;\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: (key: K, value: StoreState[K]) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext(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(\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;\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;\n triggerRef: React.RefObject;\n isPointerInsideReactTreeRef: React.RefObject;\n hoverCloseTimerRef: React.RefObject;\n side: Side;\n activationMode: ActivationMode;\n delay: number;\n disabled: boolean;\n}\n\nconst SpeedDialContext = React.createContext(\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 render,\n disabled = false,\n className,\n ref,\n ...rootProps\n } = props;\n\n const contentId = React.useId();\n\n const rootRef = React.useRef(null);\n const composedRefs = useComposedRefs(ref, rootRef);\n\n const triggerRef = React.useRef(null);\n\n const nodesRef = React.useRef>(new Map());\n const isPointerInsideReactTreeRef = React.useRef(false);\n const hoverCloseTimerRef = React.useRef(null);\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n open: openProp ?? defaultOpen ?? false,\n }));\n\n const propsRef = useAsRef({\n onOpenChange,\n onPointerDownCapture: onPointerDownCaptureProp,\n });\n\n const store = React.useMemo(() => {\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) => {\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(\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 element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n ref: composedRefs,\n className: cn(\"relative flex flex-col items-end\", className),\n onPointerDownCapture,\n },\n rootProps,\n ),\n render,\n state: {\n slot: \"speed-dial\",\n state: getDataState(open),\n disabled,\n },\n });\n\n return (\n \n \n {element}\n \n \n );\n}\n\ninterface SpeedDialTriggerProps\n extends Omit<\n React.ComponentProps,\n \"onClick\" | \"onMouseEnter\" | \"onMouseLeave\"\n > {\n onClick?: (event: React.MouseEvent) => void;\n onMouseEnter?: (event: React.MouseEvent) => void;\n onMouseLeave?: (event: React.MouseEvent) => void;\n}\n\nfunction SpeedDialTrigger(props: SpeedDialTriggerProps) {\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(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) => {\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) => {\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) => {\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 \n );\n}\n\ninterface SpeedDialItemImplContextValue {\n delay: number;\n open: boolean;\n}\n\nconst SpeedDialItemImplContext =\n React.createContext(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(\n () => ({ delay, open }),\n [delay, open],\n );\n\n return (\n \n {children}\n \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,\n VariantProps {\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 render,\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(null);\n const composedRef: React.RefCallback = useComposedRefs(\n ref,\n contentRef,\n );\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({});\n\n const openRafRef = React.useRef(null);\n const unmountTimerRef = React.useRef(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) => {\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) => {\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(\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 processedChildren = React.useMemo(() => {\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 \n {child}\n \n );\n });\n }, [children, renderState.animating]);\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n id: contentId,\n role: \"menu\",\n \"aria-orientation\": orientation,\n ref: composedRef,\n className: cn(speedDialContentVariants({ side, className })),\n style: contentStyle,\n onMouseEnter,\n onMouseLeave,\n children: processedChildren,\n },\n contentProps,\n ),\n render,\n state: {\n slot: \"speed-dial-content\",\n state: getDataState(renderState.animating),\n orientation,\n side,\n },\n });\n\n const shouldMount = forceMount || renderState.shouldRender;\n\n if (!mounted || !shouldMount) return null;\n\n return element;\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=open]:translate-x-0 data-[state=open]:translate-y-0 data-[state=closed]:opacity-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(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 { render, 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(\n () => ({ actionId, labelId }),\n [actionId, labelId],\n );\n\n const itemStyle = React.useMemo(\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 element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"none\",\n className: cn(speedDialItemVariants({ side, className })),\n style: itemStyle,\n children,\n },\n itemProps,\n ),\n render,\n state: {\n slot: \"speed-dial-item\",\n state: getDataState(open),\n side,\n },\n });\n\n return (\n \n {element}\n \n );\n}\n\ninterface SpeedDialActionProps\n extends Omit, \"onSelect\" | \"onClick\"> {\n onSelect?: (event: Event) => void;\n onClick?: (event: React.MouseEvent) => 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(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) => {\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 \n );\n}\n\nfunction SpeedDialLabel({ render, className, ...props }: DivProps) {\n const { labelId } = useSpeedDialItemContext(LABEL_NAME);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n id: labelId,\n className: cn(\n \"pointer-events-none whitespace-nowrap rounded-md bg-popover px-2 py-1 text-popover-foreground text-sm shadow-md\",\n className,\n ),\n },\n props,\n ),\n render,\n state: {\n slot: \"speed-dial-label\",\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 = React.Ref | 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(ref: PossibleRef, 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(...refs: PossibleRef[]): React.RefCallback {\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(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ], "registryDependencies": [ "button", "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react" ] }