{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-metrics",
  "title": "Bento metrics",
  "description": "A metrics section in Tiles: big figures that roll in once when the section comes into view, with sparklines and change pills. Final at once under reduced motion.",
  "registryDependencies": [
    "utils",
    "@manniche/chart-kit",
    "@manniche/data-tile",
    "@manniche/sparkline",
    "@manniche/use-reduced-motion"
  ],
  "files": [
    {
      "path": "registry/manniche/bento-metrics/bento-metrics.tsx",
      "content": "// Bento metrics: a heading with a one-line intro over a grid of figure tiles. Each tile is a label, the big figure\n// (`BigNumber`), an optional change pill, a short note and, when it has a `trend`, a `Sparkline`. One tile can be\n// inverted to lift the figure that matters most. `note` under the heading is where a section says what the numbers\n// are, so a demo can say \"Example data.\" in plain sight.\n//\n// Signature: the figures roll in when the section first comes into view. Until then (and on the server) the real\n// values are in the page, so nothing depends on the animation; with reduced motion they simply stay put.\n//\n// Screen readers: one `h2`, a real list of tiles, each with an `h3` label. Every figure is read once, with its\n// change (\"up\" or \"down\") and its trend sentence from the sparkline. The roll is hidden from them: they always\n// hear the final value, never a half-counted one.\nimport { useCallback, useId, useLayoutEffect, useRef, useState, type ComponentProps, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\nimport { BigNumber, DeltaPill } from '@/registry/manniche/chart-kit/chart-kit'\nimport { formatValue, type ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'\nimport { DataTile, TileFact } from '@/registry/manniche/data-tile/data-tile'\nimport { useReducedMotion } from '@/registry/manniche/hooks/use-reduced-motion'\nimport { Sparkline } from '@/registry/manniche/sparkline/sparkline'\n\nexport type BentoMetric = {\n  /** A stable key for the tile. */\n  id: string\n  /** What the figure is, shown as the tile's `h3`: \"Projects shipped\". */\n  label: string\n  /** The figure. */\n  value: number\n  /** How the figure is written: currency, decimals, suffix. */\n  format?: ValueFormat\n  /** The change in the figure, shown as a pill (a percentage by default). Leave it out for none. */\n  delta?: number\n  /** How the change is written. Default one decimal and a percent sign. */\n  deltaFormat?: ValueFormat\n  /** Which direction of the change is good. \"down\" for things like refunds. Default \"up\". */\n  goodWhen?: 'up' | 'down'\n  /** Values for a small trend line, oldest first. Leave it out for none. */\n  trend?: readonly number[]\n  /** The trend's accessible name, such as \"Projects shipped, last 8 months\". Default the label. */\n  trendLabel?: string\n  /** A short line of context under the figure: what is counted and over which period. */\n  note?: ReactNode\n  /** Swap this tile to the opposite theme. Use it on one tile. */\n  inverted?: boolean\n}\n\nexport type BentoMetricsProps = Omit<ComponentProps<'section'>, 'children' | 'title'> & {\n  /** The section's `h2`. */\n  heading: ReactNode\n  /** A short line under the heading. */\n  intro?: ReactNode\n  /** A small mono label above the heading. */\n  eyebrow?: ReactNode\n  /** A fine-print line under the grid, such as \"Example data.\" or the source and date of real figures. */\n  note?: ReactNode\n  /** The figure tiles, two to six. */\n  metrics: readonly BentoMetric[]\n  /** Roll the figures in when the section first comes into view. Never under reduced motion. Default true. */\n  roll?: boolean\n}\n\nconst COLS = {\n  2: '@4xl:grid-cols-2',\n  3: '@4xl:grid-cols-3',\n  4: '@4xl:grid-cols-2',\n} as const\n\nexport function BentoMetrics({ heading, intro, eyebrow, note, metrics, roll = true, className, ref, ...rest }: BentoMetricsProps) {\n  const headingId = useId()\n  const reduced = useReducedMotion()\n  const root = useRef<HTMLElement | null>(null)\n  // The section needs its own handle for the observer, and a ref from the caller must still reach it.\n  const setRoot = useCallback(\n    (node: HTMLElement | null) => {\n      root.current = node\n      if (typeof ref === 'function') ref(node)\n      else if (ref) ref.current = node\n    },\n    [ref],\n  )\n  // \"armed\" shows the figures at zero until the section is seen; the real values are rendered until then.\n  const [armed, setArmed] = useState(false)\n  useLayoutEffect(() => {\n    const node = root.current\n    if (!roll || reduced || !node || typeof IntersectionObserver === 'undefined') return\n    setArmed(true)\n    const io = new IntersectionObserver(\n      ([e]) => {\n        if (e.isIntersecting) {\n          setArmed(false)\n          io.disconnect()\n        }\n      },\n      // Any part of the section entering the lower 80 % of the view counts, so a section taller than the screen\n      // (one column at high zoom) still starts its roll.\n      { threshold: 0, rootMargin: '0px 0px -20% 0px' },\n    )\n    io.observe(node)\n    return () => io.disconnect()\n  }, [roll, reduced])\n\n  // Derived rather than reset in the effect: if reduced motion turns on (or `roll` turns off) after the section\n  // was armed, the effect returns early and the figures must still show their real values.\n  const zero = armed && roll && !reduced\n\n  const cols = COLS[metrics.length as 2 | 3 | 4] ?? '@4xl:grid-cols-3'\n  return (\n    <section\n      ref={setRoot}\n      aria-labelledby={headingId}\n      className={cn('@container w-full py-12 @3xl:py-20', className)}\n      {...rest}\n    >\n      <div className=\"mx-auto max-w-6xl px-4 sm:px-6\">\n        <header className=\"mb-8 grid max-w-2xl gap-3 @3xl:mb-12\">\n          {eyebrow && <p className=\"font-mono text-xs tracking-[0.04em] text-muted-foreground tabular-nums\">{eyebrow}</p>}\n          <h2\n            id={headingId}\n            className=\"text-[clamp(30px,5.4cqw,52px)] leading-[1.02] font-extrabold tracking-[-0.035em] text-balance text-foreground\"\n            style={{ fontFamily: 'var(--font-display, inherit)', fontStretch: '86%' }}\n          >\n            {heading}\n          </h2>\n          {intro && <p className=\"text-base leading-relaxed text-pretty text-muted-foreground\">{intro}</p>}\n        </header>\n        <ul className={cn('grid list-none grid-cols-1 gap-3 p-0 @2xl:grid-cols-2 @2xl:gap-4', cols)}>\n          {metrics.map((m) => (\n            <li key={m.id} className=\"flex min-w-0\">\n              <DataTile title={m.label} inverted={m.inverted} className=\"w-full\">\n                <div className=\"grid h-full content-between gap-5\">\n                  <div className=\"grid gap-2\">\n                    <TileFact\n                      aside={\n                        m.delta !== undefined && (\n                          <DeltaPill value={m.delta} format={m.deltaFormat} goodWhen={m.goodWhen} />\n                        )\n                      }\n                    >\n                      {/* The figure is spoken once, in full, whatever the roll is doing. */}\n                      <span className=\"sr-only\">{formatValue(m.value, m.format)}</span>\n                      <span aria-hidden>\n                        <BigNumber value={zero ? 0 : m.value} format={m.format} size=\"lg\" roll={roll} />\n                      </span>\n                    </TileFact>\n                    {m.note && <p className=\"text-[13.5px] leading-normal text-pretty text-muted-foreground\">{m.note}</p>}\n                  </div>\n                  {m.trend && m.trend.length > 1 && (\n                    <Sparkline data={m.trend} label={m.trendLabel ?? m.label} format={m.format} height={36} />\n                  )}\n                </div>\n              </DataTile>\n            </li>\n          ))}\n        </ul>\n        {note && <p className=\"mt-4 font-mono text-xs text-muted-foreground tabular-nums\">{note}</p>}\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "templates"
  ],
  "type": "registry:block"
}