{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "balanced-heading",
  "title": "Balanced Heading",
  "description": "A heading that breaks its lines to even lengths instead of stranding one word on the last line — pure CSS text-wrap with a copy-safe fallback for engines that lack it.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/balanced-heading.tsx",
      "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type BalancedHeadingTag = \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"div\" | \"span\"\n\n/**\n * `balance` evens out every line — right for short headings.\n * `pretty` only pulls a word down when the last line would be a lone word —\n * cheaper, no line limit, right for body copy and long titles.\n * `normal` opts out (and overrides an inherited `text-wrap` value).\n */\nexport type BalancedHeadingWrap = \"balance\" | \"pretty\" | \"normal\"\n\nconst WRAP_CLASS: Record<BalancedHeadingWrap, string> = {\n  balance: \"text-balance\",\n  pretty: \"text-pretty\",\n  normal: \"text-wrap\",\n}\n\n/**\n * Fallback for engines without `text-wrap`, applied to the last two words.\n *\n * `inline-block` makes the pair an atomic inline the line breaker cannot split,\n * which is what stops a lone word from dropping to its own line. `max-w-full`\n * is the guard that makes it safe: when the pair is wider than the container it\n * wraps *inside* the box, giving back exactly the plain-wrap line widths,\n * instead of running past the edge. Measured over 19 headings × 7 container\n * widths (120–760px), the naive `white-space: nowrap` version of this trick\n * pushed text outside the container in 31 of the 133 combinations, by up to\n * 158px; this one added zero overflow in all 133.\n *\n * `@supports` reverts it to a plain inline wherever the real property exists,\n * so the guard never competes with the browser's own line breaker. Each wrap\n * mode tests its own value: engines shipped `balance` before `pretty`.\n */\nconst PAIR_CLASS: Record<\"balance\" | \"pretty\", string> = {\n  balance:\n    \"inline-block max-w-full supports-[text-wrap:balance]:inline supports-[text-wrap:balance]:max-w-none\",\n  pretty:\n    \"inline-block max-w-full supports-[text-wrap:pretty]:inline supports-[text-wrap:pretty]:max-w-none\",\n}\n\n/** head (ends in whitespace) · the last two words · trailing whitespace. */\nconst LAST_PAIR = /^([\\s\\S]*\\s)(\\S+\\s+\\S+)(\\s*)$/\n\n/**\n * Wrap the trailing \"word space word\" of the last text child in the guard span.\n *\n * Only text is split, and only by inserting element boundaries — no character\n * is added, removed or substituted, so `textContent`, a selection and a copy\n * all still yield the original string. That rules out the two folk remedies:\n * `&nbsp;` swaps U+0020 for U+00A0 and a word joiner injects U+2060, both of\n * which land in the clipboard.\n */\nfunction bindLastPair(children: React.ReactNode, pairClassName: string): React.ReactNode {\n  const items = React.Children.toArray(children)\n  const last = items[items.length - 1]\n  // Nothing bindable: no children, or the heading ends in an element. Returning\n  // the children untouched is the honest outcome — better than guessing where\n  // the last word lives inside someone else's markup.\n  if (typeof last !== \"string\") return children\n  const match = LAST_PAIR.exec(last)\n  // Fewer than three words: the \"pair\" would be the whole string, and binding a\n  // string that already fails to fit changes nothing.\n  if (!match) return children\n  const [, head, pair, trailing] = match\n  return [\n    ...items.slice(0, -1),\n    head,\n    <span className={pairClassName} data-slot=\"last-pair\" key=\"zy-balanced-heading-pair\">\n      {pair}\n    </span>,\n    trailing,\n  ]\n}\n\nexport interface BalancedHeadingProps extends React.HTMLAttributes<HTMLElement> {\n  /** Rendered element. The heading *level* is the caller's decision, never the component's. */\n  as?: BalancedHeadingTag\n  /** Line-breaking strategy. See {@link BalancedHeadingWrap}. */\n  wrap?: BalancedHeadingWrap\n  /**\n   * Keep the last two words on the same line in engines without `text-wrap`.\n   * Inert (and free) everywhere the property is supported. Turn it off when the\n   * heading ends in a very long compound word you would rather let wrap alone.\n   */\n  guardLastPair?: boolean\n}\n\n/**\n * A heading whose lines are broken to even lengths instead of \"full line, full\n * line, one lonely word\".\n *\n * It is CSS, not JavaScript: no measurement, no effect, no `\"use client\"`, so\n * the first server-rendered frame is already the final one. The only markup it\n * adds is a guard span around the last two words, which does nothing at all in\n * a browser that implements `text-wrap`.\n *\n * Measured in Edge 150 (Chromium): `balance` evens out paragraphs up to six\n * lines and is a *complete* no-op from seven lines on. Same filler text, same\n * 520px box: at six lines it took the line-width standard deviation from 155px\n * to 33px; at seven lines the number is 141px before and 141px after, with\n * byte-identical line breaks. Use `wrap=\"pretty\"` for anything that can grow\n * past six lines; it has no such limit.\n */\nexport const BalancedHeading = React.forwardRef<HTMLElement, BalancedHeadingProps>(\n  ({ as = \"h2\", wrap = \"balance\", guardLastPair = true, className, children, ...props }, ref) => {\n    const Tag = as as React.ElementType\n    const guarded =\n      guardLastPair && wrap !== \"normal\" ? bindLastPair(children, PAIR_CLASS[wrap]) : children\n\n    return (\n      <Tag\n        className={cn(WRAP_CLASS[wrap], className)}\n        data-slot=\"balanced-heading\"\n        ref={ref}\n        {...props}\n      >\n        {guarded}\n      </Tag>\n    )\n  },\n)\n\nBalancedHeading.displayName = \"BalancedHeading\"\n\nexport default BalancedHeading\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}