{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slope-chart",
  "type": "registry:ui",
  "title": "Slope chart",
  "description": "Before and after on two axes: lines draw in, rank moves sit beside each value, and switching datasets slides every line to its new slope.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/slope-chart/slope-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  useImperativeHandle,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type KeyboardEvent,\n  type PointerEvent,\n  type Ref,\n} from \"react\";\nimport { AnimatePresence, motion, useInView, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\nexport interface SlopeItem {\n  /** Stable identity. An item keeps its line across datasets, so switching morphs it. */\n  key: string;\n  /** Name beside the start value, in the tooltip, and in the table. */\n  label: string;\n  /** Value in the first period. */\n  start: number;\n  /** Value in the second period. */\n  end: number;\n}\n\n/** Use a slope chart to show how several items changed between exactly two moments, and how their order changed. */\nexport interface SlopeChartProps {\n  data: SlopeItem[];\n  /** What is measured, such as \"Conversion rate by channel\". Names the chart for assistive technology. */\n  label: string;\n  /** Heading of the first column, such as \"Q1\". */\n  startLabel: string;\n  /** Heading of the second column, such as \"Q2\". */\n  endLabel: string;\n  formatValue?: (value: number) => string;\n  /** Formats the change in the tooltip. Defaults to the difference in formatValue's terms. */\n  formatChange?: (change: number, item: SlopeItem) => string;\n  /** Plot height in pixels. Defaults to 44 pixels per item. */\n  height?: number;\n  /** The item drawn in the accent, such as the one the story is about. */\n  highlightKey?: string | null;\n  /** Controlled item in focus. The others fade while one is in focus. */\n  activeKey?: string | null;\n  onActiveChange?: (key: string | null) => void;\n  /** Rank movement beside each end value. */\n  ranks?: boolean;\n  emptyLabel?: string;\n  ref?: Ref<HTMLElement>;\n  className?: string;\n}\n\ntype Row = {\n  item: SlopeItem;\n  y0: number;\n  y1: number;\n  l0: number;\n  l1: number;\n  rank0: number;\n  rank1: number;\n  order: number;\n};\n\nconst { spring } = motionTokens;\nconst follow = { stiffness: 480, damping: 42, restDelta: 0.01 };\nconst LABEL_GAP = 20,\n  PAD = 14,\n  HEAD = 28;\nconst clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));\nconst grouped = new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 1 });\n\nconst subscribeNothing = () => () => {};\nfunction useReducedMotionSafe() {\n  const hydrated = useSyncExternalStore(\n    subscribeNothing,\n    () => true,\n    () => false,\n  );\n  return !!useReducedMotion() && hydrated;\n}\n\n/** Moves labels apart just enough to stop overlaps: touching labels form a group centred on where its members want to be. */\nfunction spread(wanted: number[], gap: number, low: number, high: number): number[] {\n  const order = wanted.map((y, index) => ({ y, index })).sort((a, b) => a.y - b.y);\n  let groups = order.map((entry) => ({ members: [entry], top: entry.y }));\n  for (let changed = true; changed;) {\n    changed = false;\n    const next: typeof groups = [];\n    for (const group of groups) {\n      const prev = next[next.length - 1];\n      if (prev && prev.top + prev.members.length * gap > group.top) {\n        const members = [...prev.members, ...group.members];\n        next[next.length - 1] = {\n          members,\n          top: members.reduce((sum, member, k) => sum + member.y - k * gap, 0) / members.length,\n        };\n        changed = true;\n      } else next.push(group);\n    }\n    groups = next.map((group) => ({\n      ...group,\n      top: clamp(group.top, low, Math.max(low, high - (group.members.length - 1) * gap)),\n    }));\n  }\n  const out = Array.from({ length: wanted.length }, () => 0);\n  groups.forEach((group) =>\n    group.members.forEach((member, k) => {\n      out[member.index] = group.top + k * gap;\n    }),\n  );\n  return out;\n}\n\n/** Rank change arrow: up, down, or level. The number rolls in when it changes. */\nfunction Rank({ move, reduced }: { move: number; reduced: boolean }) {\n  const text = move === 0 ? \"=\" : `${Math.abs(move)}`;\n  return (\n    <span\n      className={cn(\n        \"inline-flex min-w-[30px] items-center gap-0.5 text-(length:--text-xs) tabular-nums\",\n        move > 0 ? \"text-ink-2\" : \"text-ink-3\",\n      )}\n      data-move={move > 0 ? \"up\" : move < 0 ? \"down\" : \"level\"}\n      aria-hidden=\"true\"\n    >\n      {move !== 0 && (\n        <svg width=\"8\" height=\"8\" viewBox=\"0 0 8 8\">\n          <path\n            d={move > 0 ? \"M4 7V1M1.5 3.5 4 1l2.5 2.5\" : \"M4 1v6M1.5 4.5 4 7l2.5-2.5\"}\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.3\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      )}\n      <span className=\"relative inline-flex overflow-hidden\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={text}\n            initial={reduced ? { opacity: 0 } : { opacity: 0, y: move >= 0 ? 6 : -6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={reduced ? { opacity: 0 } : { opacity: 0, y: move >= 0 ? -6 : 6 }}\n            transition={reduced ? { duration: 0 } : spring.snappy}\n          >\n            {text}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </span>\n  );\n}\n\nexport function SlopeChart({\n  data,\n  label,\n  startLabel,\n  endLabel,\n  formatValue,\n  formatChange,\n  height,\n  highlightKey = null,\n  activeKey,\n  onActiveChange,\n  ranks = true,\n  emptyLabel = \"No data\",\n  ref,\n  className,\n}: SlopeChartProps) {\n  const reduced = useReducedMotionSafe();\n  const figure = useRef<HTMLElement>(null);\n  const stage = useRef<HTMLDivElement>(null);\n  const tip = useRef<HTMLDivElement>(null);\n  useImperativeHandle(ref, () => figure.current as HTMLElement);\n  const inView = useInView(figure, { once: true, amount: 0.3 });\n  const empty = data.length === 0;\n  const plotHeight = height ?? Math.max(200, data.length * 44);\n\n  const [width, setWidth] = useState(0);\n  useLayoutEffect(() => {\n    const node = stage.current;\n    if (!node) return;\n    const read = () => setWidth(node.clientWidth);\n    read();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n  const narrow = width < 440;\n  const leftRoom = narrow ? (width < 300 ? 116 : 132) : 168,\n    rightRoom = ranks ? (narrow ? 76 : 96) : narrow ? 52 : 64;\n  const x0 = leftRoom,\n    x1 = Math.max(x0 + 40, width - rightRoom);\n\n  // One shared value axis for both columns, padded so dots never touch the edges.\n  const values = data.flatMap((item) => [item.start, item.end]);\n  let lo = Math.min(...values),\n    hi = Math.max(...values);\n  if (!Number.isFinite(lo)) {\n    lo = 0;\n    hi = 1;\n  }\n  if (hi - lo < 1e-9) {\n    lo -= 1;\n    hi += 1;\n  }\n  const yOf = (value: number) => PAD + (1 - (value - lo) / (hi - lo)) * (plotHeight - PAD * 2);\n  const rankOf = (field: \"start\" | \"end\") => {\n    const sorted = [...data].sort((a, b) => b[field] - a[field]);\n    return (item: SlopeItem) => sorted.indexOf(item) + 1;\n  };\n  const r0 = rankOf(\"start\"),\n    r1 = rankOf(\"end\");\n  const l0 = spread(\n    data.map((item) => yOf(item.start)),\n    LABEL_GAP,\n    8,\n    plotHeight - 8,\n  );\n  const l1 = spread(\n    data.map((item) => yOf(item.end)),\n    LABEL_GAP,\n    8,\n    plotHeight - 8,\n  );\n  const rows: Row[] = data.map((item, i) => ({\n    item,\n    y0: yOf(item.start),\n    y1: yOf(item.end),\n    l0: l0[i]!,\n    l1: l1[i]!,\n    rank0: r0(item),\n    rank1: r1(item),\n    order: 0,\n  }));\n  const byEnd = [...rows].sort((a, b) => a.rank1 - b.rank1);\n  byEnd.forEach((row, k) => {\n    row.order = k;\n  });\n\n  const [ownActive, setOwnActive] = useState<string | null>(null);\n  const wanted = activeKey !== undefined ? activeKey : ownActive;\n  const active = wanted && data.some((item) => item.key === wanted) ? wanted : null;\n  const setActive = (key: string | null) => {\n    if (activeKey === undefined) setOwnActive(key);\n    if (key !== active) onActiveChange?.(key);\n  };\n\n  // The tooltip glides to the middle of the line in focus and stays inside the chart.\n  const tipX = useMotionValue(0),\n    tipY = useMotionValue(0);\n  const tipSpringX = useSpring(tipX, follow),\n    tipSpringY = useSpring(tipY, follow);\n  const wasShown = useRef(false);\n  const activeRow = rows.find((row) => row.item.key === active) ?? null;\n  // The tooltip keeps the last item's content while it fades out, so it never collapses to an empty box mid-fade.\n  const [shownKey, setShownKey] = useState<string | null>(null);\n  if (active && active !== shownKey) setShownKey(active);\n  const tipRow = activeRow ?? rows.find((row) => row.item.key === shownKey) ?? null;\n  useLayoutEffect(() => {\n    const bubble = tip.current;\n    if (!activeRow || !bubble || !width) {\n      wasShown.current = false;\n      return;\n    }\n    const tw = bubble.offsetWidth,\n      th = bubble.offsetHeight;\n    const mid = (activeRow.y0 + activeRow.y1) / 2,\n      cx = (x0 + x1) / 2;\n    const above = mid - th - 16 >= 0;\n    const left = clamp(cx - tw / 2, 0, Math.max(0, width - tw));\n    const top = clamp(HEAD + (above ? mid - th - 16 : mid + 16), 0, Math.max(0, plotHeight + HEAD - th));\n    tipX.set(left);\n    tipY.set(top);\n    if (!wasShown.current || reduced) {\n      tipSpringX.jump(left);\n      tipSpringY.jump(top);\n    }\n    wasShown.current = true;\n  });\n\n  // The line nearest the pointer, measured at the pointer's position along the plot.\n  const nearest = (clientX: number, clientY: number) => {\n    const rect = stage.current?.getBoundingClientRect();\n    if (!rect || empty) return null;\n    const x = clamp(clientX - rect.left, x0, x1),\n      y = clientY - rect.top - HEAD;\n    const t = (x - x0) / Math.max(1, x1 - x0);\n    let best: Row | null = null,\n      distance = Infinity;\n    for (const row of rows) {\n      const inLine = Math.abs(row.y0 + (row.y1 - row.y0) * t - y);\n      const onLabel =\n        clientX - rect.left < x0 ? Math.abs(row.l0 - y) : clientX - rect.left > x1 ? Math.abs(row.l1 - y) : Infinity;\n      const d = Math.min(inLine, onLabel);\n      if (d < distance) {\n        distance = d;\n        best = row;\n      }\n    }\n    return distance <= 22 ? (best?.item.key ?? null) : null;\n  };\n  const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    if (event.pointerType === \"mouse\" || event.buttons) setActive(nearest(event.clientX, event.clientY));\n  };\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (empty) return;\n    if (event.key === \"Escape\" && active) {\n      event.preventDefault();\n      setActive(null);\n      return;\n    }\n    const at = activeRow ? activeRow.order : -1;\n    const moves: Record<string, number> = {\n      ArrowDown: at + 1,\n      ArrowRight: at + 1,\n      ArrowUp: at < 0 ? 0 : at - 1,\n      ArrowLeft: at < 0 ? 0 : at - 1,\n      Home: 0,\n      End: byEnd.length - 1,\n    };\n    if (!(event.key in moves)) return;\n    event.preventDefault();\n    setActive(byEnd[clamp(moves[event.key]!, 0, byEnd.length - 1)]!.item.key);\n  };\n\n  const format = (value: number) => (formatValue ? formatValue(value) : grouped.format(value));\n  const change = (item: SlopeItem) =>\n    formatChange\n      ? formatChange(item.end - item.start, item)\n      : `${item.end >= item.start ? \"+\" : \"−\"}${format(Math.abs(item.end - item.start))}`;\n  const rankText = (row: Row) =>\n    row.rank0 === row.rank1\n      ? \"same rank\"\n      : row.rank1 < row.rank0\n        ? `up ${row.rank0 - row.rank1} to rank ${row.rank1}`\n        : `down ${row.rank1 - row.rank0} to rank ${row.rank1}`;\n  const announce = activeRow\n    ? `${activeRow.item.label}, ${startLabel} ${format(activeRow.item.start)}, ${endLabel} ${format(activeRow.item.end)}, ${change(activeRow.item)}, ${rankText(activeRow)}`\n    : \"\";\n  const summary = empty\n    ? `${label}. ${emptyLabel}.`\n    : `${label}, ${startLabel} to ${endLabel}. ${byEnd.map((row) => `${row.item.label} ${format(row.item.start)} to ${format(row.item.end)}`).join(\", \")}.`;\n  const settle = reduced ? { duration: 0 } : spring.morph;\n  const drawn = inView || reduced;\n\n  return (\n    <figure\n      ref={figure}\n      className={cn(\"relative m-0 grid min-w-0 font-body tracking-body text-ink\", className)}\n      aria-label={label}\n    >\n      {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the plot is a focusable chart widget explored by pointer and arrow keys */}\n      <div\n        ref={stage}\n        className=\"relative min-w-0 touch-pan-y outline-none select-none [-webkit-tap-highlight-color:transparent]\"\n        data-narrow={narrow || undefined}\n        data-active={active ? true : undefined}\n        role=\"group\"\n        tabIndex={empty ? -1 : 0}\n        aria-roledescription=\"slope chart\"\n        aria-label={`${label}. Use up and down arrows to move between items in ${endLabel} order.`}\n        onPointerMove={onPointerMove}\n        onPointerDown={(event) => {\n          if (event.pointerType !== \"mouse\") {\n            event.currentTarget.setPointerCapture?.(event.pointerId);\n            setActive(nearest(event.clientX, event.clientY));\n          }\n        }}\n        onPointerUp={(event) => {\n          if (event.pointerType !== \"mouse\") setActive(null);\n        }}\n        onPointerCancel={() => setActive(null)}\n        onPointerLeave={(event) => {\n          if (event.pointerType === \"mouse\") setActive(null);\n        }}\n        onKeyDown={onKeyDown}\n        onBlur={() => setActive(null)}\n        onFocus={(event) => {\n          if (event.currentTarget.matches(\":focus-visible\") && !empty && !active) setActive(byEnd[0]!.item.key);\n        }}\n      >\n        {width > 0 && (\n          <>\n            <div className=\"relative h-7\" aria-hidden=\"true\">\n              <span\n                className=\"absolute top-0 text-sm leading-[20px] whitespace-nowrap text-ink-3\"\n                style={{ left: x0, translate: \"-50% 0\" }}\n              >\n                {startLabel}\n              </span>\n              <span\n                className=\"absolute top-0 text-sm leading-[20px] whitespace-nowrap text-ink-3\"\n                style={{ left: x1, translate: \"-50% 0\" }}\n              >\n                {endLabel}\n              </span>\n            </div>\n            <div className=\"relative\" style={{ height: plotHeight }}>\n              <svg\n                className=\"block overflow-visible\"\n                width={width}\n                height={plotHeight}\n                aria-hidden=\"true\"\n                focusable=\"false\"\n              >\n                <line\n                  className=\"stroke-line [stroke-width:1] [shape-rendering:crispEdges]\"\n                  x1={x0}\n                  x2={x0}\n                  y1={0}\n                  y2={plotHeight}\n                />\n                <line\n                  className=\"stroke-line [stroke-width:1] [shape-rendering:crispEdges]\"\n                  x1={x1}\n                  x2={x1}\n                  y1={0}\n                  y2={plotHeight}\n                />\n                {rows.map((row, i) => {\n                  const key = row.item.key,\n                    on = active === key,\n                    accent = highlightKey === key;\n                  const delay = reduced ? 0 : i * 0.05;\n                  // Every line is one neutral ink; the story item takes the first series (derived from the accent) and the item in focus comes forward.\n                  return (\n                    <g\n                      key={key}\n                      className={cn(\n                        \"transition-opacity duration-standard ease-standard motion-reduce:transition-none\",\n                        accent\n                          ? \"[--line-ink:var(--series-1)]\"\n                          : on\n                            ? \"[--line-ink:var(--ink)]\"\n                            : \"[--line-ink:color-mix(in_oklab,var(--ink)_36%,var(--surface))]\",\n                        active !== null && !on && \"opacity-14\",\n                      )}\n                      data-on={on || undefined}\n                      data-accent={accent || undefined}\n                      data-dim={(active !== null && !on) || undefined}\n                    >\n                      <motion.path\n                        className=\"fill-none stroke-line-strong [stroke-width:1]\"\n                        initial={false}\n                        animate={{ d: `M${x0 - 6},${row.l0}L${x0 - 1},${row.y0}` }}\n                        transition={settle}\n                      />\n                      <motion.path\n                        className=\"fill-none stroke-line-strong [stroke-width:1]\"\n                        initial={false}\n                        animate={{ d: `M${x1 + 6},${row.l1}L${x1 + 1},${row.y1}` }}\n                        transition={settle}\n                      />\n                      <motion.line\n                        className=\"fill-none stroke-(--line-ink) [stroke-width:2] [stroke-linecap:round] transition-[stroke] duration-fast ease-standard motion-reduce:transition-none\"\n                        x1={x0}\n                        x2={x1}\n                        initial={reduced ? false : { y1: row.y0, y2: row.y1, pathLength: 0 }}\n                        animate={{ y1: row.y0, y2: row.y1, pathLength: drawn ? 1 : 0 }}\n                        transition={{\n                          y1: settle,\n                          y2: settle,\n                          pathLength: reduced\n                            ? { duration: 0 }\n                            : { duration: 0.7, ease: motionTokens.ease.inOut, delay: 0.1 + delay },\n                        }}\n                      />\n                      <motion.circle\n                        className=\"origin-center fill-(--line-ink) stroke-surface [stroke-width:2] [transform-box:fill-box] transition-[fill] duration-fast ease-standard motion-reduce:transition-none\"\n                        cx={x0}\n                        r={4}\n                        initial={reduced ? false : { cy: row.y0, scale: 0 }}\n                        animate={{ cy: row.y0, scale: drawn ? 1 : 0 }}\n                        transition={{ cy: settle, scale: reduced ? { duration: 0 } : { ...spring.snappy, delay } }}\n                      />\n                      <motion.circle\n                        className=\"origin-center fill-(--line-ink) stroke-surface [stroke-width:2] [transform-box:fill-box] transition-[fill] duration-fast ease-standard motion-reduce:transition-none\"\n                        cx={x1}\n                        r={4}\n                        initial={reduced ? false : { cy: row.y1, scale: 0 }}\n                        animate={{ cy: row.y1, scale: drawn ? 1 : 0 }}\n                        transition={{\n                          cy: settle,\n                          scale: reduced ? { duration: 0 } : { ...spring.snappy, delay: 0.7 + delay },\n                        }}\n                      />\n                    </g>\n                  );\n                })}\n              </svg>\n              {rows.map((row, i) => {\n                const key = row.item.key,\n                  on = active === key,\n                  accent = highlightKey === key;\n                const delay = reduced ? 0 : i * 0.05;\n                return (\n                  <div\n                    key={key}\n                    className={cn(\n                      \"transition-opacity duration-standard ease-standard motion-reduce:transition-none\",\n                      active !== null && !on && \"opacity-30\",\n                    )}\n                    data-on={on || undefined}\n                    data-accent={accent || undefined}\n                    data-dim={(active !== null && !on) || undefined}\n                    aria-hidden=\"true\"\n                  >\n                    <motion.span\n                      className={cn(\n                        \"pointer-events-none absolute top-0 -mt-[10px] flex h-5 items-center whitespace-nowrap max-w-full justify-end\",\n                        narrow\n                          ? \"gap-[6px] text-(length:--text-xs) leading-[20px]\"\n                          : \"gap-2 text-(length:--text-sm) leading-[20px]\",\n                      )}\n                      style={{ right: width - x0 + 10, maxWidth: x0 - 10 }}\n                      initial={reduced ? false : { y: row.l0, opacity: 0 }}\n                      animate={{ y: row.l0, opacity: drawn ? 1 : 0 }}\n                      transition={{ y: settle, opacity: { duration: 0.3, delay } }}\n                    >\n                      <span\n                        className={cn(\n                          \"min-w-0 overflow-hidden text-ellipsis\",\n                          on || accent ? \"text-ink\" : \"text-ink-2\",\n                        )}\n                      >\n                        {row.item.label}\n                      </span>\n                      <span className={cn(\"text-ink tabular-nums\", accent && \"font-medium\")}>\n                        {format(row.item.start)}\n                      </span>\n                    </motion.span>\n                    <motion.span\n                      className={cn(\n                        \"pointer-events-none absolute top-0 -mt-[10px] flex h-5 items-center whitespace-nowrap\",\n                        narrow\n                          ? \"gap-[6px] text-(length:--text-xs) leading-[20px]\"\n                          : \"gap-2 text-(length:--text-sm) leading-[20px]\",\n                      )}\n                      style={{ left: x1 + 10 }}\n                      initial={reduced ? false : { y: row.l1, opacity: 0 }}\n                      animate={{ y: row.l1, opacity: drawn ? 1 : 0 }}\n                      transition={{ y: settle, opacity: { duration: 0.3, delay: 0.7 + delay } }}\n                    >\n                      <span className={cn(\"text-ink tabular-nums\", accent && \"font-medium\")}>\n                        {format(row.item.end)}\n                      </span>\n                      {ranks && <Rank move={row.rank0 - row.rank1} reduced={reduced} />}\n                    </motion.span>\n                  </div>\n                );\n              })}\n            </div>\n          </>\n        )}\n        <motion.div\n          ref={tip}\n          className={cn(\n            \"pointer-events-none absolute top-0 left-0 z-2 grid max-w-[min(240px,100%)] min-w-[168px] gap-[3px] rounded-[14px] border border-line bg-surface px-3 py-[10px] shadow-floating [transition:opacity_var(--duration-fast)_var(--ease-standard),scale_var(--duration-fast)_var(--ease-standard)] motion-reduce:transition-none\",\n            active ? \"opacity-100 [scale:1]\" : \"opacity-0 [scale:.96]\",\n          )}\n          style={{ x: reduced ? tipX : tipSpringX, y: reduced ? tipY : tipSpringY }}\n          aria-hidden=\"true\"\n        >\n          <p className=\"m-0 mb-0.5 text-sm leading-body font-medium whitespace-nowrap text-ink\">\n            {tipRow?.item.label ?? \"\"}\n          </p>\n          <p className=\"m-0 flex items-center justify-between gap-3 text-sm leading-[1.3]\">\n            <span className=\"text-ink-2\">{startLabel}</span>\n            <span className=\"font-medium text-ink tabular-nums\">{tipRow ? format(tipRow.item.start) : \"\"}</span>\n          </p>\n          <p className=\"m-0 flex items-center justify-between gap-3 text-sm leading-[1.3]\">\n            <span className=\"text-ink-2\">{endLabel}</span>\n            <span className=\"font-medium text-ink tabular-nums\">{tipRow ? format(tipRow.item.end) : \"\"}</span>\n          </p>\n          <p className=\"mx-0 mt-0.5 mb-0 text-xs leading-body text-ink-3 tabular-nums\">\n            {tipRow ? `${change(tipRow.item)}, ${rankText(tipRow)}` : \"\"}\n          </p>\n        </motion.div>\n        {empty && (\n          <p className=\"m-0 grid min-h-40 place-items-center text-(length:--text-sm) text-ink-3\">{emptyLabel}</p>\n        )}\n      </div>\n      <p className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {announce}\n      </p>\n      <p className=\"sr-only\">{summary}</p>\n      {!empty && (\n        <div className=\"sr-only\">\n          <table>\n            <caption>{label}</caption>\n            <thead>\n              <tr>\n                <th scope=\"col\">Item</th>\n                <th scope=\"col\">{startLabel}</th>\n                <th scope=\"col\">{endLabel}</th>\n                <th scope=\"col\">Change</th>\n                <th scope=\"col\">Rank</th>\n              </tr>\n            </thead>\n            <tbody>\n              {byEnd.map((row) => (\n                <tr key={row.item.key}>\n                  <th scope=\"row\">{row.item.label}</th>\n                  <td>{format(row.item.start)}</td>\n                  <td>{format(row.item.end)}</td>\n                  <td>{change(row.item)}</td>\n                  <td>\n                    {row.rank0} to {row.rank1}\n                  </td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      )}\n    </figure>\n  );\n}\n\nexport default SlopeChart;\n",
      "type": "registry:component",
      "target": "@components/iris/slope-chart/slope-chart.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/slope-chart",
  "meta": {
    "kind": "component",
    "group": "Charts",
    "export": "SlopeChart",
    "tags": [
      "data",
      "chart",
      "new"
    ],
    "docs": "https://irisui.dev/components/slope-chart"
  }
}
