{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rich-text-editor",
  "title": "Rich Text Editor",
  "description": "Tiptap rich text editor with formatting toolbar, links, alignment and font size.",
  "dependencies": [
    "@tiptap/extension-link@^3.3.0",
    "@tiptap/extension-placeholder@^3.3.0",
    "@tiptap/extension-text-align@^3.3.0",
    "@tiptap/extension-text-style@^3.3.0",
    "@tiptap/react@^3.3.0",
    "@tiptap/starter-kit@^3.3.0",
    "lucide-react@^1.48.0"
  ],
  "registryDependencies": [
    "https://rdf.dev.360online.com/r/button.json",
    "https://rdf.dev.360online.com/r/field.json",
    "https://rdf.dev.360online.com/r/input.json",
    "https://rdf.dev.360online.com/r/label.json",
    "https://rdf.dev.360online.com/r/popover.json",
    "https://rdf.dev.360online.com/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/rapid/ui/rich-text-editor/extensions/font-size.ts",
      "content": "import { Extension } from '@tiptap/react'\n\nexport const FontSize = Extension.create({\n\tname: 'fontSize',\n\taddOptions() {\n\t\treturn {\n\t\t\ttypes: ['textStyle'],\n\t\t}\n\t},\n\taddGlobalAttributes() {\n\t\treturn [\n\t\t\t{\n\t\t\t\ttypes: ['textStyle'],\n\t\t\t\tattributes: {\n\t\t\t\t\tfontSize: {\n\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\tparseHTML: (element) => element.style.fontSize.replace(/['\"]+/g, ''),\n\t\t\t\t\t\trenderHTML: (attributes) => {\n\t\t\t\t\t\t\tif (!attributes.fontSize) return {}\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tstyle: `font-size: ${attributes.fontSize}`,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t]\n\t},\n})\n",
      "type": "registry:ui",
      "target": "components/ui/rich-text-editor/extensions/font-size.ts"
    },
    {
      "path": "registry/rapid/ui/rich-text-editor/extensions/index.ts",
      "content": "export * from './font-size'\n",
      "type": "registry:ui",
      "target": "components/ui/rich-text-editor/extensions/index.ts"
    },
    {
      "path": "registry/rapid/ui/rich-text-editor/index.ts",
      "content": "export * from './rich-text-editor'\n",
      "type": "registry:ui",
      "target": "components/ui/rich-text-editor/index.ts"
    },
    {
      "path": "registry/rapid/ui/rich-text-editor/rich-text-editor.tsx",
      "content": "'use client'\n\n// NOTE: UI strings are hardcoded in English. This is a template component — wire it to your app's i18n when adopting it.\nimport Link from '@tiptap/extension-link'\nimport Placeholder from '@tiptap/extension-placeholder'\nimport TextAlign from '@tiptap/extension-text-align'\nimport { TextStyleKit } from '@tiptap/extension-text-style'\nimport { type Editor, EditorContent, Extension, useEditor } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport {\n\tAlignCenter,\n\tAlignLeft,\n\tAlignRight,\n\tBold,\n\tItalic,\n\tLink2,\n\tList,\n\tListOrdered,\n\tQuote,\n\tStrikethrough,\n\tType,\n} from 'lucide-react'\nimport * as React from 'react'\nimport { useCallback } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { FieldError } from '@/components/ui/field'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'\nimport { cn } from '@/lib/utils'\nimport { FontSize } from './extensions'\n\nexport type MenuItemType =\n\t| 'bold'\n\t| 'italic'\n\t| 'strike'\n\t| 'bulletList'\n\t| 'orderedList'\n\t| 'blockquote'\n\t| 'alignLeft'\n\t| 'alignCenter'\n\t| 'alignRight'\n\t| 'link'\n\t| 'fontSize'\n\ninterface MenuItemConfig {\n\ttype: MenuItemType\n\ticon: React.ReactNode\n\ttooltip: string\n\tisActive?: (editor: Editor) => boolean\n\tonClick?: (editor: Editor) => void\n\tdropdown?: ({ editor }: { editor: Editor | null }) => React.JSX.Element\n}\n\nconst defaultMenuItems: MenuItemConfig[] = [\n\t{\n\t\ttype: 'bold',\n\t\ticon: <Bold />,\n\t\ttooltip: 'Bold',\n\t\tisActive: (editor) => editor.isActive('bold'),\n\t\tonClick: (editor) => editor.chain().focus().toggleBold().run(),\n\t},\n\t{\n\t\ttype: 'italic',\n\t\ticon: <Italic />,\n\t\ttooltip: 'Italic',\n\t\tisActive: (editor) => editor.isActive('italic'),\n\t\tonClick: (editor) => editor.chain().focus().toggleItalic().run(),\n\t},\n\t{\n\t\ttype: 'strike',\n\t\ticon: <Strikethrough />,\n\t\ttooltip: 'Strikethrough',\n\t\tisActive: (editor) => editor.isActive('strike'),\n\t\tonClick: (editor) => editor.chain().focus().toggleStrike().run(),\n\t},\n\t{\n\t\ttype: 'bulletList',\n\t\ticon: <List />,\n\t\ttooltip: 'Bullet list',\n\t\tisActive: (editor) => editor.isActive('bulletList'),\n\t\tonClick: (editor) => editor.chain().focus().toggleBulletList().run(),\n\t},\n\t{\n\t\ttype: 'orderedList',\n\t\ticon: <ListOrdered />,\n\t\ttooltip: 'Numbered list',\n\t\tisActive: (editor) => editor.isActive('orderedList'),\n\t\tonClick: (editor) => editor.chain().focus().toggleOrderedList().run(),\n\t},\n\t{\n\t\ttype: 'blockquote',\n\t\ticon: <Quote />,\n\t\ttooltip: 'Blockquote',\n\t\tisActive: (editor) => editor.isActive('blockquote'),\n\t\tonClick: (editor) => editor.chain().focus().toggleBlockquote().run(),\n\t},\n\t{\n\t\ttype: 'alignLeft',\n\t\ticon: <AlignLeft />,\n\t\ttooltip: 'Align left',\n\t\tisActive: (editor) => editor.isActive({ textAlign: 'left' }),\n\t\tonClick: (editor) => editor.chain().focus().setTextAlign('left').run(),\n\t},\n\t{\n\t\ttype: 'alignCenter',\n\t\ticon: <AlignCenter />,\n\t\ttooltip: 'Center text',\n\t\tisActive: (editor) => editor.isActive({ textAlign: 'center' }),\n\t\tonClick: (editor) => editor.chain().focus().setTextAlign('center').run(),\n\t},\n\t{\n\t\ttype: 'alignRight',\n\t\ticon: <AlignRight />,\n\t\ttooltip: 'Align right',\n\t\tisActive: (editor) => editor.isActive({ textAlign: 'right' }),\n\t\tonClick: (editor) => editor.chain().focus().setTextAlign('right').run(),\n\t},\n\t{\n\t\ttype: 'fontSize',\n\t\ticon: <Type />,\n\t\ttooltip: 'Font size',\n\t\tdropdown: ({ editor }: { editor: Editor | null }) => <FontSizeDropdown editor={editor} />,\n\t},\n]\n\n// Pressed toolbar buttons keep the secondary icon colour on the lighter accent background\nconst PRESSED_CLASS = 'bg-accent hover:bg-[color-mix(in_oklch,var(--accent),var(--secondary-foreground)_8%)]'\n\nconst FONT_SIZES = ['12px', '14px', '16px', '18px', '20px', '24px', '32px']\n\nconst FontSizeDropdown = ({ editor }: { editor: Editor | null }) => {\n\tconst [isOpen, setIsOpen] = React.useState(false)\n\n\treturn (\n\t\t<Popover open={isOpen} onOpenChange={setIsOpen}>\n\t\t\t<Tooltip>\n\t\t\t\t<TooltipTrigger\n\t\t\t\t\trender={\n\t\t\t\t\t\t<PopoverTrigger\n\t\t\t\t\t\t\t// Keep the click away from the wrapper's click-to-focus handler\n\t\t\t\t\t\t\tonClick={(e) => e.stopPropagation()}\n\t\t\t\t\t\t\trender={<Button type=\"button\" size=\"icon-sm\" variant=\"ghost\" aria-label=\"Font size\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t<Type />\n\t\t\t\t</TooltipTrigger>\n\t\t\t\t<TooltipContent>Font size</TooltipContent>\n\t\t\t</Tooltip>\n\t\t\t<PopoverContent className=\"w-40 gap-0.5 p-1\">\n\t\t\t\t<div className=\"flex flex-col gap-0.5\">\n\t\t\t\t\t{FONT_SIZES.map((size) => (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tkey={size}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tclassName=\"justify-start font-normal\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\teditor?.chain().focus().setMark('textStyle', { fontSize: size }).run()\n\t\t\t\t\t\t\t\tsetIsOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{size}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</PopoverContent>\n\t\t</Popover>\n\t)\n}\n\ninterface RichTextEditorProps {\n\tname?: string\n\tlabel?: string\n\tvalue?: string\n\tonChange: (value: string) => void\n\tplaceholder?: string\n\thtmlPlaceholder?: string\n\texcludeMenuItems?: MenuItemType[]\n\tclassName?: string\n\t/** marks the editor as invalid (destructive border + ring) */\n\t'aria-invalid'?: boolean | 'true' | 'false'\n}\n\n/**\n * The link to store for what the user typed, or null when it is not a web address.\n * `new URL` alone accepts almost anything once https:// is added (browsers encode spaces\n * as %20), so the host also has to look like a domain: no spaces and at least one dot.\n */\nfunction toLinkHref(input: string): string | null {\n\tconst value = input.trim()\n\ttry {\n\t\tconst url = new URL(/^https?:\\/\\//.test(value) ? value : `https://${value}`)\n\t\treturn /^[^\\s%]+\\.[^\\s%.]+$/.test(url.hostname) ? url.toString() : null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nconst LinkDropdown = ({ editor }: { editor: Editor | null }) => {\n\tconst [url, setUrl] = React.useState<string>('')\n\tconst urlId = React.useId()\n\tconst urlErrorId = React.useId()\n\tconst [isUrlInvalid, setIsUrlInvalid] = React.useState(false)\n\tconst [isOpen, setIsOpen] = React.useState(false)\n\tconst [isTextSelected, setIsTextSelected] = React.useState(false)\n\tconst [isLinkActive, setIsLinkActive] = React.useState(false)\n\n\t// Listen to selection changes to update button state\n\tReact.useEffect(() => {\n\t\tif (!editor) return\n\n\t\tconst updateSelectionState = () => {\n\t\t\tsetIsTextSelected(editor.state.selection.content().size > 0)\n\t\t\tsetIsLinkActive(editor.isActive('link'))\n\t\t}\n\n\t\t// Initial state\n\t\tupdateSelectionState()\n\n\t\t// Listen to selection and transaction updates\n\t\teditor.on('selectionUpdate', updateSelectionState)\n\t\teditor.on('transaction', updateSelectionState)\n\n\t\treturn () => {\n\t\t\teditor.off('selectionUpdate', updateSelectionState)\n\t\t\teditor.off('transaction', updateSelectionState)\n\t\t}\n\t}, [editor])\n\n\tReact.useEffect(() => {\n\t\tif (isOpen && editor?.isActive('link')) {\n\t\t\tsetUrl(editor.getAttributes('link').href || '')\n\t\t} else if (!isOpen) {\n\t\t\tsetUrl('')\n\t\t}\n\t}, [isOpen, editor])\n\n\tconst setLink = useCallback(\n\t\t(inputUrl: string) => {\n\t\t\tif (!editor) return\n\n\t\t\tif (!inputUrl.trim()) {\n\t\t\t\teditor.chain().focus().extendMarkRange('link').unsetLink().run()\n\t\t\t\tsetIsOpen(false)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst href = toLinkHref(inputUrl)\n\t\t\tif (!href) {\n\t\t\t\tsetIsUrlInvalid(true)\n\t\t\t\treturn\n\t\t\t}\n\t\t\teditor.chain().focus().extendMarkRange('link').setLink({ href }).run()\n\t\t\tsetUrl('')\n\t\t\tsetIsOpen(false)\n\t\t},\n\t\t[editor],\n\t)\n\n\treturn (\n\t\t<Popover modal open={isOpen} onOpenChange={setIsOpen}>\n\t\t\t<Tooltip>\n\t\t\t\t<TooltipTrigger\n\t\t\t\t\trender={\n\t\t\t\t\t\t<PopoverTrigger\n\t\t\t\t\t\t\t// Keep the click away from the wrapper's click-to-focus handler\n\t\t\t\t\t\t\tonClick={(e) => e.stopPropagation()}\n\t\t\t\t\t\t\trender={\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\t\t\t\tdisabled={!isTextSelected && !isLinkActive}\n\t\t\t\t\t\t\t\t\tvariant={isLinkActive ? 'secondary' : 'ghost'}\n\t\t\t\t\t\t\t\t\tclassName={cn(isLinkActive && PRESSED_CLASS)}\n\t\t\t\t\t\t\t\t\taria-label=\"Insert or edit link\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t<Link2 />\n\t\t\t\t</TooltipTrigger>\n\t\t\t\t<TooltipContent>{isLinkActive ? 'Edit link' : 'Insert link'}</TooltipContent>\n\t\t\t</Tooltip>\n\t\t\t<PopoverContent className=\"w-72\">\n\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t<Label htmlFor={urlId}>URL</Label>\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\tid={urlId}\n\t\t\t\t\t\t\ttype=\"url\"\n\t\t\t\t\t\t\tvalue={url}\n\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\tsetUrl(e.target.value)\n\t\t\t\t\t\t\t\tsetIsUrlInvalid(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t// Enter adds the link, like submitting a form. Not a <form>: the editor usually sits\n\t\t\t\t\t\t\t// inside the app's own form, and Enter would submit that instead.\n\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\tif (e.key !== 'Enter') return\n\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\tsetLink(url)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tplaceholder=\"https://example.com\"\n\t\t\t\t\t\t\taria-invalid={isUrlInvalid || undefined}\n\t\t\t\t\t\t\taria-describedby={isUrlInvalid ? urlErrorId : undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{isUrlInvalid && <FieldError id={urlErrorId}>Enter a valid web address, like example.com</FieldError>}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex justify-end gap-2\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\teditor?.chain().focus().extendMarkRange('link').unsetLink().run()\n\t\t\t\t\t\t\t\tsetIsOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tRemove link\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button variant=\"default\" onClick={() => setLink(url)} type=\"button\" size=\"sm\">\n\t\t\t\t\t\t\tAdd link\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</PopoverContent>\n\t\t</Popover>\n\t)\n}\n\nconst ALL_MENU_ITEMS: MenuItemType[] = [...defaultMenuItems.map((item) => item.type), 'link']\n\nconst MenuBar = ({\n\teditor,\n\tvisibleItems = ALL_MENU_ITEMS,\n}: {\n\teditor: Editor | null\n\tvisibleItems?: MenuItemType[]\n}) => {\n\tif (!editor) return null\n\n\tconst filteredMenuItems = defaultMenuItems.filter((item) => visibleItems.includes(item.type))\n\n\treturn (\n\t\t<TooltipProvider>\n\t\t\t<div className=\"flex flex-wrap items-center gap-0.5 border-b p-1\">\n\t\t\t\t{filteredMenuItems.map((item) =>\n\t\t\t\t\titem.dropdown ? (\n\t\t\t\t\t\t<React.Fragment key={item.type}>{item.dropdown({ editor })}</React.Fragment>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<Tooltip key={item.type}>\n\t\t\t\t\t\t\t<TooltipTrigger\n\t\t\t\t\t\t\t\trender={\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\t\t\t\t\tvariant={item.isActive?.(editor) ? 'secondary' : 'ghost'}\n\t\t\t\t\t\t\t\t\t\tclassName={cn(item.isActive?.(editor) && PRESSED_CLASS)}\n\t\t\t\t\t\t\t\t\t\taria-label={item.tooltip}\n\t\t\t\t\t\t\t\t\t\taria-pressed={item.isActive ? item.isActive(editor) : undefined}\n\t\t\t\t\t\t\t\t\t\tonClick={() => item.onClick?.(editor)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{item.icon}\n\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t<TooltipContent>{item.tooltip}</TooltipContent>\n\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t\t{visibleItems.includes('link') && <LinkDropdown editor={editor} />}\n\t\t\t</div>\n\t\t</TooltipProvider>\n\t)\n}\n\ndeclare module '@tiptap/core' {\n\tinterface Commands<ReturnType> {\n\t\tfontSize: {\n\t\t\tunsetFontSize: () => ReturnType\n\t\t\tsetFontSize: (fontSize: string) => ReturnType\n\t\t}\n\t\tmargin: {\n\t\t\tsetMargin: (margin: string) => ReturnType\n\t\t}\n\t}\n}\n\nexport const RichTextEditor = ({\n\tname,\n\tvalue,\n\tonChange,\n\tplaceholder,\n\thtmlPlaceholder,\n\tlabel,\n\texcludeMenuItems,\n\tclassName,\n\t'aria-invalid': ariaInvalid,\n\t...props\n}: RichTextEditorProps) => {\n\tconst isContentEmpty = !value || value === '<p></p>' || value.trim() === ''\n\tconst skipSync = React.useRef(false)\n\tconst labelId = React.useId()\n\tconst editor = useEditor({\n\t\timmediatelyRender: false,\n\t\t// Expose the editable area as a labelled multi-line textbox for screen readers\n\t\teditorProps: {\n\t\t\tattributes: {\n\t\t\t\trole: 'textbox',\n\t\t\t\t'aria-multiline': 'true',\n\t\t\t\t...(label ? { 'aria-labelledby': labelId } : {}),\n\t\t\t\t...(ariaInvalid ? { 'aria-invalid': String(ariaInvalid) } : {}),\n\t\t\t},\n\t\t},\n\t\textensions: [\n\t\t\tStarterKit.configure({\n\t\t\t\tlink: false, // Disable the default Link extension since we add our own\n\t\t\t\thardBreak: {\n\t\t\t\t\tkeepMarks: true,\n\t\t\t\t\tHTMLAttributes: {\n\t\t\t\t\t\tclass: 'my-hard-break',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t\tTextStyleKit,\n\t\t\tTextAlign.configure({\n\t\t\t\ttypes: ['paragraph'],\n\t\t\t\talignments: ['left', 'center', 'right'],\n\t\t\t}),\n\t\t\tPlaceholder.configure({\n\t\t\t\tplaceholder,\n\t\t\t\tshowOnlyWhenEditable: true,\n\t\t\t\tincludeChildren: false,\n\t\t\t\temptyEditorClass: 'is-editor-empty',\n\t\t\t\temptyNodeClass: 'is-node-empty',\n\t\t\t}),\n\t\t\tLink.configure({\n\t\t\t\topenOnClick: false,\n\t\t\t\tHTMLAttributes: {\n\t\t\t\t\tclass: 'text-primary underline underline-offset-4 hover:decoration-2',\n\t\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\t\ttarget: '_blank',\n\t\t\t\t},\n\t\t\t}),\n\t\t\tFontSize,\n\t\t\tExtension.create({\n\t\t\t\tname: 'margin',\n\t\t\t\taddGlobalAttributes() {\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttypes: ['paragraph'],\n\t\t\t\t\t\t\tattributes: {\n\t\t\t\t\t\t\t\tmargin: {\n\t\t\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t\t\t\tparseHTML: (element) => element.style.margin,\n\t\t\t\t\t\t\t\t\trenderHTML: (attributes) => {\n\t\t\t\t\t\t\t\t\t\tif (!attributes.margin) return {}\n\t\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\t\tstyle: `margin: ${attributes.margin}`,\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\taddCommands() {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsetMargin:\n\t\t\t\t\t\t\t(margin: string) =>\n\t\t\t\t\t\t\t({ editor }) => {\n\t\t\t\t\t\t\t\treturn editor.chain().focus().setMargin(margin).run()\n\t\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\tcontent: value,\n\t\tonUpdate: ({ editor }) => {\n\t\t\tskipSync.current = true\n\t\t\tonChange(editor.getHTML())\n\t\t},\n\t\t...props,\n\t})\n\n\t// Sync external value changes (e.g. from form.reset()) into the editor\n\tReact.useEffect(() => {\n\t\tif (skipSync.current) {\n\t\t\tskipSync.current = false\n\t\t\treturn\n\t\t}\n\t\tif (editor && !editor.isDestroyed && value !== undefined) {\n\t\t\teditor.commands.setContent(value, { emitUpdate: false })\n\t\t}\n\t}, [editor, value])\n\n\treturn (\n\t\t<div className={cn('flex flex-col gap-2', className)} data-slot=\"rich-text-editor\" data-field={name}>\n\t\t\t{label && (\n\t\t\t\t<Label id={labelId} onClick={() => editor?.commands.focus()}>\n\t\t\t\t\t{label}\n\t\t\t\t</Label>\n\t\t\t)}\n\t\t\t{/* biome-ignore lint/a11y/noStaticElementInteractions: click-to-focus wrapper for the editor */}\n\t\t\t<div\n\t\t\t\taria-invalid={ariaInvalid}\n\t\t\t\tclassName=\"w-full cursor-text overflow-hidden rounded-lg border border-border text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\"\n\t\t\t\t// Only react to the wrapper itself, not to events bubbling from the toolbar or the portalled popovers\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\tif (e.currentTarget.contains(e.target as Node)) editor?.commands.focus()\n\t\t\t\t}}\n\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\tif (e.key === 'Enter' && e.target === e.currentTarget) {\n\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\teditor?.commands.focus()\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<MenuBar\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tvisibleItems={excludeMenuItems ? ALL_MENU_ITEMS.filter((item) => !excludeMenuItems.includes(item)) : undefined}\n\t\t\t\t/>\n\t\t\t\t<div className=\"relative\">\n\t\t\t\t\t<EditorContent\n\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\tclassName=\"min-h-48 px-2.5 py-2 [&_*:focus]:outline-none [&_.ProseMirror]:relative [&_.ProseMirror]:break-words [&_.ProseMirror]:overflow-hidden [&_.ProseMirror_p:empty]:min-h-[1em] [&_.ProseMirror_ul]:list-disc [&_.ProseMirror_ul]:pl-6 [&_.ProseMirror_ol]:list-decimal [&_.ProseMirror_ol]:pl-6 [&_.ProseMirror_li]:pl-1 [&_.ProseMirror_blockquote]:border-l-4 [&_.ProseMirror_blockquote]:border-muted-foreground/30 [&_.ProseMirror_blockquote]:pl-4 [&_.ProseMirror_blockquote]:italic [&_.is-editor-empty]:before:content-[attr(data-placeholder)] [&_.is-editor-empty]:before:absolute [&_.is-editor-empty]:before:text-muted-foreground [&_.is-editor-empty]:before:pointer-events-none [&_.is-editor-empty]:before:h-0\"\n\t\t\t\t\t/>\n\t\t\t\t\t{htmlPlaceholder && isContentEmpty && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"pointer-events-none absolute inset-0 px-2.5 py-2 text-sm text-muted-foreground [&_a]:text-muted-foreground [&_p]:m-0\"\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: trusted placeholder HTML from component props\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: htmlPlaceholder }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/rich-text-editor/rich-text-editor.tsx"
    }
  ],
  "docs": "UI strings in this component are hardcoded in English. It is a template component: wire it to your app's i18n (e.g. next-intl) after installing.",
  "type": "registry:ui"
}