{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "formatting-toolbar",
  "type": "registry:ui",
  "title": "Formatting Toolbar",
  "description": "An ARIA toolbar with toggle buttons, an alignment radio group, a style menu, overflow into “More”, tooltips and scoped shortcuts.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/formatting-toolbar.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nexport type FormatAlign = \"left\" | \"center\" | \"right\";\nexport type FormatBlock = \"p\" | \"h1\" | \"h2\" | \"h3\";\n\nexport interface FormatState {\n  bold: boolean;\n  italic: boolean;\n  underline: boolean;\n  strike: boolean;\n  align: FormatAlign;\n  block: FormatBlock;\n}\n\nexport const defaultFormatState: FormatState = {\n  bold: false,\n  italic: false,\n  underline: false,\n  strike: false,\n  align: \"left\",\n  block: \"p\",\n};\n\nexport interface FormattingToolbarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n  /** Controlled formatting state. */\n  value?: FormatState;\n  /** Initial state when uncontrolled. */\n  defaultValue?: Partial<FormatState>;\n  /** Called with the next state on every change. */\n  onValueChange?: (value: FormatState) => void;\n  size?: \"sm\" | \"md\";\n  /** Show tooltips with the label and shortcut on hover and keyboard focus. */\n  tooltips?: boolean;\n  /** Hover delay before the first tooltip appears, in ms. Neighbours then open instantly. */\n  tooltipDelay?: number;\n  /** Enable Ctrl / ⌘ shortcuts (B, I, U, ⇧X, ⇧L / E / R, ⌥0–3) on the toolbar and `shortcutTarget`. */\n  shortcuts?: boolean;\n  /** Element whose key presses also trigger the shortcuts (e.g. the editor or preview). */\n  shortcutTarget?: React.RefObject<HTMLElement | null>;\n  /** Collapse trailing items into a \"More\" menu when the toolbar runs out of room. */\n  overflow?: boolean;\n  /** Pressed-state tint, sliding indicator and focus rings. */\n  accent?: string;\n  /** Accessible name of the toolbar. */\n  label?: string;\n}\n\ntype ToggleKey = \"bold\" | \"italic\" | \"underline\" | \"strike\";\ninterface Keys {\n  shift?: boolean;\n  alt?: boolean;\n  /** KeyboardEvent.code, so shortcuts survive keyboard layouts and ⌥ on macOS. */\n  code: string;\n  /** Character shown in the hint. */\n  key: string;\n}\ntype ToolItem =\n  | { kind: \"block\"; id: \"block\" }\n  | { kind: \"separator\"; id: string }\n  | { kind: \"toggle\"; id: ToggleKey; label: string; keys: Keys; icon: string }\n  | { kind: \"align\"; id: string; value: FormatAlign; label: string; keys: Keys; icon: string };\n\nconst ITEMS: ToolItem[] = [\n  { kind: \"block\", id: \"block\" },\n  { kind: \"separator\", id: \"sep-1\" },\n  { kind: \"toggle\", id: \"bold\", label: \"Bold\", keys: { code: \"KeyB\", key: \"B\" }, icon: \"M5 3.25h4a2.6 2.6 0 0 1 0 5.2H5zM5 8.45h4.75a2.65 2.65 0 0 1 0 5.3H5z\" },\n  { kind: \"toggle\", id: \"italic\", label: \"Italic\", keys: { code: \"KeyI\", key: \"I\" }, icon: \"M7 3.25h5M4 12.75h5M9.6 3.25 6.4 12.75\" },\n  { kind: \"toggle\", id: \"underline\", label: \"Underline\", keys: { code: \"KeyU\", key: \"U\" }, icon: \"M4.75 3v4.75a3.25 3.25 0 0 0 6.5 0V3M3.75 13.75h8.5\" },\n  {\n    kind: \"toggle\",\n    id: \"strike\",\n    label: \"Strikethrough\",\n    keys: { shift: true, code: \"KeyX\", key: \"X\" },\n    icon: \"M2.75 8h10.5M11 4.9C10.55 3.85 9.5 3.25 8 3.25c-1.8 0-3 .85-3 2.2 0 .95.6 1.6 1.7 2M5 11.1c.45 1.05 1.5 1.65 3 1.65 1.8 0 3-.85 3-2.2 0-.4-.1-.75-.3-1.05\",\n  },\n  { kind: \"separator\", id: \"sep-2\" },\n  { kind: \"align\", id: \"align-left\", value: \"left\", label: \"Align left\", keys: { shift: true, code: \"KeyL\", key: \"L\" }, icon: \"M2.75 4h10.5M2.75 8h6.5M2.75 12h8.5\" },\n  { kind: \"align\", id: \"align-center\", value: \"center\", label: \"Align centre\", keys: { shift: true, code: \"KeyE\", key: \"E\" }, icon: \"M2.75 4h10.5M4.75 8h6.5M3.75 12h8.5\" },\n  { kind: \"align\", id: \"align-right\", value: \"right\", label: \"Align right\", keys: { shift: true, code: \"KeyR\", key: \"R\" }, icon: \"M2.75 4h10.5M6.75 8h6.5M4.75 12h8.5\" },\n];\n\nconst BLOCKS: { value: FormatBlock; label: string; keys: Keys }[] = [\n  { value: \"p\", label: \"Paragraph\", keys: { alt: true, code: \"Digit0\", key: \"0\" } },\n  { value: \"h1\", label: \"Heading 1\", keys: { alt: true, code: \"Digit1\", key: \"1\" } },\n  { value: \"h2\", label: \"Heading 2\", keys: { alt: true, code: \"Digit2\", key: \"2\" } },\n  { value: \"h3\", label: \"Heading 3\", keys: { alt: true, code: \"Digit3\", key: \"3\" } },\n];\n\nconst DIMENSIONS = {\n  sm: { button: 28, block: 112, text: \"text-[12px]\", icon: \"size-3.5\" },\n  md: { button: 32, block: 124, text: \"text-[13px]\", icon: \"size-4\" },\n} as const;\nconst GAP = 2; // px between items (gap-0.5)\nconst SEPARATOR = 9; // 1px line + 4px margin each side\nconst CHROME = 10; // 4px padding + 1px border on each side\nconst SKIP_DELAY = 400; // ms after a tooltip hides during which the next one opens instantly\nconst EDGE = 8;\n\n/* ------------------------------- environment ------------------------------- */\n\nfunction subscribeReducedMotion(cb: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", cb);\n  return () => mq.removeEventListener(\"change\", cb);\n}\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\nfunction detectMac() {\n  const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n  return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform ?? nav.platform ?? \"\");\n}\nconst noopSubscribe = () => () => {};\n/** \"Ctrl\" on the server and first paint, refined to ⌘ on Apple platforms after hydration. */\nfunction useIsMac() {\n  return React.useSyncExternalStore(noopSubscribe, detectMac, () => false);\n}\n\nfunction shortcutHint(keys: Keys, mac: boolean) {\n  if (mac) return `${keys.alt ? \"⌥\" : \"\"}${keys.shift ? \"⇧\" : \"\"}⌘${keys.key}`;\n  return `Ctrl+${keys.alt ? \"Alt+\" : \"\"}${keys.shift ? \"Shift+\" : \"\"}${keys.key}`;\n}\nfunction ariaShortcut(keys: Keys, mac: boolean) {\n  return [mac ? \"Meta\" : \"Control\", keys.alt ? \"Alt\" : \"\", keys.shift ? \"Shift\" : \"\", keys.key].filter(Boolean).join(\"+\");\n}\nfunction spokenShortcut(keys: Keys, mac: boolean) {\n  return [mac ? \"Command\" : \"Control\", keys.alt ? (mac ? \"Option\" : \"Alt\") : \"\", keys.shift ? \"Shift\" : \"\", keys.key].filter(Boolean).join(\" \");\n}\nfunction matches(event: KeyboardEvent | React.KeyboardEvent, keys: Keys, mac: boolean) {\n  const mod = mac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;\n  return mod && event.code === keys.code && event.shiftKey === Boolean(keys.shift) && event.altKey === Boolean(keys.alt);\n}\n\nfunction Glyph({ d, className }: { d: string; className: string }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" className={className}>\n      <path d={d} stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\n/* --------------------------------- toolbar --------------------------------- */\n\n/**\n * An ARIA toolbar for text formatting: bold / italic / underline / strikethrough toggle buttons\n * (aria-pressed), an alignment radio group, a block-type menu button and separators, with one tab\n * stop and roving focus (Arrow keys, Home / End). Trailing items collapse into a \"More\" menu when\n * the toolbar runs out of room, tooltips show the label and a platform-aware shortcut, and the\n * Ctrl / ⌘ shortcuts can be scoped to a target element. It only manages state — style anything.\n */\nexport function FormattingToolbar({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  size = \"md\",\n  tooltips = true,\n  tooltipDelay = 500,\n  shortcuts = true,\n  shortcutTarget,\n  overflow = true,\n  accent = \"#FF7448\",\n  label = \"Text formatting\",\n  className = \"\",\n  style,\n  ...props\n}: FormattingToolbarProps) {\n  const uid = React.useId();\n  const mac = useIsMac();\n  const reduceMotion = usePrefersReducedMotion();\n  const dims = DIMENSIONS[size] ?? DIMENSIONS.md;\n  const wrapRef = React.useRef<HTMLDivElement>(null);\n  const toolbarRef = React.useRef<HTMLDivElement>(null);\n  const [internal, setInternal] = React.useState<FormatState>(() => ({ ...defaultFormatState, ...defaultValue }));\n  const value = valueProp ?? internal;\n  const [visibleCount, setVisibleCount] = React.useState(ITEMS.length);\n  const [activeId, setActiveId] = React.useState<string>(\"block\");\n  const [menu, setMenu] = React.useState<{ kind: \"block\" | \"more\"; anchor: HTMLElement; focus: \"first\" | \"last\" | \"checked\" | \"panel\" } | null>(null);\n  const [tip, setTip] = React.useState<{ id: string; text: string; hint?: string; left: number } | null>(null);\n  const tipTimer = React.useRef(0);\n  const tipOpen = React.useRef(false);\n  const tipHiddenAt = React.useRef(-Infinity);\n\n  const update = (patch: Partial<FormatState>) => {\n    const next = { ...value, ...patch };\n    if (valueProp === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n\n  /* -------------------------------- overflow -------------------------------- */\n\n  React.useEffect(() => {\n    const wrap = wrapRef.current;\n    if (!wrap || !overflow) return;\n    const widthOf = (item: ToolItem) => (item.kind === \"separator\" ? SEPARATOR : item.kind === \"block\" ? dims.block : dims.button);\n    const measure = () => {\n      const available = wrap.clientWidth;\n      const total = (count: number, more: boolean) => {\n        const shown = ITEMS.slice(0, count);\n        const n = shown.length + (more ? 1 : 0);\n        return CHROME + shown.reduce((sum, item) => sum + widthOf(item), 0) + (more ? dims.button : 0) + Math.max(0, n - 1) * GAP;\n      };\n      let count = ITEMS.length;\n      if (total(count, false) > available) {\n        count = ITEMS.length - 1;\n        while (count > 1 && total(count, true) > available) count--;\n        // Never end on a separator.\n        while (count > 1 && ITEMS[count - 1].kind === \"separator\") count--;\n      }\n      setVisibleCount(count);\n    };\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(wrap);\n    return () => ro.disconnect();\n  }, [overflow, dims]);\n\n  const count = overflow ? visibleCount : ITEMS.length;\n  const visible = ITEMS.slice(0, count);\n  const hidden = ITEMS.slice(count).filter((item) => item.kind !== \"separator\");\n  const focusableIds = [...visible.filter((i) => i.kind !== \"separator\").map((i) => i.id), ...(hidden.length ? [\"more\"] : [])];\n  const tabStop = focusableIds.includes(activeId) ? activeId : focusableIds[0];\n\n  /* -------------------------------- shortcuts ------------------------------- */\n\n  /** Applies the shortcut matching `event`, if any. */\n  const runShortcut = (event: KeyboardEvent | React.KeyboardEvent) => {\n    const isMac = detectMac();\n    for (const item of ITEMS) {\n      if (item.kind === \"toggle\" && matches(event, item.keys, isMac)) {\n        event.preventDefault();\n        update({ [item.id]: !value[item.id] });\n        return true;\n      }\n      if (item.kind === \"align\" && matches(event, item.keys, isMac)) {\n        event.preventDefault();\n        update({ align: item.value });\n        return true;\n      }\n    }\n    for (const block of BLOCKS) {\n      if (matches(event, block.keys, isMac)) {\n        event.preventDefault();\n        update({ block: block.value });\n        return true;\n      }\n    }\n    return false;\n  };\n\n  // The native listener on the target calls the latest handler without re-subscribing.\n  const shortcutHandler = React.useRef(runShortcut);\n  React.useEffect(() => {\n    shortcutHandler.current = runShortcut;\n  });\n  React.useEffect(() => {\n    const target = shortcutTarget?.current;\n    if (!shortcuts || !target) return;\n    const onKey = (event: KeyboardEvent) => {\n      if (!event.defaultPrevented) shortcutHandler.current(event);\n    };\n    target.addEventListener(\"keydown\", onKey);\n    return () => target.removeEventListener(\"keydown\", onKey);\n  }, [shortcuts, shortcutTarget]);\n\n  /* --------------------------------- tooltips -------------------------------- */\n\n  const hideTip = React.useCallback(() => {\n    window.clearTimeout(tipTimer.current);\n    if (tipOpen.current) tipHiddenAt.current = performance.now();\n    tipOpen.current = false;\n    setTip(null);\n  }, []);\n\n  const showTip = (el: HTMLElement) => {\n    if (!tooltips || menu) return;\n    window.clearTimeout(tipTimer.current);\n    const wrap = wrapRef.current;\n    const id = el.dataset.tbId ?? \"\";\n    if (!wrap || (tipOpen.current && tip?.id === id)) return;\n    const place = () => {\n      const w = wrap.getBoundingClientRect();\n      const r = el.getBoundingClientRect();\n      tipOpen.current = true;\n      setTip({ id, text: el.dataset.tip ?? \"\", hint: el.dataset.hint || undefined, left: Math.round(r.left - w.left + r.width / 2) });\n    };\n    // Delay group: while a tooltip is open (or just closed), neighbours open instantly.\n    const warm = tipOpen.current || performance.now() - tipHiddenAt.current < SKIP_DELAY;\n    if (warm) place();\n    else tipTimer.current = window.setTimeout(place, tooltipDelay);\n  };\n\n  // Escape dismisses the tooltip without moving focus or the pointer (WCAG 1.4.13).\n  React.useEffect(() => {\n    if (!tip) return;\n    const onKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") hideTip();\n    };\n    document.addEventListener(\"keydown\", onKey);\n    return () => document.removeEventListener(\"keydown\", onKey);\n  }, [tip, hideTip]);\n\n  React.useEffect(() => () => window.clearTimeout(tipTimer.current), []);\n\n  /** Attributes shared by every roving item: tab stop and tooltip text (read by the delegated handlers). */\n  const itemProps = (id: string, text: string, hint?: string) => ({\n    \"data-tb-item\": \"\",\n    \"data-tb-id\": id,\n    \"data-tip\": text,\n    \"data-hint\": hint ?? \"\",\n    tabIndex: id === tabStop ? 0 : -1,\n  });\n  const itemFrom = (target: EventTarget | null) => (target instanceof Element ? target.closest<HTMLElement>(\"[data-tb-item]\") : null);\n\n  const onToolbarKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (shortcuts && runShortcut(event)) return;\n    const items = Array.from(event.currentTarget.querySelectorAll<HTMLElement>(\"[data-tb-item]\"));\n    const index = items.indexOf(document.activeElement as HTMLElement);\n    if (index < 0) return;\n    const focusAt = (i: number) => items[(i + items.length) % items.length]?.focus();\n    if (event.key === \"ArrowRight\") focusAt(index + 1);\n    else if (event.key === \"ArrowLeft\") focusAt(index - 1);\n    else if (event.key === \"Home\") focusAt(0);\n    else if (event.key === \"End\") focusAt(-1);\n    else return;\n    event.preventDefault();\n  };\n\n  const openMenu = (kind: \"block\" | \"more\", anchor: HTMLElement, focus: \"first\" | \"last\" | \"checked\" | \"panel\") => {\n    // The tooltip render is suppressed while a menu is open (see below).\n    setTip(null);\n    setMenu({ kind, anchor, focus });\n  };\n  const closeMenu = (returnFocus: boolean) => {\n    if (returnFocus) menu?.anchor.focus({ preventScroll: true });\n    setMenu(null);\n  };\n\n  const menuButtonKeys = (kind: \"block\" | \"more\") => (event: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (event.key === \"ArrowDown\" || event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      event.stopPropagation();\n      openMenu(kind, event.currentTarget, kind === \"block\" ? \"checked\" : \"first\");\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      event.stopPropagation();\n      openMenu(kind, event.currentTarget, \"last\");\n    }\n  };\n\n  const pressedClass =\n    \"aria-pressed:bg-[color-mix(in_oklab,var(--ft-accent)_18%,transparent)] aria-pressed:text-zinc-950 aria-pressed:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--ft-accent)_45%,transparent)] dark:aria-pressed:text-white\";\n  const buttonClass = `relative inline-flex shrink-0 items-center justify-center rounded-lg text-zinc-600 outline-hidden transition-[background-color,color,box-shadow] duration-150 hover:bg-zinc-900/[0.05] hover:text-zinc-950 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-solid focus-visible:outline-[var(--ft-accent)] dark:text-zinc-400 dark:hover:bg-white/[0.06] dark:hover:text-white`;\n  const sq = { width: dims.button, height: dims.button };\n  const blockLabel = BLOCKS.find((b) => b.value === value.block)?.label ?? \"Paragraph\";\n  const blockKeys = BLOCKS.find((b) => b.value === value.block)?.keys;\n\n  const renderItem = (item: ToolItem) => {\n    if (item.kind === \"separator\") {\n      return <div key={item.id} role=\"separator\" aria-orientation=\"vertical\" className=\"mx-1 h-[55%] w-px shrink-0 self-center bg-zinc-900/10 dark:bg-white/10\" />;\n    }\n    if (item.kind === \"block\") {\n      return (\n        <button\n          key={item.id}\n          type=\"button\"\n          aria-haspopup=\"menu\"\n          aria-expanded={menu?.kind === \"block\"}\n          aria-controls={menu?.kind === \"block\" ? `${uid}-menu` : undefined}\n          aria-label={`Text style: ${blockLabel}`}\n          {...itemProps(item.id, \"Text style\", blockKeys ? shortcutHint(blockKeys, mac) : undefined)}\n          onKeyDown={menuButtonKeys(\"block\")}\n          onClick={(event) => (menu?.kind === \"block\" ? closeMenu(false) : openMenu(\"block\", event.currentTarget, \"panel\"))}\n          className={`${buttonClass} justify-between gap-1 px-2.5 font-medium tracking-tight text-zinc-800 dark:text-zinc-200 ${dims.text} ${menu?.kind === \"block\" ? \"bg-zinc-900/[0.05] dark:bg-white/[0.06]\" : \"\"}`}\n          style={{ width: dims.block, height: dims.button }}\n        >\n          <span className=\"truncate\">{blockLabel}</span>\n          <Glyph d=\"M4.5 6.25 8 9.75l3.5-3.5\" className={`${dims.icon} shrink-0 opacity-60`} />\n        </button>\n      );\n    }\n    const describedBy = `${uid}-${item.id}-keys`;\n    if (item.kind === \"toggle\") {\n      const pressed = value[item.id];\n      return (\n        <button\n          key={item.id}\n          type=\"button\"\n          aria-label={item.label}\n          aria-pressed={pressed}\n          aria-keyshortcuts={shortcuts ? ariaShortcut(item.keys, mac) : undefined}\n          aria-describedby={shortcuts ? describedBy : undefined}\n          {...itemProps(item.id, item.label, shortcuts ? shortcutHint(item.keys, mac) : undefined)}\n          onClick={() => update({ [item.id]: !pressed })}\n          className={`${buttonClass} ${pressedClass}`}\n          style={sq}\n        >\n          <Glyph d={item.icon} className={dims.icon} />\n          {shortcuts ? (\n            <span id={describedBy} hidden>\n              {spokenShortcut(item.keys, mac)}\n            </span>\n          ) : null}\n        </button>\n      );\n    }\n    const checked = value.align === item.value;\n    return (\n      <button\n        key={item.id}\n        type=\"button\"\n        role=\"radio\"\n        aria-checked={checked}\n        aria-label={item.label}\n        aria-keyshortcuts={shortcuts ? ariaShortcut(item.keys, mac) : undefined}\n        aria-describedby={shortcuts ? describedBy : undefined}\n        {...itemProps(item.id, item.label, shortcuts ? shortcutHint(item.keys, mac) : undefined)}\n        onClick={() => update({ align: item.value })}\n        onKeyDown={(event) => {\n          if (event.key === \" \" || event.key === \"Enter\") {\n            event.preventDefault();\n            update({ align: item.value });\n          }\n        }}\n        className={`${buttonClass} ${checked ? \"text-zinc-950 dark:text-white\" : \"\"}`}\n        style={sq}\n      >\n        {checked ? (\n          <motion.span\n            layoutId={`${uid}-align`}\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 rounded-lg bg-[color-mix(in_oklab,var(--ft-accent)_18%,transparent)] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--ft-accent)_45%,transparent)]\"\n            transition={reduceMotion ? { duration: 0 } : { type: \"spring\", stiffness: 520, damping: 38 }}\n          />\n        ) : null}\n        <Glyph d={item.icon} className={`relative ${dims.icon}`} />\n        {shortcuts ? (\n          <span id={describedBy} hidden>\n            {spokenShortcut(item.keys, mac)}\n          </span>\n        ) : null}\n      </button>\n    );\n  };\n\n  // Group the alignment radios so they are announced as one radio group.\n  const rendered: React.ReactNode[] = [];\n  let radios: React.ReactNode[] = [];\n  const flushRadios = () => {\n    if (!radios.length) return;\n    rendered.push(\n      <div key=\"align-group\" role=\"radiogroup\" aria-label=\"Alignment\" className=\"flex shrink-0 items-center gap-0.5\">\n        {radios}\n      </div>,\n    );\n    radios = [];\n  };\n  for (const item of visible) {\n    if (item.kind === \"align\") radios.push(renderItem(item));\n    else {\n      flushRadios();\n      if (!(item.kind === \"separator\" && item === visible[visible.length - 1])) rendered.push(renderItem(item));\n    }\n  }\n  flushRadios();\n\n  /* ---------------------------------- menus ---------------------------------- */\n\n  const menuEntries: MenuEntry[] =\n    menu?.kind === \"block\"\n      ? BLOCKS.map((b) => ({\n          id: b.value,\n          role: \"menuitemradio\" as const,\n          label: b.label,\n          checked: value.block === b.value,\n          hint: shortcuts ? shortcutHint(b.keys, mac) : undefined,\n          preview: b.value,\n          keepOpen: false,\n          onSelect: () => update({ block: b.value }),\n        }))\n      : hidden.flatMap((item): MenuEntry[] => {\n          if (item.kind === \"toggle\") {\n            return [\n              {\n                id: item.id,\n                role: \"menuitemcheckbox\",\n                label: item.label,\n                checked: value[item.id],\n                icon: item.icon,\n                hint: shortcuts ? shortcutHint(item.keys, mac) : undefined,\n                keepOpen: true,\n                onSelect: () => update({ [item.id]: !value[item.id] }),\n              },\n            ];\n          }\n          if (item.kind === \"align\") {\n            return [\n              {\n                id: item.id,\n                role: \"menuitemradio\",\n                label: item.label,\n                checked: value.align === item.value,\n                icon: item.icon,\n                hint: shortcuts ? shortcutHint(item.keys, mac) : undefined,\n                keepOpen: true,\n                onSelect: () => update({ align: item.value }),\n              },\n            ];\n          }\n          if (item.kind === \"block\") {\n            return BLOCKS.map((b) => ({\n              id: b.value,\n              role: \"menuitemradio\" as const,\n              label: b.label,\n              checked: value.block === b.value,\n              hint: shortcuts ? shortcutHint(b.keys, mac) : undefined,\n              keepOpen: true,\n              onSelect: () => update({ block: b.value }),\n            }));\n          }\n          return [];\n        });\n\n  return (\n    <div\n      ref={wrapRef}\n      className={`relative flex w-full min-w-0 ${className}`}\n      style={{ [\"--ft-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      <div\n        ref={toolbarRef}\n        role=\"toolbar\"\n        aria-label={label}\n        aria-orientation=\"horizontal\"\n        onKeyDown={onToolbarKeyDown}\n        onFocus={(event) => {\n          const item = itemFrom(event.target);\n          if (!item) return;\n          setActiveId(item.dataset.tbId ?? \"\");\n          if (item === event.target && item.matches(\":focus-visible\")) showTip(item);\n        }}\n        onBlur={hideTip}\n        onPointerOver={(event) => {\n          const item = itemFrom(event.target);\n          if (event.pointerType === \"mouse\" && item) showTip(item);\n        }}\n        onPointerOut={(event) => {\n          const item = itemFrom(event.target);\n          if (event.pointerType === \"mouse\" && item && !item.contains(event.relatedTarget as Node | null)) hideTip();\n        }}\n        onPointerDown={hideTip}\n        className={`inline-flex max-w-full shrink-0 items-center gap-0.5 rounded-xl border border-zinc-900/10 bg-white p-1 shadow-[0_1px_2px_rgb(0_0_0/0.05),0_10px_24px_-14px_rgb(0_0_0/0.25)] dark:border-white/10 dark:bg-[#1a1d21] dark:shadow-[0_12px_28px_-14px_rgb(0_0_0/0.8)] ${overflow ? \"\" : \"flex-wrap\"}`}\n      >\n        {rendered}\n        {hidden.length ? (\n          <button\n            type=\"button\"\n            aria-label=\"More formatting\"\n            aria-haspopup=\"menu\"\n            aria-expanded={menu?.kind === \"more\"}\n            aria-controls={menu?.kind === \"more\" ? `${uid}-menu` : undefined}\n            {...itemProps(\"more\", `More · ${hidden.length}`)}\n            onKeyDown={menuButtonKeys(\"more\")}\n            onClick={(event) => (menu?.kind === \"more\" ? closeMenu(false) : openMenu(\"more\", event.currentTarget, \"panel\"))}\n            className={`${buttonClass} ${menu?.kind === \"more\" ? \"bg-zinc-900/[0.05] dark:bg-white/[0.06]\" : \"\"}`}\n            style={sq}\n          >\n            <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className={dims.icon} fill=\"currentColor\">\n              <circle cx=\"3.5\" cy=\"8\" r=\"1.25\" />\n              <circle cx=\"8\" cy=\"8\" r=\"1.25\" />\n              <circle cx=\"12.5\" cy=\"8\" r=\"1.25\" />\n            </svg>\n            {hidden.some((item) => (item.kind === \"toggle\" && value[item.id]) || (item.kind === \"align\" && value.align === item.value && item.value !== \"left\")) ? (\n              <span aria-hidden=\"true\" className=\"absolute right-1 top-1 size-1.5 rounded-full bg-[var(--ft-accent)]\" />\n            ) : null}\n          </button>\n        ) : null}\n      </div>\n\n      <AnimatePresence>\n        {tip && tooltips && !menu ? (\n          <motion.div\n            key=\"tip\"\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute top-full z-20 mt-2 flex -translate-x-1/2 items-center gap-2 whitespace-nowrap rounded-md bg-zinc-900 px-2 py-1 text-[12px] font-medium tracking-tight text-white shadow-lg dark:bg-zinc-100 dark:text-zinc-900\"\n            style={{ left: tip.left }}\n            initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -3 }}\n            animate={{ opacity: 1, y: 0, left: tip.left }}\n            exit={{ opacity: 0, transition: { duration: 0.08 } }}\n            transition={reduceMotion ? { duration: 0.1 } : { type: \"spring\", stiffness: 600, damping: 40, opacity: { duration: 0.12 } }}\n          >\n            {tip.text}\n            {tip.hint ? <kbd className=\"font-mono text-[10px] tracking-normal text-white/55 dark:text-zinc-900/55\">{tip.hint}</kbd> : null}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {menu ? (\n          <ToolbarMenu\n            key={menu.kind}\n            id={`${uid}-menu`}\n            label={menu.kind === \"block\" ? \"Text style\" : \"More formatting\"}\n            anchor={menu.anchor}\n            focus={menu.focus}\n            entries={menuEntries}\n            reduceMotion={reduceMotion}\n            accent={accent}\n            onClose={closeMenu}\n          />\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n/* ---------------------------------- menu ---------------------------------- */\n\ninterface MenuEntry {\n  id: string;\n  role: \"menuitemcheckbox\" | \"menuitemradio\";\n  label: string;\n  checked: boolean;\n  icon?: string;\n  hint?: string;\n  /** Block previews render the label at that heading size. */\n  preview?: FormatBlock;\n  /** Toggles in the overflow menu keep it open so several can be changed. */\n  keepOpen: boolean;\n  onSelect: () => void;\n}\n\nconst PREVIEW_CLASS: Record<FormatBlock, string> = {\n  p: \"text-[13px] font-normal\",\n  h1: \"text-[17px] font-semibold tracking-[-0.02em]\",\n  h2: \"text-[15px] font-semibold tracking-[-0.01em]\",\n  h3: \"text-[13px] font-semibold\",\n};\n\nfunction ToolbarMenu({\n  id,\n  label,\n  anchor,\n  focus,\n  entries,\n  reduceMotion,\n  accent,\n  onClose,\n}: {\n  id: string;\n  label: string;\n  anchor: HTMLElement;\n  focus: \"first\" | \"last\" | \"checked\" | \"panel\";\n  entries: MenuEntry[];\n  reduceMotion: boolean;\n  accent: string;\n  onClose: (returnFocus: boolean) => void;\n}) {\n  const nodeRef = React.useRef<HTMLDivElement | null>(null);\n  const typeahead = React.useRef({ buffer: \"\", timer: 0 });\n\n  const place = React.useCallback(() => {\n    const node = nodeRef.current;\n    if (!node) return;\n    const r = anchor.getBoundingClientRect();\n    const vw = document.documentElement.clientWidth;\n    const vh = window.innerHeight;\n    const w = node.offsetWidth;\n    const h = node.offsetHeight;\n    const above = r.bottom + 6 + h > vh - EDGE && r.top - 6 - h >= EDGE;\n    const left = Math.max(EDGE, Math.min(r.left, vw - w - EDGE));\n    node.style.left = `${Math.round(left)}px`;\n    node.style.top = `${Math.round(above ? r.top - 6 - h : r.bottom + 6)}px`;\n    node.style.transformOrigin = `${Math.round(r.left + r.width / 2 - left)}px ${above ? \"100%\" : \"0%\"}`;\n  }, [anchor]);\n\n  const mount = (node: HTMLDivElement | null) => {\n    if (!node || node === nodeRef.current) return;\n    nodeRef.current = node;\n    if (typeof node.showPopover === \"function\" && !node.matches(\":popover-open\")) {\n      try {\n        node.showPopover();\n      } catch {\n        // position: fixed fallback\n      }\n    }\n    place();\n    const items = Array.from(node.querySelectorAll<HTMLElement>(\"[data-tm-item]\"));\n    const target =\n      focus === \"first\" ? items[0] : focus === \"last\" ? items[items.length - 1] : focus === \"checked\" ? (items.find((el) => el.getAttribute(\"aria-checked\") === \"true\") ?? items[0]) : node;\n    (target ?? node).focus({ preventScroll: true });\n  };\n\n  React.useEffect(() => {\n    let frame = 0;\n    const onPointer = (event: PointerEvent) => {\n      const t = event.target as Node;\n      if (nodeRef.current?.contains(t) || anchor.contains(t)) return;\n      onClose(false);\n    };\n    const onMove = () => {\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(place);\n    };\n    const onBlur = () => onClose(false);\n    const t = typeahead.current;\n    document.addEventListener(\"pointerdown\", onPointer, true);\n    window.addEventListener(\"scroll\", onMove, { capture: true, passive: true });\n    window.addEventListener(\"resize\", onMove);\n    window.addEventListener(\"blur\", onBlur);\n    return () => {\n      cancelAnimationFrame(frame);\n      window.clearTimeout(t.timer);\n      document.removeEventListener(\"pointerdown\", onPointer, true);\n      window.removeEventListener(\"scroll\", onMove, { capture: true });\n      window.removeEventListener(\"resize\", onMove);\n      window.removeEventListener(\"blur\", onBlur);\n    };\n  }, [anchor, onClose, place]);\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    // Keys handled here never reach the toolbar's roving focus.\n    event.stopPropagation();\n    const items = Array.from(event.currentTarget.querySelectorAll<HTMLElement>(\"[data-tm-item]\"));\n    const index = items.indexOf(document.activeElement as HTMLElement);\n    const focusAt = (i: number) => items[(i + items.length) % items.length]?.focus({ preventScroll: true });\n    switch (event.key) {\n      case \"ArrowDown\":\n        event.preventDefault();\n        focusAt(index + 1);\n        return;\n      case \"ArrowUp\":\n        event.preventDefault();\n        focusAt(index < 0 ? -1 : index - 1);\n        return;\n      case \"Home\":\n        event.preventDefault();\n        focusAt(0);\n        return;\n      case \"End\":\n        event.preventDefault();\n        focusAt(-1);\n        return;\n      case \"Escape\":\n      case \"Tab\":\n        event.preventDefault();\n        onClose(true);\n        return;\n      case \"Enter\":\n      case \" \":\n        if (index >= 0) {\n          event.preventDefault();\n          items[index].click();\n        }\n        return;\n    }\n    if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n      const t = typeahead.current;\n      window.clearTimeout(t.timer);\n      t.buffer += event.key.toLowerCase();\n      t.timer = window.setTimeout(() => (t.buffer = \"\"), 600);\n      const ordered = [...items.slice(index + 1), ...items.slice(0, index + 1)];\n      ordered.find((el) => (el.dataset.label ?? \"\").startsWith(t.buffer))?.focus({ preventScroll: true });\n    }\n  };\n\n  const motionProps = reduceMotion\n    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.08 } }\n    : {\n        initial: { opacity: 0, scale: 0.94 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.97, transition: { duration: 0.1 } },\n        transition: { type: \"spring\" as const, stiffness: 560, damping: 34, mass: 0.7 },\n      };\n\n  return (\n    <motion.div\n      ref={mount}\n      id={id}\n      role=\"menu\"\n      aria-label={label}\n      tabIndex={-1}\n      popover=\"manual\"\n      onKeyDown={onKeyDown}\n      {...motionProps}\n      className=\"fixed inset-auto m-0 min-w-[208px] rounded-xl border border-zinc-900/10 bg-white p-1 text-[13px] text-zinc-800 shadow-[0_24px_48px_-20px_rgb(0_0_0/0.4),0_2px_6px_-2px_rgb(0_0_0/0.08)] outline-hidden dark:border-white/10 dark:bg-[#1a1d21] dark:text-zinc-200 dark:shadow-[0_24px_48px_-16px_rgb(0_0_0/0.75)]\"\n      style={{ [\"--ft-accent\" as string]: accent }}\n    >\n      {entries.map((entry) => (\n        <div\n          key={entry.id}\n          role={entry.role}\n          aria-checked={entry.checked}\n          tabIndex={-1}\n          data-tm-item=\"\"\n          data-label={entry.label.toLowerCase()}\n          className=\"group/item flex min-h-8 cursor-default items-center gap-2 rounded-md px-2 py-1 outline-hidden transition-colors duration-75 focus:bg-[color-mix(in_oklab,var(--ft-accent)_14%,transparent)] focus:text-zinc-950 dark:focus:text-white\"\n          onClick={() => {\n            entry.onSelect();\n            if (!entry.keepOpen) onClose(true);\n          }}\n          onPointerMove={(event) => {\n            if (event.pointerType === \"mouse\" && document.activeElement !== event.currentTarget) event.currentTarget.focus({ preventScroll: true });\n          }}\n        >\n          <span aria-hidden=\"true\" className=\"flex w-4 shrink-0 items-center justify-center\">\n            {entry.checked ? (\n              entry.role === \"menuitemradio\" ? (\n                <span className=\"size-1.5 rounded-full bg-[var(--ft-accent)] ring-[3px] ring-[color-mix(in_oklab,var(--ft-accent)_25%,transparent)]\" />\n              ) : (\n                <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4 text-[var(--ft-accent)]\">\n                  <path d=\"M3.5 8.5 6.5 11.5 12.5 4.5\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                </svg>\n              )\n            ) : null}\n          </span>\n          {entry.icon ? <Glyph d={entry.icon} className=\"size-4 shrink-0 text-zinc-500 dark:text-zinc-400\" /> : null}\n          <span className={`min-w-0 flex-1 truncate ${entry.preview ? PREVIEW_CLASS[entry.preview] : \"\"}`}>{entry.label}</span>\n          {entry.hint ? (\n            <kbd aria-hidden=\"true\" className=\"ml-4 shrink-0 font-mono text-[10px] tracking-normal text-zinc-400 dark:text-zinc-500\">\n              {entry.hint}\n            </kbd>\n          ) : null}\n        </div>\n      ))}\n    </motion.div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "buttons"
  ],
  "docs": "Documentation: https://wancraft.my/components/formatting-toolbar"
}
