{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "selectiontoolbar",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "selectiontoolbar.tsx",
      "content": "\"use client\";\n\nimport {\n  Bold,\n  Copy,\n  Italic,\n  Link,\n  Strikethrough,\n  Underline,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport const selectionToolbarStyles = {\n  controlCornerClassName:\n    \"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]\",\n  toolbarShellClassName:\n    \"z-50 flex items-center gap-1 bg-neutral-800 px-2 py-1.5 text-neutral-200 shadow-xl ring-1 ring-black/20\",\n  toolbarButtonClassName: cn(\n    \"group/btn relative inline-flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden text-neutral-200\",\n    \"transition-[transform,background-color,color,box-shadow] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n    \"hover:scale-[1.06] hover:bg-neutral-700 hover:text-white\",\n    \"hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.07),0_8px_18px_-10px_rgba(0,0,0,0.55)]\",\n    \"active:scale-[0.96] active:duration-150 active:ease-[cubic-bezier(0.34,1.2,0.64,1)]\",\n    \"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:scale-100 disabled:hover:bg-transparent disabled:hover:shadow-none\",\n    \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20 focus-visible:ring-offset-1 focus-visible:ring-offset-neutral-800\",\n    \"motion-reduce:transition-colors motion-reduce:active:scale-100 motion-reduce:hover:scale-100\"\n  ),\n  toolbarButtonIconClassName:\n    \"inline-flex transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover/btn:scale-110 group-active/btn:scale-[0.96] motion-reduce:transform-none\",\n} as const;\n\nconst VIEWPORT_MARGIN = 8;\nconst DEFAULT_OFFSET = 10;\nconst ESTIMATED_TOOLBAR_WIDTH = 132;\nconst ESTIMATED_TOOLBAR_HEIGHT = 44;\nconst REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\nexport type SelectionToolbarCommand =\n  | \"bold\"\n  | \"italic\"\n  | \"underline\"\n  | \"strikeThrough\"\n  | \"createLink\"\n  | \"copy\";\n\nexport type SelectionToolbarItem = {\n  command?: SelectionToolbarCommand;\n  disabled?: boolean;\n  icon?: React.ReactNode;\n  id: string;\n  label: string;\n  onAction?: () => boolean | undefined;\n};\n\nexport interface SelectionToolbarProps {\n  \"aria-controls\"?: string;\n  className?: string;\n  children?: React.ReactNode;\n  containerRef: React.RefObject<HTMLElement | null>;\n  disabled?: boolean;\n  items?: SelectionToolbarItem[];\n  offset?: number;\n  onCommand?: (command: SelectionToolbarCommand) => void;\n  portalContainer?: HTMLElement | null;\n  side?: \"auto\" | \"bottom\" | \"top\";\n  zIndex?: number;\n}\n\ntype ToolbarPlacement = {\n  anchorY: number;\n  placement: \"above\" | \"below\";\n  x: number;\n};\n\ntype ActiveState = Record<\n  \"bold\" | \"italic\" | \"strikeThrough\" | \"underline\",\n  boolean\n>;\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction usePrefersReducedMotion() {\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(REDUCED_MOTION_QUERY);\n    const update = () => setPrefersReducedMotion(mediaQuery.matches);\n\n    update();\n    mediaQuery.addEventListener(\"change\", update);\n\n    return () => mediaQuery.removeEventListener(\"change\", update);\n  }, []);\n\n  return prefersReducedMotion;\n}\n\nfunction useObservedContainer(\n  containerRef: React.RefObject<HTMLElement | null>\n) {\n  const [container, setContainer] = useState<HTMLElement | null>(\n    () => containerRef.current\n  );\n\n  useLayoutEffect(() => {\n    let active = true;\n    let frame = 0;\n\n    const sync = () => {\n      if (!active) {\n        return;\n      }\n\n      const next = containerRef.current;\n      setContainer((current) => (current === next ? current : next));\n\n      if (!next) {\n        frame = requestAnimationFrame(sync);\n      }\n    };\n\n    sync();\n\n    return () => {\n      active = false;\n      cancelAnimationFrame(frame);\n    };\n  }, [containerRef]);\n\n  return container;\n}\n\nexport function getDefaultSelectionToolbarItems(): SelectionToolbarItem[] {\n  return [\n    {\n      command: \"bold\",\n      icon: <Bold className=\"h-4 w-4\" strokeWidth={2.5} />,\n      id: \"bold\",\n      label: \"Bold\",\n    },\n    {\n      command: \"italic\",\n      icon: <Italic className=\"h-4 w-4\" strokeWidth={2.5} />,\n      id: \"italic\",\n      label: \"Italic\",\n    },\n    {\n      command: \"underline\",\n      icon: <Underline className=\"h-4 w-4\" strokeWidth={2.5} />,\n      id: \"underline\",\n      label: \"Underline\",\n    },\n  ];\n}\n\nfunction readActiveState(): ActiveState {\n  return {\n    bold: document.queryCommandState(\"bold\"),\n    italic: document.queryCommandState(\"italic\"),\n    strikeThrough: document.queryCommandState(\"strikeThrough\"),\n    underline: document.queryCommandState(\"underline\"),\n  };\n}\n\nfunction getSelectedText() {\n  const selection = window.getSelection();\n  return selection?.toString() ?? \"\";\n}\n\nfunction resolveToolbarPlacement({\n  offset,\n  rect,\n  side,\n  toolbarHeight,\n}: {\n  offset: number;\n  rect: DOMRect;\n  side: NonNullable<SelectionToolbarProps[\"side\"]>;\n  toolbarHeight: number;\n}): Pick<ToolbarPlacement, \"anchorY\" | \"placement\"> {\n  const viewportHeight = window.innerHeight;\n  const spaceAbove = rect.top - VIEWPORT_MARGIN;\n  const spaceBelow = viewportHeight - rect.bottom - VIEWPORT_MARGIN;\n  const requiredHeight = toolbarHeight + offset;\n\n  if (side === \"bottom\") {\n    if (spaceBelow >= requiredHeight || spaceBelow >= spaceAbove) {\n      return { anchorY: rect.bottom, placement: \"below\" };\n    }\n\n    return { anchorY: rect.top, placement: \"above\" };\n  }\n\n  if (side === \"top\") {\n    if (spaceAbove >= requiredHeight || spaceAbove >= spaceBelow) {\n      return { anchorY: rect.top, placement: \"above\" };\n    }\n\n    return { anchorY: rect.bottom, placement: \"below\" };\n  }\n\n  if (spaceAbove >= requiredHeight || spaceAbove >= spaceBelow) {\n    return { anchorY: rect.top, placement: \"above\" };\n  }\n\n  return { anchorY: rect.bottom, placement: \"below\" };\n}\n\nfunction computeToolbarPlacement({\n  offset,\n  rect,\n  side,\n  toolbarHeight,\n  toolbarWidth,\n}: {\n  offset: number;\n  rect: DOMRect;\n  side: NonNullable<SelectionToolbarProps[\"side\"]>;\n  toolbarHeight: number;\n  toolbarWidth: number;\n}): ToolbarPlacement {\n  const viewportWidth = window.innerWidth;\n  const halfWidth = toolbarWidth / 2;\n  const x = clamp(\n    rect.left + rect.width / 2,\n    VIEWPORT_MARGIN + halfWidth,\n    Math.max(\n      VIEWPORT_MARGIN + halfWidth,\n      viewportWidth - halfWidth - VIEWPORT_MARGIN\n    )\n  );\n  const { anchorY, placement } = resolveToolbarPlacement({\n    offset,\n    rect,\n    side,\n    toolbarHeight,\n  });\n\n  return { anchorY, placement, x };\n}\n\nfunction promptForLinkUrl(defaultValue: string) {\n  // Native prompt keeps the zero-dependency link flow for contentEditable surfaces.\n  // biome-ignore lint/suspicious/noAlert: lightweight registry default for inline link editing\n  return window.prompt(\"Enter link URL\", defaultValue);\n}\n\nfunction runNativeCommand(command: SelectionToolbarCommand) {\n  if (command === \"createLink\") {\n    const currentUrl = document.queryCommandValue(\"createLink\");\n    const nextUrl = promptForLinkUrl(currentUrl || \"https://\");\n\n    if (nextUrl === null) {\n      return false;\n    }\n\n    if (nextUrl.trim() === \"\") {\n      document.execCommand(\"unlink\");\n      return true;\n    }\n\n    document.execCommand(\"createLink\", false, nextUrl.trim());\n    return true;\n  }\n\n  if (command === \"copy\") {\n    const selectedText = getSelectedText();\n\n    if (selectedText && navigator.clipboard?.writeText) {\n      navigator.clipboard.writeText(selectedText).catch(() => undefined);\n      return true;\n    }\n\n    return document.execCommand(\"copy\");\n  }\n\n  return document.execCommand(command);\n}\n\nexport function isSelectionToolbarCommandActive(\n  command: SelectionToolbarCommand,\n  active: ActiveState\n) {\n  if (command === \"bold\") {\n    return active.bold;\n  }\n\n  if (command === \"italic\") {\n    return active.italic;\n  }\n\n  if (command === \"underline\") {\n    return active.underline;\n  }\n\n  if (command === \"strikeThrough\") {\n    return active.strikeThrough;\n  }\n\n  return false;\n}\n\nexport function getSelectionToolbarShortcutLabel(\n  command: SelectionToolbarCommand\n) {\n  if (command === \"bold\") {\n    return \"Ctrl+B\";\n  }\n\n  if (command === \"italic\") {\n    return \"Ctrl+I\";\n  }\n\n  if (command === \"underline\") {\n    return \"Ctrl+U\";\n  }\n\n  return undefined;\n}\n\nconst FORMATTING_SHORTCUTS: Record<string, SelectionToolbarCommand> = {\n  b: \"bold\",\n  i: \"italic\",\n  u: \"underline\",\n};\n\nfunction handleFormattingShortcutKeyDown({\n  container,\n  event,\n  runCommand,\n}: {\n  container: HTMLElement;\n  event: KeyboardEvent;\n  runCommand: (command: SelectionToolbarCommand) => boolean;\n}) {\n  const selection = window.getSelection();\n  const range =\n    selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;\n\n  if (\n    !(range && container.contains(range.commonAncestorContainer)) ||\n    selection?.isCollapsed\n  ) {\n    return;\n  }\n\n  if (!(event.metaKey || event.ctrlKey)) {\n    return;\n  }\n\n  const command = FORMATTING_SHORTCUTS[event.key.toLowerCase()];\n\n  if (!command) {\n    return;\n  }\n\n  event.preventDefault();\n  runCommand(command);\n}\n\nexport function useSelectionToolbar({\n  \"aria-controls\": ariaControls,\n  className,\n  children,\n  containerRef,\n  disabled = false,\n  items,\n  offset = DEFAULT_OFFSET,\n  onCommand,\n  portalContainer = null,\n  side = \"auto\",\n  zIndex = 50,\n}: SelectionToolbarProps) {\n  const container = useObservedContainer(containerRef);\n  const prefersReducedMotion = usePrefersReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  const [placement, setPlacement] = useState<ToolbarPlacement | null>(null);\n  const [active, setActive] = useState<ActiveState>({\n    bold: false,\n    italic: false,\n    strikeThrough: false,\n    underline: false,\n  });\n  const [focusedIndex, setFocusedIndex] = useState(0);\n  const toolbarRef = useRef<HTMLElement | null>(null);\n  const toolbarSizeRef = useRef({\n    height: ESTIMATED_TOOLBAR_HEIGHT,\n    width: ESTIMATED_TOOLBAR_WIDTH,\n  });\n  const buttonRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n  const resolvedItems = useMemo(\n    () => items ?? getDefaultSelectionToolbarItems(),\n    [items]\n  );\n  const visible = placement !== null && !disabled;\n\n  const refreshToolbarSize = useCallback(() => {\n    const node = toolbarRef.current;\n\n    if (!node) {\n      return;\n    }\n\n    const rect = node.getBoundingClientRect();\n\n    if (rect.width > 0 && rect.height > 0) {\n      toolbarSizeRef.current = {\n        height: rect.height,\n        width: rect.width,\n      };\n    }\n  }, []);\n\n  const updateFromSelection = useCallback(() => {\n    if (!container || disabled) {\n      setPlacement(null);\n      return;\n    }\n\n    const selection = window.getSelection();\n\n    if (!selection || selection.isCollapsed || selection.rangeCount === 0) {\n      setPlacement(null);\n      return;\n    }\n\n    const range = selection.getRangeAt(0);\n\n    if (!container.contains(range.commonAncestorContainer)) {\n      setPlacement(null);\n      return;\n    }\n\n    const rect = range.getBoundingClientRect();\n\n    if (rect.width === 0 && rect.height === 0) {\n      setPlacement(null);\n      return;\n    }\n\n    setPlacement(\n      computeToolbarPlacement({\n        offset,\n        rect,\n        side,\n        toolbarHeight: toolbarSizeRef.current.height,\n        toolbarWidth: toolbarSizeRef.current.width,\n      })\n    );\n    setActive(readActiveState());\n  }, [container, disabled, offset, side]);\n\n  const runCommand = useCallback(\n    (command: SelectionToolbarCommand) => {\n      if (disabled) {\n        return false;\n      }\n\n      const succeeded = runNativeCommand(command);\n\n      if (succeeded) {\n        onCommand?.(command);\n        setActive(readActiveState());\n        requestAnimationFrame(updateFromSelection);\n      }\n\n      return succeeded;\n    },\n    [disabled, onCommand, updateFromSelection]\n  );\n\n  const handleItemAction = useCallback(\n    (item: SelectionToolbarItem) => {\n      if (item.disabled || disabled) {\n        return;\n      }\n\n      if (item.onAction) {\n        const handled = item.onAction();\n\n        if (handled !== false) {\n          setActive(readActiveState());\n          requestAnimationFrame(updateFromSelection);\n        }\n\n        return;\n      }\n\n      if (item.command) {\n        runCommand(item.command);\n      }\n    },\n    [disabled, runCommand, updateFromSelection]\n  );\n\n  useEffect(() => {\n    setMounted(true);\n\n    return () => {\n      setMounted(false);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!container || disabled) {\n      setPlacement(null);\n      return;\n    }\n\n    const handlePointerDown = (event: PointerEvent) => {\n      const target = event.target as Node | null;\n\n      if (toolbarRef.current?.contains(target)) {\n        return;\n      }\n\n      if (container.contains(target)) {\n        return;\n      }\n\n      setPlacement(null);\n    };\n\n    document.addEventListener(\"selectionchange\", updateFromSelection);\n    document.addEventListener(\"pointerdown\", handlePointerDown);\n    document.addEventListener(\"scroll\", updateFromSelection, true);\n    window.addEventListener(\"resize\", updateFromSelection);\n\n    return () => {\n      document.removeEventListener(\"selectionchange\", updateFromSelection);\n      document.removeEventListener(\"pointerdown\", handlePointerDown);\n      document.removeEventListener(\"scroll\", updateFromSelection, true);\n      window.removeEventListener(\"resize\", updateFromSelection);\n    };\n  }, [container, disabled, updateFromSelection]);\n\n  useLayoutEffect(() => {\n    if (!visible) {\n      return;\n    }\n\n    refreshToolbarSize();\n    updateFromSelection();\n  }, [refreshToolbarSize, updateFromSelection, visible]);\n\n  useEffect(() => {\n    if (!visible || disabled || !container) {\n      return;\n    }\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      handleFormattingShortcutKeyDown({\n        container,\n        event,\n        runCommand,\n      });\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [container, disabled, runCommand, visible]);\n\n  useEffect(() => {\n    if (!visible) {\n      setFocusedIndex(0);\n    }\n  }, [visible]);\n\n  const handleToolbarKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n    const itemCount = resolvedItems.length;\n\n    if (itemCount === 0) {\n      return;\n    }\n\n    const lastIndex = itemCount - 1;\n\n    if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      const nextIndex = focusedIndex >= lastIndex ? 0 : focusedIndex + 1;\n      setFocusedIndex(nextIndex);\n      buttonRefs.current[nextIndex]?.focus();\n      return;\n    }\n\n    if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      const nextIndex = focusedIndex <= 0 ? lastIndex : focusedIndex - 1;\n      setFocusedIndex(nextIndex);\n      buttonRefs.current[nextIndex]?.focus();\n      return;\n    }\n\n    if (event.key === \"Home\") {\n      event.preventDefault();\n      setFocusedIndex(0);\n      buttonRefs.current[0]?.focus();\n      return;\n    }\n\n    if (event.key === \"End\") {\n      event.preventDefault();\n      setFocusedIndex(lastIndex);\n      buttonRefs.current[lastIndex]?.focus();\n    }\n  };\n\n  const transform =\n    placement?.placement === \"below\"\n      ? `translate(-50%, ${offset}px)`\n      : `translate(-50%, calc(-100% - ${offset}px))`;\n\n  const transition = prefersReducedMotion\n    ? \"opacity 120ms linear\"\n    : \"opacity 150ms ease, transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1)\";\n\n  const portalTarget =\n    portalContainer ?? (typeof document === \"undefined\" ? null : document.body);\n\n  const toolbarStyle: React.CSSProperties = {\n    left: placement?.x ?? 0,\n    opacity: visible ? 1 : 0,\n    pointerEvents: visible ? \"auto\" : \"none\",\n    position: \"fixed\",\n    top: placement?.anchorY ?? 0,\n    transform: `${transform} scale(${visible ? 1 : prefersReducedMotion ? 1 : 0.96})`,\n    transition,\n    zIndex,\n  };\n\n  const shellClassName = cn(\n    selectionToolbarStyles.toolbarShellClassName,\n    selectionToolbarStyles.controlCornerClassName,\n    className\n  );\n\n  return {\n    active,\n    ariaControls,\n    buttonRefs,\n    children,\n    disabled,\n    focusedIndex,\n    handleItemAction,\n    handleToolbarKeyDown,\n    mounted,\n    portalTarget,\n    prefersReducedMotion,\n    resolvedItems,\n    setToolbarNode: (node: HTMLElement | null) => {\n      toolbarRef.current = node;\n    },\n    shellClassName,\n    toolbarStyle,\n    visible,\n  };\n}\n\nexport type SelectionToolbarController = ReturnType<typeof useSelectionToolbar>;\n\nexport const SelectionToolbarPresets = {\n  copy: {\n    command: \"copy\" as const,\n    icon: <Copy className=\"h-4 w-4\" strokeWidth={2.5} />,\n    id: \"copy\",\n    label: \"Copy\",\n  },\n  link: {\n    command: \"createLink\" as const,\n    icon: <Link className=\"h-4 w-4\" strokeWidth={2.5} />,\n    id: \"link\",\n    label: \"Link\",\n  },\n  strikeThrough: {\n    command: \"strikeThrough\" as const,\n    icon: <Strikethrough className=\"h-4 w-4\" strokeWidth={2.5} />,\n    id: \"strikeThrough\",\n    label: \"Strikethrough\",\n  },\n};\n\nexport function getSelectionToolbarButtonClassName({\n  active = false,\n  className,\n}: {\n  active?: boolean;\n  className?: string;\n} = {}) {\n  return cn(\n    selectionToolbarStyles.toolbarButtonClassName,\n    selectionToolbarStyles.controlCornerClassName,\n    active &&\n      \"scale-100 bg-neutral-700 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] hover:bg-neutral-600\",\n    className\n  );\n}\n\ntype ToolbarButtonProps = {\n  active?: boolean;\n  children: React.ReactNode;\n  className?: string;\n  disabled?: boolean;\n  label: string;\n  onMouseDown: (event: React.MouseEvent<HTMLButtonElement>) => void;\n  shortcut?: string;\n  tabIndex?: number;\n};\n\nexport const ToolbarButton = React.forwardRef<\n  HTMLButtonElement,\n  ToolbarButtonProps\n>(function ToolbarButton(\n  {\n    active = false,\n    children,\n    className,\n    disabled = false,\n    label,\n    onMouseDown,\n    shortcut,\n    tabIndex = -1,\n  },\n  ref\n) {\n  return (\n    <button\n      aria-keyshortcuts={shortcut}\n      aria-label={shortcut ? `${label} (${shortcut})` : label}\n      aria-pressed={active}\n      className={getSelectionToolbarButtonClassName({ active, className })}\n      disabled={disabled}\n      onMouseDown={onMouseDown}\n      ref={ref}\n      tabIndex={tabIndex}\n      title={shortcut ? `${label} (${shortcut})` : label}\n      type=\"button\"\n    >\n      <span className={selectionToolbarStyles.toolbarButtonIconClassName}>\n        {children}\n      </span>\n    </button>\n  );\n});\n\nToolbarButton.displayName = \"ToolbarButton\";\n\nexport function SelectionToolbar(props: SelectionToolbarProps) {\n  const toolbar = useSelectionToolbar(props);\n\n  if (!(toolbar.mounted && toolbar.portalTarget)) {\n    return null;\n  }\n\n  return createPortal(\n    <div\n      aria-controls={toolbar.ariaControls}\n      aria-hidden={!toolbar.visible}\n      aria-label=\"Text formatting\"\n      className={toolbar.shellClassName}\n      onKeyDown={toolbar.handleToolbarKeyDown}\n      onMouseDown={(event) => event.preventDefault()}\n      ref={toolbar.setToolbarNode}\n      role=\"toolbar\"\n      style={toolbar.toolbarStyle}\n    >\n      {toolbar.resolvedItems.map((item, index) => (\n        <ToolbarButton\n          active={\n            item.command\n              ? isSelectionToolbarCommandActive(item.command, toolbar.active)\n              : false\n          }\n          disabled={toolbar.disabled || item.disabled}\n          key={item.id}\n          label={item.label}\n          onMouseDown={(event) => {\n            event.preventDefault();\n            toolbar.handleItemAction(item);\n          }}\n          ref={(node) => {\n            toolbar.buttonRefs.current[index] = node;\n          }}\n          shortcut={\n            item.command\n              ? getSelectionToolbarShortcutLabel(item.command)\n              : undefined\n          }\n          tabIndex={toolbar.focusedIndex === index ? 0 : -1}\n        >\n          {item.icon}\n        </ToolbarButton>\n      ))}\n      {toolbar.children}\n    </div>,\n    toolbar.portalTarget\n  );\n}\n\nSelectionToolbar.displayName = \"SelectionToolbar\";\n",
      "type": "registry:ui"
    }
  ],
  "title": "Selection Toolbar",
  "description": "Floating inline formatting toolbar for editable text selections with viewport-aware positioning, keyboard shortcuts, theme tokens, and bold, italic, underline, link, copy, and strikethrough actions."
}
