{
  "name": "selection-toolbar",
  "type": "registry:ui",
  "dependencies": [
    "@floating-ui/react-dom",
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/selection-toolbar.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  autoUpdate,\n  flip,\n  hide,\n  limitShift,\n  type Middleware,\n  offset,\n  type Placement,\n  shift,\n  size,\n  useFloating,\n} from \"@floating-ui/react-dom\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\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 = \"SelectionToolbar\";\nconst ITEM_NAME = \"SelectionToolbarItem\";\n\nconst SIDE_OPTIONS = [\"top\", \"right\", \"bottom\", \"left\"] as const;\nconst ALIGN_OPTIONS = [\"start\", \"center\", \"end\"] as const;\n\ntype Side = (typeof SIDE_OPTIONS)[number];\ntype Align = (typeof ALIGN_OPTIONS)[number];\ntype Boundary = Element | null;\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype ItemElement = React.ComponentRef<typeof SelectionToolbarItem>;\n\nfunction getSideAndAlignFromPlacement(placement: Placement) {\n  const [side, align = \"center\"] = placement.split(\"-\");\n  return [side as Side, align as Align] as const;\n}\n\nfunction isNotNull<T>(value: T | null): value is T {\n  return value !== null;\n}\n\ninterface SelectionRect {\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n}\n\ninterface StoreState {\n  open: boolean;\n  selectedText: string;\n  selectionRect: SelectionRect | null;\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  batch: (fn: () => void) => 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 SelectionToolbarProps extends DivProps {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onSelectionChange?: (text: string) => void;\n  container?: HTMLElement | React.RefObject<HTMLElement | null> | null;\n  portalContainer?: Element | DocumentFragment | null;\n  side?: Side;\n  sideOffset?: number;\n  align?: Align;\n  alignOffset?: number;\n  avoidCollisions?: boolean;\n  collisionBoundary?: Boundary | Boundary[];\n  collisionPadding?: number | Partial<Record<Side, number>>;\n  sticky?: \"partial\" | \"always\";\n  hideWhenDetached?: boolean;\n  updatePositionStrategy?: \"optimized\" | \"always\";\n}\n\nfunction SelectionToolbar(props: SelectionToolbarProps) {\n  const {\n    open: openProp,\n    onOpenChange,\n    onSelectionChange,\n    container: containerProp,\n    portalContainer: portalContainerProp,\n    side = \"top\",\n    sideOffset = 8,\n    align = \"center\",\n    alignOffset = 0,\n    avoidCollisions = true,\n    collisionBoundary = [],\n    collisionPadding: collisionPaddingProp = 0,\n    sticky = \"partial\",\n    hideWhenDetached = false,\n    updatePositionStrategy = \"optimized\",\n    className,\n    style,\n    asChild,\n    ...rootProps\n  } = props;\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<StoreState>(() => ({\n    open: openProp ?? false,\n    selectedText: \"\",\n    selectionRect: null,\n  }));\n\n  const propsRef = useAsRef({\n    onOpenChange,\n    onSelectionChange,\n  });\n\n  const getContainer = React.useCallback((): HTMLElement | null => {\n    if (containerProp === undefined || containerProp === null) return null;\n    if (typeof containerProp === \"object\" && \"current\" in containerProp) {\n      return containerProp.current;\n    }\n    return containerProp;\n  }, [containerProp]);\n\n  const store = React.useMemo<Store>(() => {\n    let isBatching = false;\n\n    return {\n      subscribe: (callback) => {\n        listenersRef.current.add(callback);\n        return () => listenersRef.current.delete(callback);\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 if (key === \"selectedText\" && typeof value === \"string\") {\n          stateRef.current.selectedText = value;\n          propsRef.current.onSelectionChange?.(value);\n        } else {\n          stateRef.current[key] = value;\n        }\n\n        if (!isBatching) {\n          store.notify();\n        }\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n      batch: (fn: () => void) => {\n        if (isBatching) {\n          fn();\n          return;\n        }\n        isBatching = true;\n        try {\n          fn();\n        } finally {\n          isBatching = false;\n          store.notify();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, propsRef]);\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  const selectionRect = useStore((state) => state.selectionRect, store);\n\n  const rafRef = React.useRef<number | null>(null);\n\n  const mounted = React.useSyncExternalStore(\n    () => () => {},\n    () => true,\n    () => false,\n  );\n\n  const virtualElement = React.useMemo(() => {\n    if (!selectionRect) return null;\n\n    return {\n      getBoundingClientRect: () => ({\n        x: selectionRect.left,\n        y: selectionRect.top,\n        width: selectionRect.width,\n        height: selectionRect.height,\n        top: selectionRect.top,\n        left: selectionRect.left,\n        right: selectionRect.left + selectionRect.width,\n        bottom: selectionRect.top + selectionRect.height,\n      }),\n    };\n  }, [selectionRect]);\n\n  const transformOrigin = React.useMemo<Middleware>(\n    () => ({\n      name: \"transformOrigin\",\n      fn(data) {\n        const { placement, rects } = data;\n        const [placedSide, placedAlign] =\n          getSideAndAlignFromPlacement(placement);\n        const noArrowAlign = { start: \"0%\", center: \"50%\", end: \"100%\" }[\n          placedAlign\n        ];\n\n        let x = \"\";\n        let y = \"\";\n\n        if (placedSide === \"bottom\") {\n          x = noArrowAlign;\n          y = \"0px\";\n        } else if (placedSide === \"top\") {\n          x = noArrowAlign;\n          y = `${rects.floating.height}px`;\n        } else if (placedSide === \"right\") {\n          x = \"0px\";\n          y = noArrowAlign;\n        } else if (placedSide === \"left\") {\n          x = `${rects.floating.width}px`;\n          y = noArrowAlign;\n        }\n        return { data: { x, y } };\n      },\n    }),\n    [],\n  );\n\n  const desiredPlacement = React.useMemo(\n    () => (side + (align !== \"center\" ? `-${align}` : \"\")) as Placement,\n    [side, align],\n  );\n\n  const collisionPadding = React.useMemo(\n    () =>\n      typeof collisionPaddingProp === \"number\"\n        ? collisionPaddingProp\n        : { top: 0, right: 0, bottom: 0, left: 0, ...collisionPaddingProp },\n    [collisionPaddingProp],\n  );\n\n  const boundary = React.useMemo(\n    () =>\n      Array.isArray(collisionBoundary)\n        ? collisionBoundary\n        : [collisionBoundary],\n    [collisionBoundary],\n  );\n\n  const hasExplicitBoundaries = boundary.length > 0;\n\n  const detectOverflowOptions = React.useMemo(\n    () => ({\n      padding: collisionPadding,\n      boundary: boundary.filter(isNotNull),\n      altBoundary: hasExplicitBoundaries,\n    }),\n    [collisionPadding, boundary, hasExplicitBoundaries],\n  );\n\n  const sizeMiddleware = React.useMemo(\n    () =>\n      size({\n        ...detectOverflowOptions,\n        apply: ({ elements, rects, availableWidth, availableHeight }) => {\n          const { width: anchorWidth, height: anchorHeight } = rects.reference;\n          const contentStyle = elements.floating.style;\n          contentStyle.setProperty(\n            \"--selection-toolbar-available-width\",\n            `${availableWidth}px`,\n          );\n          contentStyle.setProperty(\n            \"--selection-toolbar-available-height\",\n            `${availableHeight}px`,\n          );\n          contentStyle.setProperty(\n            \"--selection-toolbar-anchor-width\",\n            `${anchorWidth}px`,\n          );\n          contentStyle.setProperty(\n            \"--selection-toolbar-anchor-height\",\n            `${anchorHeight}px`,\n          );\n        },\n      }),\n    [detectOverflowOptions],\n  );\n\n  const middleware = React.useMemo<Array<Middleware | false | undefined>>(\n    () => [\n      offset({ mainAxis: sideOffset, alignmentAxis: alignOffset }),\n      avoidCollisions &&\n        shift({\n          mainAxis: true,\n          crossAxis: false,\n          limiter: sticky === \"partial\" ? limitShift() : undefined,\n          ...detectOverflowOptions,\n        }),\n      avoidCollisions && flip({ ...detectOverflowOptions }),\n      sizeMiddleware,\n      transformOrigin,\n      hideWhenDetached &&\n        hide({ strategy: \"referenceHidden\", ...detectOverflowOptions }),\n    ],\n    [\n      sideOffset,\n      alignOffset,\n      avoidCollisions,\n      sticky,\n      detectOverflowOptions,\n      sizeMiddleware,\n      transformOrigin,\n      hideWhenDetached,\n    ],\n  );\n\n  const { refs, floatingStyles, isPositioned, middlewareData } = useFloating({\n    open: open && !!virtualElement,\n    placement: desiredPlacement,\n    strategy: \"fixed\",\n    middleware,\n    whileElementsMounted: (reference, floating, update) => {\n      return autoUpdate(reference, floating, update, {\n        animationFrame: updatePositionStrategy === \"always\",\n      });\n    },\n    elements: {\n      reference: virtualElement,\n    },\n  });\n\n  const closeToolbar = React.useCallback(() => {\n    const state = store.getState();\n    if (state.open || state.selectedText || state.selectionRect) {\n      store.batch(() => {\n        store.setState(\"open\", false);\n        store.setState(\"selectedText\", \"\");\n        store.setState(\"selectionRect\", null);\n      });\n    }\n  }, [store]);\n\n  const updateSelection = React.useCallback(() => {\n    const selection = window.getSelection();\n    if (!selection || selection.rangeCount === 0) {\n      closeToolbar();\n      return;\n    }\n\n    const text = selection.toString().trim();\n    if (!text) {\n      closeToolbar();\n      return;\n    }\n\n    if (containerProp !== undefined) {\n      const resolvedContainer = getContainer();\n      if (!resolvedContainer) return;\n\n      const range = selection.getRangeAt(0);\n      const commonAncestor = range.commonAncestorContainer;\n      const element =\n        commonAncestor.nodeType === Node.ELEMENT_NODE\n          ? (commonAncestor as Element)\n          : commonAncestor.parentElement;\n\n      if (!element || !resolvedContainer.contains(element)) {\n        closeToolbar();\n        return;\n      }\n    }\n\n    const range = selection.getRangeAt(0);\n    const rect = range.getBoundingClientRect();\n\n    const state = store.getState();\n    const hasChanges =\n      state.selectedText !== text ||\n      !state.selectionRect ||\n      state.selectionRect.top !== rect.top ||\n      state.selectionRect.left !== rect.left ||\n      state.selectionRect.width !== rect.width ||\n      state.selectionRect.height !== rect.height ||\n      !state.open;\n\n    if (hasChanges) {\n      store.batch(() => {\n        store.setState(\"selectedText\", text);\n        store.setState(\"selectionRect\", {\n          top: rect.top,\n          left: rect.left,\n          width: rect.width,\n          height: rect.height,\n        });\n        store.setState(\"open\", true);\n      });\n    }\n  }, [containerProp, getContainer, store, closeToolbar]);\n\n  const scheduleUpdate = React.useCallback(() => {\n    if (rafRef.current !== null) return;\n    rafRef.current = requestAnimationFrame(() => {\n      if (store.getState().open) {\n        updateSelection();\n      }\n      rafRef.current = null;\n    });\n  }, [store, updateSelection]);\n\n  React.useEffect(() => {\n    const container = getContainer() ?? document;\n\n    function onMouseUp() {\n      requestAnimationFrame(() => {\n        updateSelection();\n      });\n    }\n\n    function onSelectionChange() {\n      const selection = window.getSelection();\n      if (!selection?.toString().trim()) {\n        closeToolbar();\n      }\n    }\n\n    container.addEventListener(\"mouseup\", onMouseUp);\n    document.addEventListener(\"selectionchange\", onSelectionChange);\n    window.addEventListener(\"scroll\", scheduleUpdate, { passive: true });\n    window.addEventListener(\"resize\", scheduleUpdate, { passive: true });\n\n    return () => {\n      container.removeEventListener(\"mouseup\", onMouseUp);\n      document.removeEventListener(\"selectionchange\", onSelectionChange);\n      window.removeEventListener(\"scroll\", scheduleUpdate);\n      window.removeEventListener(\"resize\", scheduleUpdate);\n      if (rafRef.current !== null) {\n        cancelAnimationFrame(rafRef.current);\n        rafRef.current = null;\n      }\n    };\n  }, [getContainer, updateSelection, closeToolbar, scheduleUpdate]);\n\n  const clearSelection = React.useCallback(() => {\n    const selection = window.getSelection();\n    if (selection) {\n      selection.removeAllRanges();\n    }\n    closeToolbar();\n  }, [closeToolbar]);\n\n  React.useEffect(() => {\n    if (!open) return;\n\n    function onMouseDown(event: MouseEvent) {\n      const target = event.target as Node;\n      if (refs.floating.current && !refs.floating.current.contains(target)) {\n        clearSelection();\n      }\n    }\n\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") {\n        clearSelection();\n      }\n    }\n\n    document.addEventListener(\"mousedown\", onMouseDown);\n    document.addEventListener(\"keydown\", onKeyDown);\n\n    return () => {\n      document.removeEventListener(\"mousedown\", onMouseDown);\n      document.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [open, refs.floating, clearSelection]);\n\n  const portalContainer =\n    portalContainerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!portalContainer || !open) return null;\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      {ReactDOM.createPortal(\n        <div\n          ref={refs.setFloating}\n          style={{\n            ...floatingStyles,\n            transform: isPositioned\n              ? floatingStyles.transform\n              : \"translate(0, -200%)\",\n            minWidth: \"max-content\",\n            ...(middlewareData.hide?.referenceHidden && {\n              visibility: \"hidden\",\n              pointerEvents: \"none\",\n            }),\n          }}\n          data-state={isPositioned ? \"positioned\" : \"measuring\"}\n        >\n          <RootPrimitive\n            role=\"toolbar\"\n            aria-label=\"Text formatting toolbar\"\n            data-slot=\"selection-toolbar\"\n            data-state={open ? \"open\" : \"closed\"}\n            {...rootProps}\n            className={cn(\n              \"flex items-center gap-1 rounded-lg border bg-card px-1.5 py-1.5 shadow-lg outline-none\",\n              isPositioned &&\n                \"animate-in duration-200 fade-in-0 zoom-in-95 [animation-timing-function:cubic-bezier(0.16,1,0.3,1)]\",\n              \"motion-reduce:animate-none motion-reduce:transition-none\",\n              className,\n            )}\n            style={{\n              transformOrigin: middlewareData.transformOrigin\n                ? `${middlewareData.transformOrigin.x} ${middlewareData.transformOrigin.y}`\n                : undefined,\n              animation: !isPositioned ? \"none\" : undefined,\n              ...style,\n            }}\n          />\n        </div>,\n        portalContainer,\n      )}\n    </StoreContext.Provider>\n  );\n}\n\ninterface SelectionToolbarItemProps extends Omit<\n  React.ComponentProps<typeof Button>,\n  \"onSelect\"\n> {\n  onSelect?: (text: string, event: Event) => void;\n}\n\nfunction SelectionToolbarItem(props: SelectionToolbarItemProps) {\n  const {\n    onSelect: onSelectProp,\n    onClick: onClickProp,\n    onPointerDown: onPointerDownProp,\n    onPointerUp: onPointerUpProp,\n    className,\n    ref,\n    ...itemProps\n  } = props;\n\n  const store = useStoreContext(ITEM_NAME);\n\n  const propsRef = useAsRef({\n    onSelect: onSelectProp,\n    onClick: onClickProp,\n    onPointerDown: onPointerDownProp,\n    onPointerUp: onPointerUpProp,\n  });\n\n  const itemRef = React.useRef<ItemElement>(null);\n  const composedRef = useComposedRefs(ref, itemRef);\n  const pointerTypeRef =\n    React.useRef<React.PointerEvent[\"pointerType\"]>(\"touch\");\n\n  const onSelect = React.useCallback(() => {\n    const item = itemRef.current;\n    if (!item) return;\n\n    const text = store.getState().selectedText;\n\n    const selectEvent = new CustomEvent(\"selectiontoolbar.select\", {\n      bubbles: true,\n      cancelable: true,\n      detail: { text },\n    });\n\n    item.addEventListener(\n      \"selectiontoolbar.select\",\n      (event) => propsRef.current.onSelect?.(text, event),\n      {\n        once: true,\n      },\n    );\n\n    item.dispatchEvent(selectEvent);\n  }, [propsRef, store]);\n\n  const onPointerDown = React.useCallback(\n    (event: React.PointerEvent<ItemElement>) => {\n      pointerTypeRef.current = event.pointerType;\n      propsRef.current.onPointerDown?.(event);\n\n      if (event.pointerType === \"mouse\") {\n        event.preventDefault();\n      }\n    },\n    [propsRef],\n  );\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<ItemElement>) => {\n      propsRef.current.onClick?.(event);\n      if (event.defaultPrevented) return;\n\n      if (pointerTypeRef.current !== \"mouse\") {\n        onSelect();\n      }\n    },\n    [propsRef, onSelect],\n  );\n\n  const onPointerUp = React.useCallback(\n    (event: React.PointerEvent<ItemElement>) => {\n      propsRef.current.onPointerUp?.(event);\n      if (event.defaultPrevented) return;\n\n      if (pointerTypeRef.current === \"mouse\") {\n        onSelect();\n      }\n    },\n    [propsRef, onSelect],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"selection-toolbar-item\"\n      variant=\"ghost\"\n      size=\"icon\"\n      {...itemProps}\n      className={cn(\"size-8\", className)}\n      ref={composedRef}\n      onPointerDown={onPointerDown}\n      onClick={onClick}\n      onPointerUp={onPointerUp}\n    />\n  );\n}\n\nfunction SelectionToolbarSeparator(props: DivProps) {\n  const { asChild, className, ...separatorProps } = props;\n\n  const SeparatorPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SeparatorPrimitive\n      role=\"separator\"\n      aria-orientation=\"vertical\"\n      aria-hidden=\"true\"\n      data-slot=\"selection-toolbar-separator\"\n      {...separatorProps}\n      className={cn(\"mx-0.5 h-6 w-px bg-border\", className)}\n    />\n  );\n}\n\nexport {\n  SelectionToolbar,\n  SelectionToolbarItem,\n  type SelectionToolbarProps,\n  SelectionToolbarSeparator,\n  useStore as useSelectionToolbar,\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": ""
    }
  ]
}