{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rich-text-toolbar",
  "title": "Rich Text Toolbar",
  "description": "Rich Text Toolbar from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/kbd.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/tooltip.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/rich-text-toolbar.tsx",
      "content": "'use client';\n\nimport {\n  AlignCenter,\n  AlignJustify,\n  AlignLeft,\n  AlignRight,\n  Baseline,\n  Bold,\n  Code,\n  Heading1,\n  Heading2,\n  Highlighter,\n  Image as ImageIcon,\n  Italic,\n  Link2,\n  List,\n  ListOrdered,\n  Quote,\n  Redo2,\n  Strikethrough,\n  Underline,\n  Undo2,\n  type LucideIcon,\n} from 'lucide-react';\nimport * as React from 'react';\n\nimport { focusRing } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { Kbd } from '@/components/ui/kbd';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\nexport type RichTextAction =\n  | 'bold'\n  | 'italic'\n  | 'underline'\n  | 'strikethrough'\n  | 'code'\n  | 'heading1'\n  | 'heading2'\n  | 'quote'\n  | 'link'\n  | 'image'\n  | 'bulletList'\n  | 'orderedList'\n  | 'undo'\n  | 'redo'\n  | 'alignLeft'\n  | 'alignCenter'\n  | 'alignRight'\n  | 'alignJustify'\n  | 'textColor'\n  | 'highlight';\n\nconst actionMeta: Record<RichTextAction, { icon: LucideIcon; shortcut?: string }> = {\n  bold: { icon: Bold, shortcut: 'Ctrl B' },\n  italic: { icon: Italic, shortcut: 'Ctrl I' },\n  underline: { icon: Underline, shortcut: 'Ctrl U' },\n  strikethrough: { icon: Strikethrough },\n  code: { icon: Code, shortcut: 'Ctrl E' },\n  heading1: { icon: Heading1 },\n  heading2: { icon: Heading2 },\n  quote: { icon: Quote },\n  link: { icon: Link2, shortcut: 'Ctrl K' },\n  image: { icon: ImageIcon },\n  bulletList: { icon: List },\n  orderedList: { icon: ListOrdered },\n  undo: { icon: Undo2, shortcut: 'Ctrl Z' },\n  redo: { icon: Redo2, shortcut: 'Ctrl Y' },\n  alignLeft: { icon: AlignLeft },\n  alignCenter: { icon: AlignCenter },\n  alignRight: { icon: AlignRight },\n  alignJustify: { icon: AlignJustify },\n  textColor: { icon: Baseline },\n  highlight: { icon: Highlighter },\n};\n\nconst fullActions: RichTextAction[] = [\n  'bold',\n  'italic',\n  'heading1',\n  'heading2',\n  'quote',\n  'link',\n  'image',\n  'bulletList',\n  'orderedList',\n];\nconst messageActions: RichTextAction[] = ['bold', 'italic', 'heading1', 'heading2', 'quote'];\n\n// Figma \"WYSIWYG toolbar\" Type: Default and CV editor share the full set, Message keeps the inline set.\nexport type RichTextToolbarType = 'default' | 'cv-editor' | 'message';\n\nexport interface RichTextToolbarButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {\n  // Accessible name and tooltip text.\n  label: string;\n  icon: React.ReactNode;\n  // Toggle state (`aria-pressed`). Leave undefined for a plain action button.\n  pressed?: boolean;\n  // Shown as a key cap in the tooltip.\n  shortcut?: string;\n}\n\nconst RichTextToolbarButton = React.forwardRef<HTMLButtonElement, RichTextToolbarButtonProps>(\n  ({ className, label, icon, pressed, shortcut, type = 'button', ...props }, ref) => {\n    const button = (\n      <button\n        ref={ref}\n        type={type}\n        aria-label={label}\n        aria-pressed={pressed}\n        aria-keyshortcuts={shortcut?.replace(/ /g, '+')}\n        data-toolbar-item=\"\"\n        tabIndex={-1}\n        className={cn(\n          'inline-flex h-control-sm w-control-sm shrink-0 items-center justify-center rounded-item text-icon-subtle transition-colors duration-150 motion-reduce:transition-none',\n          'hover:bg-muted hover:text-icon-default',\n          'aria-[pressed=true]:bg-background-brand-subtle aria-[pressed=true]:text-icon-brand',\n          'disabled:pointer-events-none disabled:opacity-disabled',\n          focusRing,\n          className,\n        )}\n        {...props}\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"inline-flex h-icon-xs w-icon-xs items-center justify-center [&>svg]:h-full [&>svg]:w-full\"\n        >\n          {icon}\n        </span>\n      </button>\n    );\n    // A disabled button gets no tooltip: the shared trigger would wrap it in an extra tab stop.\n    if (props.disabled) return button;\n    return (\n      <Tooltip>\n        <TooltipTrigger asChild>{button}</TooltipTrigger>\n        <TooltipContent side=\"top\" className=\"flex items-center gap-2\">\n          <span>{label}</span>\n          {shortcut ? <Kbd size=\"sm\">{shortcut}</Kbd> : null}\n        </TooltipContent>\n      </Tooltip>\n    );\n  },\n);\nRichTextToolbarButton.displayName = 'RichTextToolbarButton';\n\nconst RichTextToolbarSeparator = () => <span aria-hidden=\"true\" className=\"mx-1 h-5 w-px shrink-0 bg-border-subtle\" />;\n\nexport interface RichTextToolbarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n  type?: RichTextToolbarType;\n  // Overrides the actions of the chosen `type`.\n  actions?: RichTextAction[];\n  // Actions in groups with a separator between the groups, for example [['undo', 'redo'], ['bold', 'italic'], ['alignLeft', 'alignCenter']]. Wins over `actions`.\n  groups?: RichTextAction[][];\n  // Toggle state per action, for example { bold: true } while the caret is in bold text.\n  pressed?: Partial<Record<RichTextAction, boolean>>;\n  onAction?: (action: RichTextAction) => void;\n  // Content before the buttons, usually a font or style Select.\n  leading?: React.ReactNode;\n  disabled?: boolean;\n  // Failure message from the last action (for example a link that could not be inserted).\n  error?: React.ReactNode;\n}\n\nconst RichTextToolbar = React.forwardRef<HTMLDivElement, RichTextToolbarProps>(\n  (\n    {\n      className,\n      type = 'default',\n      actions,\n      groups,\n      pressed,\n      onAction,\n      leading,\n      disabled = false,\n      error,\n      children,\n      onKeyDown,\n      onFocus,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels();\n    const zero = usePreset() === 'zero';\n    const innerRef = React.useRef<HTMLDivElement | null>(null);\n    const errorId = React.useId();\n    const list = groups ? groups.flat() : (actions ?? (type === 'message' ? messageActions : fullActions));\n    const renderGroup = (group: RichTextAction[]) =>\n      group.map((action) => {\n        const meta = actionMeta[action];\n        const Icon = meta.icon;\n        return (\n          <RichTextToolbarButton\n            key={action}\n            label={l.richTextActions[action]}\n            shortcut={meta.shortcut}\n            icon={<Icon aria-hidden />}\n            pressed={pressed?.[action] ?? false}\n            disabled={disabled}\n            className={zero ? 'text-icon-default' : undefined}\n            onClick={() => onAction?.(action)}\n          />\n        );\n      });\n    const hasError = error != null && error !== false && error !== '';\n\n    const items = () =>\n      Array.from(innerRef.current?.querySelectorAll<HTMLElement>('[data-toolbar-item]') ?? []).filter(\n        (element) => !(element as HTMLButtonElement).disabled,\n      );\n\n    // Roving tabindex: exactly one enabled item is a tab stop (the last focused one, else the first).\n    React.useEffect(() => {\n      const all = Array.from(innerRef.current?.querySelectorAll<HTMLElement>('[data-toolbar-item]') ?? []);\n      const enabled = items();\n      const active = enabled.find((element) => element.tabIndex === 0) ?? enabled[0];\n      all.forEach((element) => {\n        element.tabIndex = element === active ? 0 : -1;\n      });\n    });\n\n    const handleFocus = (event: React.FocusEvent<HTMLDivElement>) => {\n      onFocus?.(event);\n      const target = (event.target as HTMLElement).closest<HTMLElement>('[data-toolbar-item]');\n      if (!target) return;\n      items().forEach((element) => {\n        element.tabIndex = element === target ? 0 : -1;\n      });\n    };\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented) return;\n      const enabled = items();\n      const current = enabled.indexOf(document.activeElement as HTMLElement);\n      if (current === -1) return;\n      let next = -1;\n      if (event.key === 'ArrowRight') next = (current + 1) % enabled.length;\n      else if (event.key === 'ArrowLeft') next = (current - 1 + enabled.length) % enabled.length;\n      else if (event.key === 'Home') next = 0;\n      else if (event.key === 'End') next = enabled.length - 1;\n      if (next === -1) return;\n      event.preventDefault();\n      enabled[next].focus();\n    };\n\n    return (\n      <TooltipProvider delayDuration={300}>\n        <div\n          ref={(node) => {\n            innerRef.current = node;\n            if (typeof ref === 'function') ref(node);\n            else if (ref) ref.current = node;\n          }}\n          role=\"toolbar\"\n          aria-label={l.textFormatting}\n          aria-orientation=\"horizontal\"\n          aria-disabled={disabled || undefined}\n          aria-describedby={hasError ? errorId : undefined}\n          data-invalid={hasError || undefined}\n          data-type={type}\n          className={cn(\n            'flex flex-wrap items-center gap-2 border-b border-solid pb-1',\n            zero ? 'border-border-default bg-background-default px-4 py-2' : 'border-transparent',\n            'data-[invalid=true]:border-border-destructive',\n            className,\n          )}\n          onFocus={handleFocus}\n          onKeyDown={handleKeyDown}\n          {...props}\n        >\n          {leading}\n          <div className=\"flex min-w-0 flex-wrap items-center gap-1\">\n            {groups\n              ? groups.map((group, index) => (\n                  <React.Fragment key={index}>\n                    {index > 0 ? <RichTextToolbarSeparator /> : null}\n                    {renderGroup(group)}\n                  </React.Fragment>\n                ))\n              : renderGroup(list)}\n            {children}\n          </div>\n          {hasError ? (\n            <span id={errorId} role=\"alert\" className=\"ml-auto typography-caption text-text-destructive\">\n              {error}\n            </span>\n          ) : null}\n        </div>\n      </TooltipProvider>\n    );\n  },\n);\nRichTextToolbar.displayName = 'RichTextToolbar';\n\nexport { RichTextToolbar, RichTextToolbarButton, RichTextToolbarSeparator };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}