{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toolbar",
  "type": "registry:component",
  "title": "Toolbar",
  "description": "A strip of tools and actions for an editor, with one tool chosen at a time.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/toolbar.tsx",
      "type": "registry:component",
      "target": "@components/panelui/toolbar.tsx",
      "content": "'use client';\n\n/**\n * Toolbar — a strip of tools and actions for an editor: one tool chosen at a\n * time, and buttons that act at once.\n *\n * ```tsx\n * <Toolbar shortcuts>\n *   <Toolbar.Group defaultValue=\"select\" aria-label=\"Tools\">\n *     <Toolbar.Tool value=\"select\" label=\"Select\" shortcut=\"V\"><PointerIcon /></Toolbar.Tool>\n *     <Toolbar.Tool value=\"pen\" label=\"Pen\" shortcut=\"P\"><PenIcon /></Toolbar.Tool>\n *   </Toolbar.Group>\n *   <Toolbar.Separator />\n *   <Toolbar.Button label=\"Undo\" shortcut=\"⌘Z\"><UndoIcon /></Toolbar.Button>\n * </Toolbar>\n * ```\n *\n * ## One marker, and it travels\n *\n * The chosen tool sits on a filled marker in the primary colour. There is\n * one marker per group and it glides from tool to tool, so changing tools\n * reads as moving a hand along the strip rather than as one button going\n * dark while another lights up.\n *\n * ## Tools and actions are different things\n *\n * `Toolbar.Tool` is a mode: exactly one in a `Toolbar.Group` is on, and it\n * stays on until another is chosen. `Toolbar.Button` is an action, undo or\n * zoom, and leaves nothing behind. Keeping the two apart is what lets the\n * strip say at a glance which mode the editor is in.\n *\n * ## The keyboard\n *\n * Arrow keys move along the strip and Home and End jump to its ends. Give a\n * tool or button a `shortcut` and its tooltip shows the key; set `shortcuts`\n * on the toolbar and a single key press, anywhere on the page outside a text\n * field, chooses that tool or presses that button.\n */\nimport { Toggle } from '@base-ui/react/toggle';\nimport { ToggleGroup } from '@base-ui/react/toggle-group';\nimport { Toolbar as BaseToolbar } from '@base-ui/react/toolbar';\nimport { Tooltip } from '@base-ui/react/tooltip';\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type ToolbarVariant = 'floating' | 'inline';\nexport type ToolbarSize = 'sm' | 'md' | 'lg';\n\nconst toolbarVariants = tv({\n  slots: {\n    root: [\n      'group/toolbar relative inline-flex w-fit items-center gap-0.5 outline-none',\n      'data-[orientation=vertical]:flex-col',\n      'data-disabled:pointer-events-none data-disabled:opacity-50',\n    ],\n    group: 'relative isolate flex items-center gap-0.5 group-data-[orientation=vertical]/toolbar:flex-col',\n    marker: [\n      'pointer-events-none absolute -z-10 rounded-(--toolbar-radius) bg-primary opacity-0 data-ready:opacity-100',\n      'shadow-[0_1px_2px_rgb(0_0_0/0.18),inset_0_1px_0_rgb(255_255_255/0.18)]',\n      'top-(--marker-y) left-(--marker-x) h-(--marker-h) w-(--marker-w)',\n      'data-ready:transition-[left,top,width,height] data-ready:duration-[380ms] data-ready:ease-[cubic-bezier(0.3,1.2,0.4,1)] motion-reduce:transition-none',\n    ],\n    item: [\n      'relative inline-flex h-(--toolbar-item) min-w-(--toolbar-item) shrink-0 cursor-pointer items-center justify-center gap-0.5 rounded-(--toolbar-radius) px-[calc((var(--toolbar-item)-var(--toolbar-icon))/2)]',\n      'text-muted-foreground outline-none select-none transition-[color,background-color,scale] duration-150',\n      'hover:bg-foreground/[0.06] hover:text-foreground active:scale-[0.94]',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n      'data-disabled:pointer-events-none data-disabled:opacity-40 disabled:pointer-events-none disabled:opacity-40',\n      // Menus opened from the strip keep their button marked while open.\n      'data-popup-open:bg-foreground/[0.08] data-popup-open:text-foreground',\n      '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-(--toolbar-icon)',\n      // A button that is only a caret is the options of the tool before it:\n      // a slim chevron against that tool, with no fill of its own.\n      'has-[>[data-slot=toolbar-caret]:only-child]:w-3.5 has-[>[data-slot=toolbar-caret]:only-child]:min-w-0 has-[>[data-slot=toolbar-caret]:only-child]:px-0',\n      'has-[>[data-slot=toolbar-caret]:only-child]:bg-transparent has-[>[data-slot=toolbar-caret]:only-child]:active:scale-100',\n      'has-[>[data-slot=toolbar-caret]:only-child]:data-popup-open:bg-transparent',\n    ],\n    // A chosen tool sits on the marker; the hover fill would hide it.\n    tool: 'data-pressed:bg-transparent data-pressed:text-primary-foreground data-pressed:hover:bg-transparent',\n    separator: [\n      'mx-1 h-[calc(var(--toolbar-item)*0.5)] w-px shrink-0 rounded-full bg-foreground/15',\n      'data-[orientation=horizontal]:mx-0 data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-[calc(var(--toolbar-item)*0.5)]',\n    ],\n    caret: [\n      'pointer-events-none -me-1 flex size-3 shrink-0 items-center justify-center opacity-60 transition-opacity duration-150 [&_svg]:size-3',\n      // On its own it takes its button's colour: muted until the hand is on\n      // it or its menu is open.\n      'only:me-0 only:opacity-100 motion-reduce:transition-none',\n    ],\n    tooltip: [\n      'flex origin-(--transform-origin) items-center gap-2 rounded-lg bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md',\n      'transition-[opacity,scale] duration-150 data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:opacity-0',\n      'data-instant:duration-0 motion-reduce:transition-none',\n    ],\n    kbd: 'rounded bg-background/15 px-1 font-sans text-[11px] tabular-nums text-background/80',\n  },\n  variants: {\n    variant: {\n      floating: {\n        root: [\n          'rounded-[calc(var(--toolbar-radius)+var(--toolbar-pad))] bg-popover p-(--toolbar-pad) text-popover-foreground',\n          'shadow-[0_10px_30px_-12px_rgb(0_0_0/0.3)] ring-1 ring-foreground/[0.06] dark:ring-foreground/10',\n        ],\n      },\n      inline: {},\n    },\n    size: {\n      sm: {\n        root: '[--toolbar-item:1.75rem] [--toolbar-icon:0.875rem] [--toolbar-radius:0.5rem] [--toolbar-pad:0.25rem]',\n      },\n      md: {\n        root: '[--toolbar-item:2.25rem] [--toolbar-icon:1.125rem] [--toolbar-radius:0.75rem] [--toolbar-pad:0.3125rem]',\n      },\n      lg: {\n        root: '[--toolbar-item:2.75rem] [--toolbar-icon:1.25rem] [--toolbar-radius:0.875rem] [--toolbar-pad:0.375rem]',\n      },\n    },\n  },\n  defaultVariants: { variant: 'floating', size: 'md' },\n});\n\ntype Orientation = 'horizontal' | 'vertical';\n\ninterface ToolbarContextValue {\n  orientation: Orientation;\n  /** Lets a tool or button answer a key press. Returns the undo. */\n  bind(key: string, run: () => void): () => void;\n}\n\nconst ToolbarContext = createContext<ToolbarContextValue | null>(null);\n\nfunction useToolbar(part: string) {\n  const context = useContext(ToolbarContext);\n  if (!context) throw new Error(`${part} must be used inside a <Toolbar>.`);\n  return context;\n}\n\n/** Whether a key press belongs to something being typed into. */\nfunction typing(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false;\n  return Boolean(\n    target.closest('input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"], [role=\"textbox\"]')\n  );\n}\n\n/** The key a shortcut is pressed with, when it is one key on its own: \"V\" is, \"⌘Z\" is not. */\nfunction plainKey(shortcut: string) {\n  return [...shortcut].length === 1 && !'⌘⌥⌃⇧'.includes(shortcut) ? shortcut.toLowerCase() : null;\n}\n\nexport interface ToolbarProps extends Omit<BaseToolbar.Root.Props, 'className' | 'style'> {\n  /** `floating` sits on a raised surface of its own; `inline` draws only the buttons. Default `floating`. */\n  variant?: ToolbarVariant;\n  /** The size of every button in it. Default `md`. */\n  size?: ToolbarSize;\n  /**\n   * Answer single-key shortcuts anywhere on the page: a key in a tool's or\n   * button's `shortcut` chooses that tool or presses that button. Keys typed\n   * into a field are left alone. Off by default; turn it on for the one\n   * toolbar that owns the page.\n   */\n  shortcuts?: boolean;\n  /** Which side of a button its tooltip opens on. Default below a row and beside a column. */\n  tooltipSide?: 'top' | 'bottom' | 'left' | 'right';\n  className?: string;\n  style?: CSSProperties;\n}\n\nconst TooltipSideContext = createContext<'top' | 'bottom' | 'left' | 'right'>('bottom');\n\n/** The strip. `orientation=\"vertical\"` stands it up as a rail. */\nfunction ToolbarRoot({\n  variant,\n  size,\n  shortcuts = false,\n  tooltipSide,\n  orientation = 'horizontal',\n  className,\n  children,\n  ...props\n}: ToolbarProps) {\n  const slots = toolbarVariants({ variant, size });\n  const bindings = useRef(new Map<string, () => void>());\n\n  useEffect(() => {\n    if (!shortcuts) return;\n    const onKey = (event: KeyboardEvent) => {\n      if (event.metaKey || event.ctrlKey || event.altKey || event.defaultPrevented || typing(event.target)) return;\n      const run = bindings.current.get(event.key.toLowerCase());\n      if (!run) return;\n      event.preventDefault();\n      run();\n    };\n    document.addEventListener('keydown', onKey);\n    return () => document.removeEventListener('keydown', onKey);\n  }, [shortcuts]);\n\n  const context = useMemo<ToolbarContextValue>(\n    () => ({\n      orientation,\n      bind: (key, run) => {\n        bindings.current.set(key, run);\n        return () => {\n          if (bindings.current.get(key) === run) bindings.current.delete(key);\n        };\n      },\n    }),\n    [orientation]\n  );\n\n  return (\n    <ToolbarContext.Provider value={context}>\n      <TooltipSideContext.Provider value={tooltipSide ?? (orientation === 'vertical' ? 'right' : 'bottom')}>\n        <Tooltip.Provider delay={500} closeDelay={0}>\n          <BaseToolbar.Root\n            data-slot=\"toolbar\"\n            orientation={orientation}\n            className={slots.root({ className })}\n            {...props}\n          >\n            {children}\n          </BaseToolbar.Root>\n        </Tooltip.Provider>\n      </TooltipSideContext.Provider>\n    </ToolbarContext.Provider>\n  );\n}\nToolbarRoot.displayName = 'Toolbar';\n\ninterface GroupContextValue {\n  choose(value: string): void;\n}\n\nconst GroupContext = createContext<GroupContextValue | null>(null);\n\nexport interface ToolbarGroupProps extends Omit<ComponentProps<'div'>, 'defaultValue' | 'onChange' | 'dir'> {\n  /** The chosen tool's `value`, when the caller holds it. */\n  value?: string;\n  /** The tool chosen at first, when the caller does not hold it. */\n  defaultValue?: string;\n  /** Called with the `value` of the tool that was chosen. */\n  onValueChange?: (value: string) => void;\n  /** Turns every tool in the group off. */\n  disabled?: boolean;\n}\n\n/**\n * Tools of which exactly one is on: the select tool, the pen, the shape.\n * Choosing the one already on leaves it on.\n */\nfunction ToolbarGroup({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  disabled,\n  className,\n  children,\n  ...props\n}: ToolbarGroupProps) {\n  const { orientation } = useToolbar('Toolbar.Group');\n  const slots = toolbarVariants();\n  const [inner, setInner] = useState(defaultValue ?? '');\n  const value = valueProp ?? inner;\n  const live = useRef({ onValueChange, controlled: valueProp !== undefined });\n  live.current = { onValueChange, controlled: valueProp !== undefined };\n\n  const choose = useCallback((next: string) => {\n    if (!live.current.controlled) setInner(next);\n    live.current.onValueChange?.(next);\n  }, []);\n\n  // The marker follows the chosen tool, measured where it sits in the group.\n  const group = useRef<HTMLDivElement>(null);\n  const marker = useRef<HTMLSpanElement>(null);\n  useIsomorphicLayoutEffect(() => {\n    const box = group.current;\n    const mark = marker.current;\n    if (!box || !mark) return;\n    const place = () => {\n      const tool = box.querySelector<HTMLElement>(':scope > [data-pressed], :scope > * > [data-pressed]');\n      if (!tool) {\n        mark.removeAttribute('data-ready');\n        return;\n      }\n      mark.style.setProperty('--marker-x', `${tool.offsetLeft}px`);\n      mark.style.setProperty('--marker-y', `${tool.offsetTop}px`);\n      mark.style.setProperty('--marker-w', `${tool.offsetWidth}px`);\n      mark.style.setProperty('--marker-h', `${tool.offsetHeight}px`);\n      // Shown only once placed, so it never slides in from the corner.\n      requestAnimationFrame(() => mark.setAttribute('data-ready', ''));\n    };\n    place();\n    const observer = new ResizeObserver(place);\n    observer.observe(box);\n    return () => observer.disconnect();\n  }, [value]);\n\n  return (\n    <GroupContext.Provider value={useMemo(() => ({ choose }), [choose])}>\n      <ToggleGroup\n        ref={group}\n        data-slot=\"toolbar-group\"\n        value={value ? [value] : []}\n        onValueChange={(next) => {\n          // A tool cannot be switched off by choosing it again.\n          const picked = next.find((each) => each !== value) ?? next[0];\n          if (picked && picked !== value) choose(picked);\n        }}\n        orientation={orientation}\n        disabled={disabled}\n        className={slots.group({ className })}\n        {...props}\n      >\n        <span ref={marker} aria-hidden data-slot=\"toolbar-marker\" className={slots.marker()} />\n        {children}\n      </ToggleGroup>\n    </GroupContext.Provider>\n  );\n}\nToolbarGroup.displayName = 'Toolbar.Group';\n\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n/** Wraps a button in its tooltip, when it has something to say. */\nfunction WithTooltip({ label, shortcut, trigger }: { label?: string; shortcut?: string; trigger: ReactElement }) {\n  const side = useContext(TooltipSideContext);\n  const slots = toolbarVariants();\n  if (!label) return trigger;\n  return (\n    <Tooltip.Root>\n      <Tooltip.Trigger render={trigger} />\n      <Tooltip.Portal>\n        <Tooltip.Positioner side={side} sideOffset={10} className=\"z-50\">\n          <Tooltip.Popup data-slot=\"toolbar-tooltip\" className={slots.tooltip()}>\n            {label}\n            {shortcut ? <kbd className={slots.kbd()}>{shortcut}</kbd> : null}\n          </Tooltip.Popup>\n        </Tooltip.Positioner>\n      </Tooltip.Portal>\n    </Tooltip.Root>\n  );\n}\n\n/** Answers the toolbar's single-key shortcut, when there is one. */\nfunction useShortcut(shortcut: string | undefined, run: () => void) {\n  const { bind } = useToolbar('A toolbar item');\n  const latest = useRef(run);\n  latest.current = run;\n  useEffect(() => {\n    const key = shortcut ? plainKey(shortcut) : null;\n    if (!key) return;\n    return bind(key, () => latest.current());\n  }, [shortcut, bind]);\n}\n\nexport interface ToolbarToolProps extends Omit<ComponentProps<'button'>, 'value' | 'children'> {\n  /** Names the tool in its group's `value`. */\n  value: string;\n  /** What the tool is called: its tooltip, and what a screen reader reads. */\n  label?: string;\n  /** The key that chooses it, shown in the tooltip, such as `V`. */\n  shortcut?: string;\n  /** Usually an icon, and a `Toolbar.Caret` when it has options. */\n  children?: ReactNode;\n}\n\n/** One tool in a `Toolbar.Group`. Choosing it moves the marker onto it. */\nfunction ToolbarTool({ value, label, shortcut, className, children, disabled, ...props }: ToolbarToolProps) {\n  const group = useContext(GroupContext);\n  if (!group) throw new Error('Toolbar.Tool must be used inside a <Toolbar.Group>.');\n  const slots = toolbarVariants();\n  useShortcut(disabled ? undefined : shortcut, () => group.choose(value));\n  return (\n    <WithTooltip\n      label={label}\n      shortcut={shortcut}\n      trigger={\n        <BaseToolbar.Button\n          data-slot=\"toolbar-tool\"\n          aria-label={label}\n          disabled={disabled}\n          className={slots.item({ className: [slots.tool(), className] })}\n          render={<Toggle value={value} />}\n          {...props}\n        >\n          {children}\n        </BaseToolbar.Button>\n      }\n    />\n  );\n}\nToolbarTool.displayName = 'Toolbar.Tool';\n\nexport interface ToolbarButtonProps extends Omit<ComponentProps<'button'>, 'children'> {\n  /** What it does: its tooltip, and what a screen reader reads. */\n  label?: string;\n  /** The key or keys that press it, shown in the tooltip, such as `⌘Z`. A single key is also answered. */\n  shortcut?: string;\n  /**\n   * Another trigger to be the button, such as a `Menu.Trigger`, so a menu\n   * opens from the strip and keeps its place in the arrow-key order.\n   */\n  render?: ReactElement;\n  /** Usually an icon. */\n  children?: ReactNode;\n}\n\n/** An action on the strip: undo, zoom, export. It does not stay on. */\nfunction ToolbarButton({ label, shortcut, render, className, children, ...props }: ToolbarButtonProps) {\n  const slots = toolbarVariants();\n  const self = useRef<HTMLButtonElement>(null);\n  useShortcut(props.disabled ? undefined : shortcut, () => self.current?.click());\n  return (\n    <WithTooltip\n      label={label}\n      shortcut={shortcut}\n      trigger={\n        <BaseToolbar.Button\n          ref={self}\n          data-slot=\"toolbar-button\"\n          aria-label={label}\n          render={render}\n          className={slots.item({ className })}\n          {...props}\n        >\n          {children}\n        </BaseToolbar.Button>\n      }\n    />\n  );\n}\nToolbarButton.displayName = 'Toolbar.Button';\n\nexport interface ToolbarSeparatorProps extends Omit<BaseToolbar.Separator.Props, 'className'> {\n  className?: string;\n}\n\n/** A short line between groups of tools that belong apart. */\nfunction ToolbarSeparator({ className, ...props }: ToolbarSeparatorProps) {\n  const slots = toolbarVariants();\n  return <BaseToolbar.Separator data-slot=\"toolbar-separator\" className={slots.separator({ className })} {...props} />;\n}\nToolbarSeparator.displayName = 'Toolbar.Separator';\n\nexport interface ToolbarCaretProps extends ComponentProps<'span'> {}\n\n/** A small chevron after a tool's icon, saying it opens more choices. */\nfunction ToolbarCaret({ className, children, render, ...props }: ToolbarCaretProps & RenderProp<'span'>) {\n  const slots = toolbarVariants();\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        'aria-hidden': true,\n        'data-slot': 'toolbar-caret',\n        className: slots.caret({ className }),\n        children: (\n          <>\n            {children ?? (\n              <svg\n                viewBox=\"0 0 12 12\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth={1.8}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"M3.5 4.75 6 7.25l2.5-2.5\" />\n              </svg>\n            )}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nToolbarCaret.displayName = 'Toolbar.Caret';\n\nexport const Toolbar = Object.assign(ToolbarRoot, {\n  Group: ToolbarGroup,\n  Tool: ToolbarTool,\n  Button: ToolbarButton,\n  Separator: ToolbarSeparator,\n  Caret: ToolbarCaret,\n});\n\nexport { ToolbarGroup, ToolbarTool, ToolbarButton, ToolbarSeparator, ToolbarCaret, toolbarVariants };\n"
    }
  ]
}
