{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "release-pill",
  "type": "registry:ui",
  "title": "Release Pill",
  "description": "The \"New\" badge above your headline, made useful. Hover it and the pill grows into a small changelog with your last few releases, then folds back into the pill when you leave.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "badge",
    "hover",
    "changelog"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/release-pill",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/release-pill.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - Every frame runs on motion values (grow, reveal, the line width), so opening, folding and\n *   cycling never re-render the pill.\n * - One shape grows into the card and folds back over the page; nothing below it is pushed down.\n * - Rows open top first and tuck back last first, each from under the row above (its z-index).\n * - Opening waits for hover intent and leaving waits before it folds; a sweep across does nothing.\n * - Reduced motion skips growing, sliding and rolling: the shape jumps and rows only fade.\n */\nconst TIMING = {\n  /** The shape growing into the card: width, height and corners. */\n  grow: { type: \"spring\", visualDuration: 0.35, bounce: 0.08 },\n  /** The card folding back into the pill, and rows tucking in. */\n  fold: { type: \"spring\", visualDuration: 0.3, bounce: 0 },\n  /** A row sliding out from under the one above. */\n  row: { type: \"spring\", visualDuration: 0.32, bounce: 0.06 },\n  /** The title's slot following a cycle to its new width. */\n  slot: { type: \"spring\", visualDuration: 0.4, bounce: 0 },\n  /** A cycled title sliding up. */\n  slide: { duration: 0.4, ease: [0.22, 1, 0.36, 1] },\n  /** A digit's wheel rolling to the new version. */\n  roll: { type: \"spring\", visualDuration: 0.7, bounce: 0.05 },\n  /** Fades under reduced motion. */\n  fade: { duration: 0.15, ease: \"easeOut\" },\n  /** One breath of the dot, in seconds, and its curve. */\n  breathe: { duration: 3.2, ease: \"easeInOut\" },\n  /** Hover intent before it opens, in seconds (the `hoverDelay` prop). */\n  hoverDelay: 0.12,\n  /** Outside the pill before it folds, in ms. */\n  leave: 250,\n  /** How fast it grows and folds, as a multiple of the pace below (the `speed` prop). */\n  speed: 1,\n  // At the default pace; `speed` divides these, the springs above and the rows' fade.\n  /** Between rows as they slide out, in seconds (the `stagger` prop). */\n  stagger: 0.04,\n  /** Between rows as they tuck back in, last first, in seconds. */\n  tuck: 0.025,\n  /** The shape leading the first row, in seconds. */\n  lead: 0.05,\n  /** The corners trailing the height as it folds, in seconds. */\n  trail: 0.04,\n  /** A row's fade, in seconds. */\n  rowFade: 0.2,\n  /** Each release in a cycling pill, in ms. */\n  cycle: 4000,\n  /** After it first shows, before a peek, in ms. */\n  peekAfter: 2000,\n  /** How long a peek stays open, in seconds (the `peekDuration` prop). */\n  peekDuration: 1.1,\n  /** After it shows, before the version rolls, in seconds. */\n  rollDelay: 0.3,\n  /** Between neighboring digits of the version, in seconds. */\n  rollStagger: 0.07,\n  /** A row's arrow nudging on hover, as a Tailwind class. */\n  nudge: \"duration-200\",\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * RELEASE PILL STORYBOARD\n *\n *  rest     a pill: a small tag, the latest release's title, an\n *           arrow and a dot that breathes\n *  view     the first time it scrolls into view the version rolls\n *           digit by digit, from the previous release to the latest\n *           (v2.3 → v2.4), the rightmost digit first\n *  intent   the pointer rests on it for ~120ms and it opens; sweeping\n *           across does nothing. Leaving waits ~250ms before it folds\n *  grow     one shape on one spring (~0.35s, the smallest overshoot):\n *           width, height and corners ease from the pill to a card\n *           that floats over what's below, so nothing is pushed down.\n *           The header row stays put: its arrow and dot give way to\n *           the date, and the summary opens under it\n *  rows     the rows slide out from under the one above, 40ms apart,\n *           each rising from 0.96 scale. Closing tucks them back in\n *           reverse, last row first, as the card shrinks to the pill\n *  peek     with peek, ~2s after it first shows it opens just far\n *           enough to reveal the second row, holds, and closes. Once\n *           per page view, and a pointer cancels it\n *  cycle    with cycle, the closed pill steps through the latest few\n *           releases every 4s: the title slides up as the next one\n *           arrives and the pill's width follows. A pointer stops it,\n *           and opening brings the latest back\n *  click    a click on the header toggles it; a click on a pill the\n *           pointer already opened pins it, so it stays open when the\n *           pointer leaves. A tap toggles\n *  keys     Enter and Space toggle, Tab walks the rows, Esc closes and\n *           returns focus to the pill, and tabbing out folds it\n *  reduced  no growing, sliding or rolling: the pill resizes at once\n *           and the rows fade in and out\n * ───────────────────────────────────────────────────────── */\n\nconst SHAPE = {\n  height: 36, // px; the closed pill\n  radius: 16, // px; the open card's corners (a 36px pill starts fully round, at 18)\n  rise: 10, // px a hidden row sits above its place\n  scale: 0.96, // scale a hidden row starts from\n  peekMargin: 8, // px below the second row a peek shows\n  cycleCount: 3, // releases a cycling pill steps through\n} as const;\n\ntype Phase = \"closed\" | \"peek\" | \"open\";\n\ntype Spring = { type: \"spring\"; visualDuration: number; bounce: number };\n\n/** A spring at `speed` times its pace. */\nconst paced = (spring: Spring, speed: number): Spring => ({\n  ...spring,\n  visualDuration: spring.visualDuration / speed,\n});\n\nexport interface Release {\n  /** Shown in monospace before the title, like \"v2.4\" or \"Day 3\". Digits roll from the previous release's version. */\n  version: string;\n  title: string;\n  /** Shown at the end of the row, like \"Oct 10\". */\n  date?: string;\n  /** Where the release's row links to. */\n  href?: string;\n  /** One line under the latest release's title when the pill is open. */\n  summary?: string;\n  /** Anything small shown with the release in the open card: a Feature or Fix tag. */\n  label?: React.ReactNode;\n}\n\nexport interface ReleasePillProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** The releases, newest first. The first one is the latest. */\n  releases: Release[];\n  /** The small tag at the start of the pill. */\n  tag?: string;\n  /** Adds a \"View changelog →\" link at the bottom of the open card. */\n  changelogHref?: string;\n  /** Open just far enough to show the second row, once, about 2s after the pill first shows. */\n  peek?: boolean;\n  /** Step the closed pill through the latest releases every 4s. A number sets how many; true is 3. */\n  cycle?: boolean | number;\n  /** Where the card grows from: out of the pill's middle, or out of its left edge. */\n  align?: \"center\" | \"start\";\n  /** How fast the pill grows into the card and folds back, as a multiple of the default pace. */\n  speed?: number;\n  /** Seconds between rows as they slide out. */\n  stagger?: number;\n  /** Seconds the pointer rests on the pill before it opens. */\n  hoverDelay?: number;\n  /** Seconds a `peek` stays open before it closes. */\n  peekDuration?: number;\n  /** An extra row at the end of the list, after the releases. */\n  children?: React.ReactNode;\n}\n\nconst isDigit = (char: string) => char.length === 1 && char >= \"0\" && char <= \"9\";\n\n/* ── VersionRoll ────────────────────────────────────────── */\n\n// Each changing character sits in a one-character window on a short strip. A digit's strip\n// counts up to its new value (3 → 4, or 4 → 0 the long way round); anything else swaps.\nfunction VersionRoll({\n  from,\n  to,\n  go,\n  reduced,\n}: {\n  from: string;\n  to: string;\n  go: boolean;\n  reduced: boolean;\n}) {\n  const length = Math.max(from.length, to.length);\n  const at = (text: string, i: number) => text[i - (length - text.length)] ?? \"\";\n  const cells = Array.from({ length }, (_, i) => ({ prev: at(from, i), next: at(to, i) }));\n  const delays: number[] = [];\n  let order = 0;\n  for (let i = length - 1; i >= 0; i--) {\n    if (cells[i].prev !== cells[i].next) delays[i] = TIMING.rollDelay + order++ * TIMING.rollStagger;\n  }\n  const width = (char: string) => (char ? \"1ch\" : \"0ch\");\n\n  return (\n    <>\n      <span className=\"sr-only\">{to}</span>\n      <span aria-hidden className=\"inline-flex\">\n        {cells.map(({ prev, next }, i) => {\n          if (prev === next) return <span key={i} className=\"whitespace-pre\">{next}</span>;\n          const strip =\n            isDigit(prev) && isDigit(next)\n              ? Array.from({ length: ((Number(next) - Number(prev) + 10) % 10) + 1 }, (_, k) =>\n                  String((Number(prev) + k) % 10),\n                )\n              : [prev, next];\n          const transition = reduced ? { duration: 0 } : { ...TIMING.roll, delay: delays[i] };\n          return (\n            <motion.span\n              key={i}\n              className=\"relative inline-block h-5 overflow-hidden\"\n              initial={false}\n              animate={{ width: go ? width(next) : width(prev) }}\n              transition={transition}\n            >\n              <motion.span\n                className=\"flex flex-col\"\n                initial={false}\n                animate={{ y: go ? `${(-100 * (strip.length - 1)) / strip.length}%` : \"0%\" }}\n                transition={transition}\n              >\n                {strip.map((char, k) => (\n                  <span key={k} className=\"block h-5 text-center whitespace-pre\">\n                    {char}\n                  </span>\n                ))}\n              </motion.span>\n            </motion.span>\n          );\n        })}\n      </span>\n    </>\n  );\n}\n\n/* ── Row ────────────────────────────────────────────────── */\n\n// One row of the list. It slides out from under the row above (which sits higher in the\n// stack), and tucks back in last row first.\nfunction Row({\n  index,\n  count,\n  phase,\n  reduced,\n  speed,\n  stagger,\n  className,\n  children,\n}: {\n  index: number;\n  count: number;\n  phase: Phase;\n  reduced: boolean;\n  speed: number;\n  stagger: number;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const shown = phase === \"open\" || (phase === \"peek\" && index < 2);\n  const delay = shown ? TIMING.lead / speed + index * stagger : ((count - 1 - index) * TIMING.tuck) / speed;\n  // Reduced motion fades; the scale and rise wait until the row has faded out.\n  const transition = reduced\n    ? { default: { duration: 0, delay: shown ? 0 : TIMING.fade.duration }, opacity: TIMING.fade }\n    : {\n        default: { ...paced(shown ? TIMING.row : TIMING.fold, speed), delay },\n        opacity: { duration: TIMING.rowFade / speed, delay },\n      };\n  return (\n    <motion.li\n      className={cn(\"relative bg-background\", className)}\n      style={{ zIndex: count - index, transformOrigin: \"50% 0%\" }}\n      initial={false}\n      animate={\n        shown\n          ? { opacity: 1, scale: 1, y: 0 }\n          : { opacity: 0, scale: SHAPE.scale, y: -SHAPE.rise }\n      }\n      transition={transition}\n    >\n      {children}\n    </motion.li>\n  );\n}\n\n/* ── Face ───────────────────────────────────────────────── */\n\n// The closed pill's contents: tag, a slot for the release, arrow, dot. It's drawn twice, once\n// invisibly in the flow to give the pill its size, and once in the shape.\nfunction Face({\n  tag,\n  slotRef,\n  slotStyle,\n  trailingStyle,\n  reduced,\n  children,\n}: {\n  tag: string;\n  slotRef?: React.Ref<HTMLSpanElement>;\n  slotStyle?: React.ComponentProps<typeof motion.span>[\"style\"];\n  trailingStyle?: React.ComponentProps<typeof motion.span>[\"style\"];\n  reduced: boolean;\n  children: React.ReactNode;\n}) {\n  return (\n    <span className=\"flex h-9 w-max shrink-0 items-center gap-2 pr-3.5 pl-1.5 text-[13px] whitespace-nowrap\">\n      <span className=\"inline-flex h-6 items-center rounded-full bg-foreground px-2.5 text-[11px] font-semibold tracking-wide text-background\">\n        {tag}\n      </span>\n      <motion.span\n        ref={slotRef}\n        className=\"grid h-5 overflow-hidden text-left leading-5\"\n        style={slotStyle}\n      >\n        {children}\n      </motion.span>\n      <motion.span className=\"flex items-center gap-2\" style={trailingStyle}>\n        <span aria-hidden className=\"text-muted-foreground\">\n          →\n        </span>\n        <span aria-hidden className=\"relative flex size-2 items-center justify-center\">\n          <motion.span\n            className=\"absolute inset-0 rounded-full bg-emerald-500\"\n            animate={{ opacity: [1, 0.35, 1], scale: [1, 0.7, 1] }}\n            transition={\n              reduced\n                ? { duration: 0 }\n                : { ...TIMING.breathe, repeat: Infinity }\n            }\n          />\n        </span>\n      </motion.span>\n    </span>\n  );\n}\n\n/* ── ReleasePill ────────────────────────────────────────── */\n\n/**\n * The \"New\" badge above a headline, made useful. Hover it and the pill grows into a small\n * changelog with the latest releases; leave and it folds back into the pill.\n */\nexport function ReleasePill({\n  releases,\n  tag = \"New\",\n  changelogHref,\n  peek = false,\n  cycle = false,\n  align = \"center\",\n  speed = TIMING.speed,\n  stagger = TIMING.stagger,\n  hoverDelay = TIMING.hoverDelay,\n  peekDuration = TIMING.peekDuration,\n  className,\n  children,\n  onPointerEnter,\n  onPointerLeave,\n  onPointerDown,\n  onFocus,\n  onBlur,\n  onKeyDown,\n  ...props\n}: ReleasePillProps) {\n  const H: number = SHAPE.height;\n  const reduced = useReducedMotion() ?? false;\n  const id = React.useId();\n  const listId = `${id}-releases`;\n\n  const latest = releases[0];\n  const previous = releases.slice(1);\n  const cycleCount = cycle\n    ? Math.min(releases.length, typeof cycle === \"number\" ? Math.max(1, cycle) : SHAPE.cycleCount)\n    : 1;\n\n  const [phase, setPhase] = React.useState<Phase>(\"closed\");\n  const [index, setIndex] = React.useState(0);\n  const [inside, setInside] = React.useState(false);\n  const [focused, setFocused] = React.useState(false);\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const sizerRef = React.useRef<HTMLSpanElement>(null);\n  const ghostSlotRef = React.useRef<HTMLSpanElement>(null);\n  const listRef = React.useRef<HTMLUListElement>(null);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n\n  const inView = useInView(rootRef, { once: true, amount: 0.6 });\n\n  // Everything that moves is a motion value, so a frame costs no React render.\n  const grow = useMotionValue(0); // 0 pill, 1 card: width, corners, the look\n  const reveal = useMotionValue(0); // 0 pill, 1 card: height\n  const lineWidth = useMotionValue(0); // the width of the release now in the pill\n  const width = useMotionValue<number | string>(\"max-content\");\n  const slotWidth = useMotionValue<number | string>(\"auto\");\n  const height = useMotionValue(H);\n  const radius = useMotionValue(H / 2);\n  const trailing = useTransform(grow, [0, 0.35], [1, 0]);\n  const detail = useTransform(grow, [0.4, 0.9], [0, 1]);\n  const tint = useTransform(grow, [0, 1], [1, 0]);\n\n  const sizes = React.useRef({\n    ready: false,\n    chrome: 0,\n    lines: [] as number[],\n    cardW: 0,\n    cardH: H,\n    peekH: H,\n  });\n  const live = React.useRef({\n    inside: false,\n    focus: false,\n    pinned: false,\n    kind: \"mouse\",\n    intent: 0,\n    leave: 0,\n    peek: 0,\n    peeked: false,\n  });\n  const latestState = React.useRef({ phase, index });\n  React.useLayoutEffect(() => {\n    latestState.current = { phase, index };\n  });\n\n  // The shape's size from its three progress values.\n  const sync = React.useCallback(() => {\n    const s = sizes.current;\n    if (!s.ready) return;\n    const g = grow.get();\n    const closed = s.chrome + lineWidth.get();\n    slotWidth.set(lineWidth.get());\n    width.set(closed + (s.cardW - closed) * g);\n    height.set(H + (s.cardH - H) * reveal.get());\n    radius.set(Math.max(0, H / 2 + (SHAPE.radius - H / 2) * g));\n  }, [H, grow, reveal, lineWidth, slotWidth, width, height, radius]);\n\n  const measure = React.useCallback(() => {\n    const sizer = sizerRef.current;\n    const slot = ghostSlotRef.current;\n    const list = listRef.current;\n    if (!sizer || !slot || !list) return;\n    const s = sizes.current;\n    s.chrome = sizer.getBoundingClientRect().width - slot.getBoundingClientRect().width;\n    s.lines = Array.from(slot.children, (line) => line.getBoundingClientRect().width);\n    s.cardW = list.offsetWidth;\n    s.cardH = H + list.offsetHeight;\n    const second = list.children[Math.min(1, list.children.length - 1)] as HTMLElement | undefined;\n    s.peekH = second\n      ? Math.min(s.cardH, H + second.offsetTop + second.offsetHeight + SHAPE.peekMargin)\n      : s.cardH;\n    s.ready = true;\n    lineWidth.set(s.lines[latestState.current.index] ?? s.lines[0] ?? 0);\n    sync();\n  }, [H, lineWidth, sync]);\n\n  React.useLayoutEffect(() => {\n    measure();\n    const observer = new ResizeObserver(measure);\n    if (sizerRef.current) observer.observe(sizerRef.current);\n    if (listRef.current) observer.observe(listRef.current);\n    return () => observer.disconnect();\n  }, [measure]);\n\n  React.useEffect(() => {\n    const stops = [grow, reveal, lineWidth].map((value) => value.on(\"change\", sync));\n    return () => stops.forEach((stop) => stop());\n  }, [grow, reveal, lineWidth, sync]);\n\n  /* Open, peek, close. */\n  React.useEffect(() => {\n    const s = sizes.current;\n    const open = phase !== \"closed\";\n    const target =\n      phase === \"open\"\n        ? 1\n        : phase === \"peek\"\n          ? Math.min(1, (s.peekH - H) / Math.max(1, s.cardH - H))\n          : 0;\n    if (reduced) {\n      grow.jump(open ? 1 : 0);\n      reveal.jump(target);\n      return;\n    }\n    const controls = [\n      animate(\n        grow,\n        open ? 1 : 0,\n        open ? paced(TIMING.grow, speed) : { ...paced(TIMING.fold, speed), delay: TIMING.trail / speed },\n      ),\n      animate(reveal, target, paced(open ? TIMING.grow : TIMING.fold, speed)),\n    ];\n    return () => controls.forEach((control) => control.stop());\n  }, [phase, reduced, H, grow, reveal, speed]);\n\n  /* The pill's width follows the release now showing. */\n  React.useEffect(() => {\n    const s = sizes.current;\n    const w = s.lines[index];\n    if (!s.ready || w === undefined) return;\n    if (reduced) {\n      lineWidth.jump(w);\n      return;\n    }\n    const control = animate(lineWidth, w, TIMING.slot);\n    return () => control.stop();\n  }, [index, reduced, lineWidth]);\n\n  const clearTimers = React.useCallback(() => {\n    const L = live.current;\n    window.clearTimeout(L.intent);\n    window.clearTimeout(L.leave);\n    window.clearTimeout(L.peek);\n  }, []);\n  React.useEffect(() => clearTimers, [clearTimers]);\n\n  const openNow = React.useCallback(() => {\n    clearTimers();\n    live.current.peeked = true;\n    setIndex(0); // a cycling pill returns to the latest release as it opens\n    setPhase(\"open\");\n  }, [clearTimers]);\n\n  const closeNow = React.useCallback(() => {\n    clearTimers();\n    live.current.pinned = false;\n    setPhase(\"closed\");\n  }, [clearTimers]);\n\n  const scheduleClose = React.useCallback(() => {\n    const L = live.current;\n    window.clearTimeout(L.leave);\n    L.leave = window.setTimeout(() => {\n      if (!L.inside && !L.focus) closeNow();\n    }, TIMING.leave);\n  }, [closeNow]);\n\n  /* Pointer: hover intent for mice. Taps and clicks go through the header. */\n  const handlePointerEnter = (event: React.PointerEvent<HTMLDivElement>) => {\n    onPointerEnter?.(event);\n    if (event.pointerType !== \"mouse\") return;\n    const L = live.current;\n    L.inside = true;\n    L.peeked = true; // a pointer has found it; the hint is no longer needed\n    setInside(true);\n    window.clearTimeout(L.leave);\n    window.clearTimeout(L.peek);\n    if (latestState.current.phase !== \"open\") {\n      window.clearTimeout(L.intent);\n      L.intent = window.setTimeout(openNow, hoverDelay * 1000);\n    }\n  };\n\n  const handlePointerLeave = (event: React.PointerEvent<HTMLDivElement>) => {\n    onPointerLeave?.(event);\n    if (event.pointerType !== \"mouse\") return;\n    const L = live.current;\n    L.inside = false;\n    setInside(false);\n    window.clearTimeout(L.intent);\n    if (latestState.current.phase === \"open\" && !L.pinned) scheduleClose();\n  };\n\n  const onTriggerClick = (event: React.MouseEvent) => {\n    const L = live.current;\n    const byMouse = event.detail > 0 && L.kind === \"mouse\";\n    if (latestState.current.phase !== \"open\") {\n      openNow();\n      L.pinned = true;\n    } else if (byMouse && !L.pinned) {\n      L.pinned = true; // the pointer opened it; a click keeps it open\n    } else {\n      closeNow();\n    }\n  };\n\n  // A click or tap outside folds it, pinned or not.\n  React.useEffect(() => {\n    if (phase !== \"open\") return;\n    const onDown = (event: PointerEvent) => {\n      if (!rootRef.current?.contains(event.target as Node)) closeNow();\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [phase, closeNow]);\n\n  /* Peek: once, a couple of seconds after it first shows. */\n  React.useEffect(() => {\n    if (!peek || !inView || reduced) return;\n    const L = live.current;\n    if (L.peeked) return;\n    const start = window.setTimeout(() => {\n      if (L.peeked || latestState.current.phase !== \"closed\" || L.inside || L.focus) return;\n      L.peeked = true;\n      setPhase(\"peek\");\n      L.peek = window.setTimeout(\n        () => setPhase((current) => (current === \"peek\" ? \"closed\" : current)),\n        peekDuration * 1000,\n      );\n    }, TIMING.peekAfter);\n    return () => window.clearTimeout(start);\n  }, [peek, inView, reduced, peekDuration]);\n\n  /* Cycle: step through the latest releases until a pointer or focus arrives. */\n  const engaged = inside || focused || phase !== \"closed\";\n  React.useEffect(() => {\n    if (cycleCount < 2 || !inView || engaged) return;\n    const timer = window.setTimeout(() => setIndex((i) => (i + 1) % cycleCount), TIMING.cycle);\n    return () => window.clearTimeout(timer);\n  }, [cycleCount, inView, engaged, index]);\n\n  if (!latest) return null;\n\n  const current = releases[Math.min(index, releases.length - 1)];\n  const rollFrom = previous[0]?.version;\n  const rolling = index === 0 && rollFrom !== undefined && rollFrom !== latest.version;\n  const open = phase === \"open\";\n  const rowCount = 1 + previous.length + (children ? 1 : 0) + (changelogHref ? 1 : 0);\n\n  // A release in the pill. Only the live copy of the latest release rolls its version.\n  const line = (release: Release, roll = false) => (\n    <span className=\"flex items-center gap-1.5\">\n      <span className=\"font-mono text-xs text-muted-foreground tabular-nums\">\n        {roll && release === latest && rolling ? (\n          <VersionRoll from={rollFrom} to={latest.version} go={inView} reduced={reduced} />\n        ) : (\n          release.version\n        )}\n      </span>\n      <span className=\"font-medium\">{release.title}</span>\n    </span>\n  );\n\n  const pillAnchor = align === \"center\" ? \"left-1/2 -translate-x-1/2\" : \"left-0\";\n  const shapeStyle = { width, height, borderRadius: radius };\n\n  return (\n    <div\n      {...props}\n      ref={rootRef}\n      data-state={phase}\n      className={cn(\"pointer-events-none relative z-10 inline-flex h-9 align-middle\", className)}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      onPointerDown={(event) => {\n        onPointerDown?.(event);\n        live.current.kind = event.pointerType;\n      }}\n      onFocus={(event) => {\n        onFocus?.(event);\n        if (event.target.matches(\":focus-visible\")) {\n          live.current.focus = true;\n          setFocused(true);\n        }\n      }}\n      onBlur={(event) => {\n        onBlur?.(event);\n        if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n        live.current.focus = false;\n        setFocused(false);\n        if (latestState.current.phase === \"open\") scheduleClose();\n      }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        if (event.key !== \"Escape\" || latestState.current.phase === \"closed\") return;\n        event.preventDefault();\n        closeNow();\n        triggerRef.current?.focus();\n      }}\n    >\n      {/* Gives the pill its place in the page. The widest release sets its width, so a cycling pill never reflows what's beside it. */}\n      <span ref={sizerRef} aria-hidden className=\"invisible inline-flex\">\n        <Face tag={tag} slotRef={ghostSlotRef} reduced>\n          {releases.slice(0, cycleCount).map((release, i) => (\n            <span key={i} className=\"col-start-1 row-start-1 w-max justify-self-start\">\n              {line(release)}\n            </span>\n          ))}\n        </Face>\n      </span>\n\n      <motion.div\n        aria-hidden\n        className={cn(\n          \"absolute top-0 shadow-[0_18px_44px_-14px_rgb(0_0_0/0.28),0_4px_14px_-6px_rgb(0_0_0/0.14)] dark:shadow-[0_18px_44px_-14px_rgb(0_0_0/0.8)]\",\n          pillAnchor,\n        )}\n        style={{ ...shapeStyle, opacity: grow }}\n      />\n\n      <motion.div\n        className={cn(\n          \"pointer-events-auto absolute top-0 overflow-hidden bg-background text-foreground ring-1 ring-foreground/10\",\n          \"has-[[data-release-pill-trigger]:focus-visible]:outline-2 has-[[data-release-pill-trigger]:focus-visible]:outline-offset-2 has-[[data-release-pill-trigger]:focus-visible]:outline-foreground/70\",\n          pillAnchor,\n        )}\n        style={shapeStyle}\n      >\n        <motion.div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-x-0 top-0 h-full bg-foreground/[0.045]\"\n          style={{ opacity: tint }}\n        />\n\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          data-release-pill-trigger=\"\"\n          aria-expanded={open}\n          aria-controls={listId}\n          onClick={onTriggerClick}\n          className=\"relative flex h-9 w-full cursor-pointer items-center text-left outline-none [-webkit-tap-highlight-color:transparent]\"\n        >\n          <Face\n            tag={tag}\n            slotStyle={{ width: slotWidth }}\n            trailingStyle={{ opacity: trailing }}\n            reduced={reduced}\n          >\n            <AnimatePresence initial={false}>\n              <motion.span\n                key={index}\n                className=\"col-start-1 row-start-1 w-max justify-self-start\"\n                initial={reduced ? { opacity: 0 } : { y: \"100%\", opacity: 0 }}\n                animate={{ y: 0, opacity: 1 }}\n                exit={reduced ? { opacity: 0 } : { y: \"-100%\", opacity: 0 }}\n                transition={reduced ? TIMING.fade : TIMING.slide}\n              >\n                {line(current, true)}\n              </motion.span>\n            </AnimatePresence>\n          </Face>\n          {latest.date ? (\n            <motion.span\n              aria-hidden={!open}\n              className=\"absolute right-3.5 text-xs text-muted-foreground tabular-nums\"\n              style={{ opacity: detail }}\n            >\n              {latest.date}\n            </motion.span>\n          ) : null}\n        </button>\n\n        <ul\n          ref={listRef}\n          id={listId}\n          inert={!open}\n          className=\"absolute top-9 left-0 w-[min(var(--release-pill-width,22rem),calc(100vw-2rem))] text-left text-[13px]\"\n        >\n          <Row\n            index={0}\n            count={rowCount}\n            phase={phase}\n            reduced={reduced}\n            speed={speed}\n            stagger={stagger}\n          >\n            <a\n              href={latest.href ?? \"#\"}\n              className=\"group/row block px-3.5 pb-3 leading-5 text-muted-foreground outline-none hover:text-foreground focus-visible:bg-foreground/[0.06] focus-visible:text-foreground\"\n            >\n              {latest.label ? <span className=\"mr-2 align-middle\">{latest.label}</span> : null}\n              {latest.summary ?? \"Read the release notes\"}\n              <span\n                aria-hidden\n                className={`ml-1 inline-block transition-transform ${TIMING.nudge} group-hover/row:translate-x-0.5`}\n              >\n                →\n              </span>\n            </a>\n          </Row>\n\n          {previous.map((release, i) => (\n            <Row\n              key={i}\n              index={i + 1}\n              count={rowCount}\n              phase={phase}\n              reduced={reduced}\n              speed={speed}\n              stagger={stagger}\n              className={i === 0 ? \"border-t border-foreground/10\" : undefined}\n            >\n              <a\n                href={release.href ?? \"#\"}\n                className=\"flex items-baseline gap-3 px-3.5 py-2 outline-none hover:bg-foreground/[0.05] focus-visible:bg-foreground/[0.06]\"\n              >\n                <span className=\"w-12 shrink-0 font-mono text-xs text-muted-foreground tabular-nums\">\n                  {release.version}\n                </span>\n                <span className=\"min-w-0 flex-1 truncate font-medium\">{release.title}</span>\n                {release.label}\n                {release.date ? (\n                  <span className=\"shrink-0 text-xs text-muted-foreground tabular-nums\">\n                    {release.date}\n                  </span>\n                ) : null}\n              </a>\n            </Row>\n          ))}\n\n          {children ? (\n            <Row\n              index={1 + previous.length}\n              count={rowCount}\n              phase={phase}\n              reduced={reduced}\n              speed={speed}\n              stagger={stagger}\n              className=\"border-t border-foreground/10 px-3.5 py-2.5\"\n            >\n              {children}\n            </Row>\n          ) : null}\n\n          {changelogHref ? (\n            <Row\n              index={rowCount - 1}\n              count={rowCount}\n              phase={phase}\n              reduced={reduced}\n              speed={speed}\n              stagger={stagger}\n              className=\"border-t border-foreground/10\"\n            >\n              <a\n                href={changelogHref}\n                className=\"group/row flex items-center justify-between px-3.5 py-2.5 font-medium outline-none hover:bg-foreground/[0.05] focus-visible:bg-foreground/[0.06]\"\n              >\n                View changelog\n                <span\n                  aria-hidden\n                  className={`transition-transform ${TIMING.nudge} group-hover/row:translate-x-0.5`}\n                >\n                  →\n                </span>\n              </a>\n            </Row>\n          ) : null}\n        </ul>\n      </motion.div>\n    </div>\n  );\n}\n"
    }
  ]
}