{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flip-text",
  "title": "Flip text",
  "description": "Letters tumble on hover with a vermilion accent. Click, tap, or use the keyboard to flip the whole phrase. Respects reduced motion.",
  "registryDependencies": [
    "@jbm/tokens"
  ],
  "files": [
    {
      "path": "registry/jbm/ui/flip-text.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, type CSSProperties } from \"react\"\nimport { color, font } from \"../lib/tokens\"\n\nexport type FlipTextProps = {\n  children: string\n  /** Duration of each character flip, in milliseconds. */\n  duration?: number\n  accentColor?: string\n  reducedMotion?: boolean\n  className?: string\n  style?: CSSProperties\n}\n\n/** Hover a character to flip it; click, tap, Enter, or Space flips the whole text. */\nexport function FlipText({\n  children,\n  duration = 450,\n  accentColor = color.accent,\n  reducedMotion,\n  className,\n  style,\n}: FlipTextProps) {\n  const ref = useRef<HTMLButtonElement>(null)\n  const animations = useRef(new Set<Animation>())\n  useEffect(() => {\n    const active = animations.current\n    const media = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const stop = () => {\n      for (const animation of active) animation.cancel()\n      active.clear()\n    }\n    const update = () => {\n      if (reducedMotion ?? media.matches) stop()\n    }\n    update()\n    media.addEventListener(\"change\", update)\n    return () => {\n      media.removeEventListener(\"change\", update)\n      stop()\n    }\n  }, [children, reducedMotion])\n\n  function flip(element: HTMLElement, delay = 0) {\n    if (\n      reducedMotion ??\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    )\n      return\n    for (const animation of element.getAnimations()) animation.cancel()\n    const animation = element.animate(\n      [\n        {\n          transform: \"perspective(400px) translateY(0) rotateX(0deg)\",\n          color: \"inherit\",\n        },\n        {\n          transform: \"perspective(400px) translateY(-0.15em) rotateX(180deg)\",\n          color: accentColor,\n          offset: 0.5,\n        },\n        {\n          transform: \"perspective(400px) translateY(0) rotateX(360deg)\",\n          color: \"inherit\",\n        },\n      ],\n      {\n        duration: Number.isFinite(duration) ? Math.max(0, duration) : 450,\n        delay,\n        easing: \"cubic-bezier(0.22, 1, 0.36, 1)\",\n      }\n    )\n    animations.current.add(animation)\n    const remove = () => animations.current.delete(animation)\n    animation.onfinish = remove\n    animation.oncancel = remove\n  }\n\n  const segmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" })\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      aria-label={`Flip text: ${children}`}\n      className={className}\n      onClick={() =>\n        ref.current\n          ?.querySelectorAll<HTMLElement>(\"[data-flip-character]\")\n          .forEach((element, index) => flip(element, index * 25))\n      }\n      style={{\n        display: \"inline-block\",\n        maxWidth: \"100%\",\n        font: \"inherit\",\n        fontFamily: font.sans,\n        fontWeight: 800,\n        lineHeight: 1.3,\n        color: color.ink,\n        background: \"transparent\",\n        border: 0,\n        padding: \"0.2em 0\",\n        cursor: \"pointer\",\n        textAlign: \"inherit\",\n        whiteSpace: \"pre-wrap\",\n        overflowWrap: \"anywhere\",\n        ...style,\n      }}\n    >\n      <span aria-hidden=\"true\">\n        {children.split(/(\\s+)/u).map((word, wordIndex) =>\n          /^\\s*$/u.test(word) ? (\n            word\n          ) : (\n            <span\n              key={wordIndex}\n              style={{ display: \"inline-block\", maxWidth: \"100%\" }}\n            >\n              {[...segmenter.segment(word)].map(({ segment }, index) => (\n                <span\n                  key={index}\n                  data-flip-character=\"\"\n                  onPointerEnter={(event) => {\n                    if (event.pointerType === \"mouse\") flip(event.currentTarget)\n                  }}\n                  style={{ display: \"inline-block\" }}\n                >\n                  {segment}\n                </span>\n              ))}\n            </span>\n          )\n        )}\n      </span>\n    </button>\n  )\n}\n",
      "type": "registry:ui",
      "target": "src/jbm/ui/flip-text.tsx"
    }
  ],
  "type": "registry:ui"
}