{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waffle-chart",
  "type": "registry:ui",
  "title": "Waffle chart",
  "description": "A ten by ten unit chart where every cell is one percent, and cells fly to their new group when the data changes.",
  "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/waffle-chart/waffle-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useImperativeHandle,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent,\n  type Ref,\n} from \"react\";\nimport { animate, motion, useInView, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\nexport interface WaffleCategory {\n  /** Stable identity. Cells keep following their category when the data changes, so a new dataset flows instead of repainting. */\n  key: string;\n  /** Name in the legend, the tooltip, and the data table. */\n  label: string;\n  /** Raw amount. Shares are computed from the total; negatives count as zero. */\n  value: number;\n  /** Any CSS color. Defaults to the first series for accentKey, the supporting series for the next three, then neutral steps. */\n  color?: string;\n}\n\n/** A unit chart: every cell is one share of the whole, so parts of a total read as countable squares. */\nexport interface WaffleChartProps {\n  /** Categories in fill order. The first fills from the bottom-left corner, column by column. */\n  data: WaffleCategory[];\n  /** What the whole is, such as \"Electricity generation, 2023\". Names the chart for assistive technology. */\n  label: string;\n  /** Unit after each raw value in the tooltip and table, such as \"TWh\". */\n  unit?: string;\n  formatValue?: (value: number, category: WaffleCategory) => string;\n  /** Grid rows. rows × columns cells make the whole; 10 × 10 means one cell per percent. */\n  rows?: number;\n  columns?: number;\n  /** The category painted in the accent. Pass null to keep every category neutral. Defaults to the first category. */\n  accentKey?: string | null;\n  /** Controlled focused category. Others dim while one is focused. */\n  activeKey?: string | null;\n  defaultActiveKey?: string | null;\n  onActiveChange?: (key: string | null) => void;\n  /** Category list with shares that roll to their new value. */\n  legend?: boolean;\n  /** Decimal places for shares. */\n  decimals?: number;\n  emptyLabel?: string;\n  ref?: Ref<HTMLElement>;\n  className?: string;\n}\n\ntype Cell = { id: number; key: string | null; slot: number };\ntype Pointer = { key: string; slot: number } | null;\n\nconst { spring } = motionTokens;\nconst GAP = 3;\n/** accentKey takes the first series and the next three categories the supporting series. Anything past that falls back to neutral steps that alternate in lightness. */\nconst SHADES = [40, 20, 30, 14];\n/** A second, colour-free channel: neutral categories cycle through square, round, and framed cells, so neighbours differ in shape as well as lightness. */\nconst SHAPES = [\"square\", \"round\", \"framed\"] as const;\n/** Shape is the colour-free second channel: a cell morphs between these as it changes category. */\nconst cellShape = {\n  square: \"\",\n  round: \"rounded-[50%]\",\n  framed: \"[--inner:inset_0_0_0_2.5px_var(--cell),inset_0_0_0_3.25px_var(--surface)]\",\n} as const;\nconst swatchShape = {\n  square: \"\",\n  round: \"rounded-[50%]\",\n  framed: \"shadow-[inset_0_0_0_2px_var(--cell),inset_0_0_0_3px_var(--surface)]\",\n} as const;\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/** Whole cells per category by largest remainder, so the counts always add up to the grid. */\nfunction allocate(data: WaffleCategory[], total: number, cells: number): number[] {\n  if (total <= 0) return data.map(() => 0);\n  const exact = data.map((item) => (Math.max(0, item.value) / total) * cells);\n  const counts = exact.map(Math.floor);\n  let left = cells - counts.reduce((sum, count) => sum + count, 0);\n  const order = exact\n    .map((value, index) => ({ index, rest: value - Math.floor(value) }))\n    .sort((a, b) => b.rest - a.rest);\n  for (let k = 0; left > 0 && k < order.length; k++, left--) counts[order[k]!.index]!++;\n  return counts;\n}\n\n/**\n * Hands cells to categories while moving as few as possible: each category keeps the cells it already had,\n * surplus cells join the categories that grew, and every block takes its cells in their previous order so paths never cross.\n */\nfunction reassign(previous: Cell[], data: WaffleCategory[], counts: number[]): Cell[] {\n  const bySlot = [...previous].sort((a, b) => a.slot - b.slot);\n  const kept = new Map<string, Cell[]>();\n  const pool: Cell[] = [];\n  const want = new Map(data.map((item, index) => [item.key, counts[index]]));\n  for (const cell of bySlot) {\n    const list = cell.key !== null ? (kept.get(cell.key) ?? []) : [];\n    if (cell.key !== null && list.length < (want.get(cell.key) ?? 0)) {\n      list.push(cell);\n      kept.set(cell.key, list);\n    } else pool.push(cell);\n  }\n  const next: Cell[] = [];\n  let slot = 0;\n  data.forEach((item, index) => {\n    const own = kept.get(item.key) ?? [];\n    while (own.length < counts[index]! && pool.length) own.push(pool.shift()!);\n    own.sort((a, b) => a.slot - b.slot).forEach((cell) => next.push({ id: cell.id, key: item.key, slot: slot++ }));\n  });\n  pool.forEach((cell) => next.push({ id: cell.id, key: null, slot: slot++ }));\n  return next.sort((a, b) => a.id - b.id);\n}\n\n/** A share that rolls to its new value on a spring and holds its width with tabular figures. */\nfunction Rolling({ value, decimals, reduced }: { value: number; decimals: number; reduced: boolean }) {\n  const shown = useMotionValue(value);\n  const text = useTransform(shown, (current) => `${current.toFixed(decimals)}%`);\n  useEffect(() => {\n    if (reduced) {\n      shown.jump(value);\n      return;\n    }\n    const controls = animate(shown, value, spring.smooth);\n    return () => controls.stop();\n  }, [reduced, shown, value]);\n  return <motion.span>{text}</motion.span>;\n}\n\nexport function WaffleChart({\n  data,\n  label,\n  unit = \"\",\n  formatValue,\n  rows = 10,\n  columns = 10,\n  accentKey,\n  activeKey,\n  defaultActiveKey = null,\n  onActiveChange,\n  legend = true,\n  decimals = 0,\n  emptyLabel = \"No data\",\n  ref,\n  className,\n}: WaffleChartProps) {\n  const reduced = useReducedMotionSafe();\n  const figure = useRef<HTMLElement>(null);\n  const grid = 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.35 });\n  const count = Math.max(1, rows * columns);\n  const total = data.reduce((sum, item) => sum + Math.max(0, item.value), 0);\n  const empty = !data.length || total <= 0;\n  const accent = accentKey === undefined ? (data[0]?.key ?? null) : accentKey;\n\n  // Cells are persistent objects; each data change reassigns them so they travel instead of repainting.\n  const signature = `${count}|${data.map((item) => `${item.key}:${item.value}`).join(\",\")}`;\n  const counts = useMemo(() => allocate(data, total, count), [signature]); // eslint-disable-line react-hooks/exhaustive-deps\n  const [layout, setLayout] = useState(() => ({\n    cells: reassign(\n      Array.from({ length: count }, (_, id) => ({ id, key: null, slot: id })),\n      data,\n      counts,\n    ),\n    moved: new Set<number>(),\n    version: 0,\n  }));\n  const [seen, setSeen] = useState(signature);\n  if (seen !== signature) {\n    setSeen(signature);\n    const base = Array.from({ length: count }, (_, id) => layout.cells[id] ?? { id, key: null, slot: id });\n    const next = reassign(base, data, counts);\n    setLayout({\n      cells: next,\n      moved: new Set(\n        next\n          .filter((cell) => base[cell.id]!.slot !== cell.slot || base[cell.id]!.key !== cell.key)\n          .map((cell) => cell.id),\n      ),\n      version: layout.version + 1,\n    });\n  }\n  const { cells, moved, version } = layout;\n\n  const [ownActive, setOwnActive] = useState<string | null>(defaultActiveKey);\n  const pinned = activeKey !== undefined ? activeKey : ownActive;\n  const setPinned = (key: string | null) => {\n    if (activeKey === undefined) setOwnActive(key);\n    onActiveChange?.(key);\n  };\n  const [pointer, setPointer] = useState<Pointer>(null);\n  const [preview, setPreview] = useState<string | null>(null);\n  const focusKey = pointer?.key ?? preview ?? pinned;\n  const active = focusKey && data.some((item) => item.key === focusKey) ? focusKey : null;\n\n  const [side, setSide] = useState(0);\n  useLayoutEffect(() => {\n    const node = grid.current;\n    if (!node) return;\n    const read = () => setSide(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 size = side ? (side - GAP * (columns - 1)) / columns : 0;\n  const height = size ? size * rows + GAP * (rows - 1) : 0;\n  // Column by column from the bottom-left corner, like a glass filling up.\n  const place = (slot: number) => ({\n    x: Math.floor(slot / rows) * (size + GAP),\n    y: (rows - 1 - (slot % rows)) * (size + GAP),\n  });\n\n  const colorOf = (key: string | null) => {\n    if (key === null) return \"transparent\";\n    const index = data.findIndex((item) => item.key === key),\n      item = data[index];\n    if (!item) return \"transparent\";\n    if (item.color) return item.color;\n    if (key === accent) return \"var(--series-1)\";\n    const rank = data.filter((entry) => entry.key !== accent).findIndex((entry) => entry.key === key);\n    return rank < 3\n      ? `var(--series-${rank + 2})`\n      : `color-mix(in oklab, var(--ink) ${SHADES[(rank - 3) % SHADES.length]}%, var(--surface))`;\n  };\n\n  const shapeOf = (key: string | null) => {\n    if (key === null || key === accent) return \"square\";\n    const rank = data.filter((entry) => entry.key !== accent).findIndex((entry) => entry.key === key);\n    return rank < 0 ? \"square\" : SHAPES[rank % SHAPES.length];\n  };\n\n  // The tooltip glides between cells on its own spring and stays inside the figure.\n  const tipX = useMotionValue(0),\n    tipY = useMotionValue(0);\n  const follow = { stiffness: 520, damping: 44, restDelta: 0.01 };\n  const glideX = useSpring(tipX, follow),\n    glideY = useSpring(tipY, follow);\n  const wasShown = useRef(false);\n  useLayoutEffect(() => {\n    const bubble = tip.current,\n      host = figure.current,\n      board = grid.current;\n    if (!pointer || !bubble || !host || !board || !size) {\n      wasShown.current = false;\n      return;\n    }\n    const hostBox = host.getBoundingClientRect(),\n      boardBox = board.getBoundingClientRect();\n    const { x, y } = place(pointer.slot);\n    const cx = boardBox.left - hostBox.left + x + size / 2,\n      cy = boardBox.top - hostBox.top + y;\n    const tw = bubble.offsetWidth,\n      th = bubble.offsetHeight;\n    let top = cy - th - 10;\n    if (top < 0) top = cy + size + 10;\n    const left = clamp(cx - tw / 2, 0, Math.max(0, hostBox.width - tw));\n    tipX.set(left);\n    tipY.set(clamp(top, 0, Math.max(0, hostBox.height - th)));\n    if (!wasShown.current || reduced) {\n      glideX.jump(tipX.get());\n      glideY.jump(tipY.get());\n    }\n    wasShown.current = true;\n  });\n\n  const slotAt = (clientX: number, clientY: number) => {\n    const box = grid.current?.getBoundingClientRect();\n    if (!box || !size) return null;\n    const col = clamp(Math.floor((clientX - box.left) / (size + GAP)), 0, columns - 1);\n    const row = clamp(Math.floor((clientY - box.top) / (size + GAP)), 0, rows - 1);\n    return col * rows + (rows - 1 - row);\n  };\n  const pointAt = (slot: number | null): Pointer => {\n    if (slot === null) return null;\n    const cell = cells.find((item) => item.slot === slot);\n    return cell?.key ? { key: cell.key, slot } : null;\n  };\n  const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    if (event.pointerType === \"mouse\" || event.buttons) setPointer(pointAt(slotAt(event.clientX, event.clientY)));\n  };\n  const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    if (event.pointerType !== \"mouse\") event.currentTarget.setPointerCapture?.(event.pointerId);\n    setPointer(pointAt(slotAt(event.clientX, event.clientY)));\n  };\n\n  // Arrows walk the grid cell by cell; Page keys jump between categories.\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (empty) return;\n    if (event.key === \"Escape\" && pointer) {\n      event.preventDefault();\n      setPointer(null);\n      return;\n    }\n    const filled = cells.filter((cell) => cell.key !== null).length;\n    const at = pointer?.slot ?? 0;\n    const col = Math.floor(at / rows),\n      row = at % rows;\n    const blocks = counts\n      .map((size, index) => ({ start: counts.slice(0, index).reduce((sum, value) => sum + value, 0), size }))\n      .filter((block) => block.size > 0);\n    const block = Math.max(\n      0,\n      blocks.findIndex((item) => at >= item.start && at < item.start + item.size),\n    );\n    const moves: Record<string, () => number> = {\n      ArrowUp: () => col * rows + Math.min(rows - 1, row + 1),\n      ArrowDown: () => col * rows + Math.max(0, row - 1),\n      ArrowRight: () => Math.min(columns - 1, col + 1) * rows + row,\n      ArrowLeft: () => Math.max(0, col - 1) * rows + row,\n      PageDown: () => blocks[Math.min(blocks.length - 1, block + 1)]!.start,\n      PageUp: () => (at > blocks[block]!.start ? blocks[block]!.start : blocks[Math.max(0, block - 1)]!.start),\n      Home: () => 0,\n      End: () => filled - 1,\n    };\n    const move = moves[event.key];\n    if (!move) return;\n    event.preventDefault();\n    setPointer(pointAt(clamp(pointer ? move() : 0, 0, Math.max(0, filled - 1))));\n  };\n\n  const format = (value: number, item: WaffleCategory) =>\n    `${formatValue ? formatValue(value, item) : grouped.format(value)}${unit ? ` ${unit}` : \"\"}`;\n  const share = (item: WaffleCategory) => (total > 0 ? (Math.max(0, item.value) / total) * 100 : 0);\n  const reading = pointer ? (data.find((item) => item.key === pointer.key) ?? null) : null;\n  const announce = reading\n    ? `${reading.label}, ${share(reading).toFixed(decimals)}%, ${format(reading.value, reading)}`\n    : \"\";\n  const summary = empty\n    ? `${label}. ${emptyLabel}.`\n    : `${label}. ${data.map((item) => `${item.label} ${share(item).toFixed(decimals)}%`).join(\", \")}.`;\n  const shownX = reduced ? tipX : glideX,\n    shownY = reduced ? tipY : glideY;\n  const settle = reduced ? { duration: 0 } : spring.morph;\n\n  return (\n    <figure\n      ref={figure}\n      className={cn(\"@container relative m-0 min-w-0 font-body tracking-body text-ink\", className)}\n      aria-label={label}\n      data-legend={legend || undefined}\n    >\n      <div\n        className={cn(\n          \"grid min-w-0 grid-cols-[minmax(0,1fr)] items-center gap-6\",\n          legend && \"@min-[420px]:grid-cols-[minmax(0,232px)_minmax(0,1fr)] @min-[420px]:gap-6\",\n        )}\n      >\n        <div className=\"min-w-0\">\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={grid}\n            className=\"relative mx-auto w-full max-w-[300px] cursor-default touch-pan-y outline-none select-none [-webkit-tap-highlight-color:transparent]\"\n            style={{ height: height || undefined, aspectRatio: height ? undefined : `${columns} / ${rows}` }}\n            data-active={active ? true : undefined}\n            role=\"group\"\n            tabIndex={empty ? -1 : 0}\n            aria-roledescription=\"waffle chart\"\n            aria-label={`${label}. ${count} cells. Use arrow keys to move between cells, Page Up and Page Down to jump between categories.`}\n            onPointerMove={onPointerMove}\n            onPointerDown={onPointerDown}\n            onPointerUp={(event) => {\n              if (event.pointerType !== \"mouse\") setPointer(null);\n            }}\n            onPointerCancel={() => setPointer(null)}\n            onPointerLeave={(event) => {\n              if (event.pointerType === \"mouse\") setPointer(null);\n            }}\n            onKeyDown={onKeyDown}\n            onBlur={() => setPointer(null)}\n            onFocus={(event) => {\n              if (event.currentTarget.matches(\":focus-visible\") && !empty)\n                setPointer((current) => current ?? pointAt(0));\n            }}\n          >\n            {size > 0 &&\n              cells.map((cell) => {\n                const { x, y } = place(cell.slot);\n                const travel = moved.has(cell.id) && !reduced;\n                const delay = reduced ? 0 : (cell.slot / count) * 0.32;\n                const on = cell.key !== null && cell.key === active;\n                const shown = inView || reduced;\n                // A travelling cell dips while it flies, so the flow reads as cells lifting out of one block and landing in another.\n                const dip = travel ? [1, 0.62 + (version % 2) * 0.001, 1] : 1;\n                // Each cell is one unit. Its colour changes mid-flight with the same stagger as its travel; outside the active category it steps back.\n                return (\n                  <motion.span\n                    key={cell.id}\n                    className={cn(\n                      \"absolute top-0 left-0 rounded-[22%] bg-(--cell) shadow-[var(--inner),var(--outer)] will-change-transform [--inner:0_0_0_0_transparent] [--outer:0_0_0_0_transparent] [transition:background-color_var(--duration-considered)_var(--ease-standard)_var(--delay,0s),border-radius_var(--duration-considered)_var(--ease-standard)_var(--delay,0s),box-shadow_var(--duration-considered)_var(--ease-standard)_var(--delay,0s),opacity_var(--duration-fast)_var(--ease-standard)] motion-reduce:transition-none\",\n                      cellShape[shapeOf(cell.key)],\n                      active &&\n                        !on &&\n                        \"opacity-26! [transition:background-color_var(--duration-considered)_var(--ease-standard)_var(--delay,0s),border-radius_var(--duration-considered)_var(--ease-standard)_var(--delay,0s),opacity_var(--duration-standard)_var(--ease-standard)]\",\n                      pointer?.slot === cell.slot &&\n                        \"[--outer:0_0_0_2px_var(--surface),0_0_0_3.5px_color-mix(in_oklab,var(--ink)_55%,transparent)]\",\n                    )}\n                    aria-hidden=\"true\"\n                    data-on={on || undefined}\n                    data-cursor={pointer?.slot === cell.slot || undefined}\n                    data-empty={cell.key === null || undefined}\n                    data-shape={shapeOf(cell.key)}\n                    style={\n                      {\n                        width: size,\n                        height: size,\n                        \"--cell\": colorOf(cell.key),\n                        \"--delay\": `${delay}s`,\n                      } as CSSProperties\n                    }\n                    initial={reduced ? false : { x, y, scale: 0.4, opacity: 0 }}\n                    animate={\n                      shown\n                        ? { x, y, scale: dip, opacity: cell.key === null ? 0 : 1 }\n                        : { x, y, scale: 0.4, opacity: 0 }\n                    }\n                    transition={\n                      reduced\n                        ? { duration: 0 }\n                        : {\n                            x: { ...settle, delay },\n                            y: { ...settle, delay },\n                            scale: travel\n                              ? { duration: 0.62, times: [0, 0.42, 1], ease: motionTokens.ease.inOut, delay }\n                              : { ...spring.snappy, delay: delay * 0.6 },\n                            opacity: {\n                              duration: motionTokens.duration.standard,\n                              ease: motionTokens.ease.standard,\n                              delay: delay * 0.6,\n                            },\n                          }\n                    }\n                  />\n                );\n              })}\n          </div>\n          {!empty && (\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-[150px] gap-0.5 rounded-[14px] border border-line bg-surface px-3 py-2 shadow-floating [transition:opacity_var(--duration-fast)_var(--ease-standard),scale_var(--duration-fast)_var(--ease-standard)] motion-reduce:transition-none\",\n                pointer ? \"opacity-100 [scale:1]\" : \"opacity-0 [scale:.96]\",\n              )}\n              data-shown={pointer ? true : undefined}\n              style={{ x: shownX, y: shownY }}\n              aria-hidden=\"true\"\n            >\n              <p className=\"m-0 flex min-w-0 items-center gap-2 text-sm leading-[1.3]\">\n                <span\n                  className={cn(\n                    \"size-[10px] flex-none rounded-[3px] bg-(--cell) transition-[background-color] duration-fast ease-standard motion-reduce:transition-none\",\n                    swatchShape[shapeOf(reading?.key ?? null)],\n                  )}\n                  data-shape={shapeOf(reading?.key ?? null)}\n                  style={{ \"--cell\": colorOf(reading?.key ?? null) } as CSSProperties}\n                />\n                <span className=\"min-w-0 flex-1 truncate text-ink-2\">{reading?.label ?? \"\"}</span>\n                <span className=\"ml-2 flex-none font-medium tabular-nums\">\n                  {reading ? `${share(reading).toFixed(decimals)}%` : \"\"}\n                </span>\n              </p>\n              <p className=\"my-0 mr-0 ml-[18px] text-xs leading-body whitespace-nowrap text-ink-3 tabular-nums\">\n                {reading ? format(reading.value, reading) : \"\"}\n              </p>\n            </motion.div>\n          )}\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        {/* The legend doubles as the reading: each share rolls to its new value. Below the side-by-side layout it reads as one list that shares the grid's measure, its row padding hanging outside so swatches and shares line up with the cell edges. */}\n        {legend && !empty && (\n          <ul\n            className=\"m-0 grid min-w-0 list-none grid-cols-[repeat(auto-fill,minmax(132px,1fr))] gap-x-2 gap-y-0.5 p-0 @min-[420px]:grid-cols-[minmax(0,1fr)] [@container(max-width:419px)]:mx-[max(-10px,calc((100%-320px)/2))] [@container(max-width:419px)]:grid-cols-[minmax(0,1fr)]\"\n            aria-label={`${label} categories`}\n            onPointerLeave={() => setPreview(null)}\n          >\n            {data.map((item) => (\n              <li key={item.key}>\n                <button\n                  type=\"button\"\n                  className={cn(\n                    \"grid min-h-9 w-full cursor-pointer grid-cols-[10px_minmax(0,1fr)_auto] items-center gap-[10px] rounded-button border-0 bg-transparent px-[10px] py-1 text-left text-sm leading-body [-webkit-tap-highlight-color:transparent] [transition:color_var(--duration-fast)_var(--ease-standard),background-color_var(--duration-fast)_var(--ease-standard),opacity_var(--duration-fast)_var(--ease-standard),transform_var(--duration-instant)_var(--ease-standard)] hover:text-ink active:[transform:scale(.98)] motion-reduce:transition-none\",\n                    active === item.key || pinned === item.key ? \"text-ink\" : \"text-ink-2\",\n                    active === item.key && \"bg-[color-mix(in_oklab,var(--ink)_5%,transparent)]\",\n                    active !== null && active !== item.key && \"opacity-50\",\n                  )}\n                  aria-pressed={pinned === item.key}\n                  data-on={active === item.key || undefined}\n                  data-dim={(active !== null && active !== item.key) || undefined}\n                  onClick={() => setPinned(pinned === item.key ? null : item.key)}\n                  onPointerEnter={(event) => {\n                    if (event.pointerType === \"mouse\") setPreview(item.key);\n                  }}\n                  onFocus={(event) => {\n                    if (event.currentTarget.matches(\":focus-visible\")) setPreview(item.key);\n                  }}\n                  onBlur={() => setPreview(null)}\n                >\n                  <span\n                    className={cn(\"size-[10px] rounded-[3px] bg-(--cell)\", swatchShape[shapeOf(item.key)])}\n                    data-shape={shapeOf(item.key)}\n                    style={{ \"--cell\": colorOf(item.key) } as CSSProperties}\n                    aria-hidden=\"true\"\n                  />\n                  <span className=\"min-w-0 truncate\">{item.label}</span>\n                  <span className=\"min-w-[3.2em] text-right text-(length:--text-base) font-medium text-ink tabular-nums\">\n                    <Rolling value={share(item)} decimals={decimals} reduced={reduced} />\n                  </span>\n                </button>\n              </li>\n            ))}\n          </ul>\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\">Category</th>\n                <th scope=\"col\">Value</th>\n                <th scope=\"col\">Share</th>\n                <th scope=\"col\">Cells</th>\n              </tr>\n            </thead>\n            <tbody>\n              {data.map((item, index) => (\n                <tr key={item.key}>\n                  <th scope=\"row\">{item.label}</th>\n                  <td>{format(item.value, item)}</td>\n                  <td>{share(item).toFixed(decimals)}%</td>\n                  <td>{counts[index]}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      )}\n    </figure>\n  );\n}\n\nexport default WaffleChart;\n",
      "type": "registry:component",
      "target": "@components/iris/waffle-chart/waffle-chart.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/waffle-chart",
  "meta": {
    "kind": "component",
    "group": "Charts",
    "export": "WaffleChart",
    "tags": [
      "data",
      "chart",
      "new"
    ],
    "docs": "https://irisui.dev/components/waffle-chart"
  }
}
