{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-grid",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/card.json"
  ],
  "files": [
    {
      "path": "bento-grid.tsx",
      "target": "components/ui/bento-grid.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Card,\n  type CardHighlight,\n  type CardProps,\n} from \"./card\";\n\nexport interface BentoGridProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Force an explicit column count; omit for a responsive auto-fit grid. */\n  columns?: number;\n}\n\nexport const BentoGrid = React.forwardRef<HTMLDivElement, BentoGridProps>(\n  ({ className, columns, style, children, ...p }, ref) => {\n    const gridRef = React.useRef<HTMLDivElement | null>(null);\n    const setRefs = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        gridRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      },\n      [ref],\n    );\n\n    React.useEffect(() => {\n      const grid = gridRef.current;\n      if (!grid) return;\n      if (\n        typeof window.matchMedia === \"function\" &&\n        window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n      )\n        return;\n\n      let frame = 0;\n      let pending: { x: number; y: number } | null = null;\n\n      /* Measuring inside the frame would force a synchronous layout on every\n       * pointer move, since the previous frame already wrote inline styles.\n       * The geometry only changes on resize or scroll, so it is cached and the\n       * reads happen up front, never interleaved with the writes. */\n      type Measured = { grid: DOMRect; cards: [HTMLElement, DOMRect][] };\n      let cache: Measured | null = null;\n      let observed: HTMLElement[] = [];\n\n      const resizeObserver =\n        typeof ResizeObserver === \"undefined\"\n          ? null\n          : new ResizeObserver(() => {\n              cache = null;\n            });\n      resizeObserver?.observe(grid);\n\n      const measure = (): Measured => {\n        const cards = Array.from(\n          grid.querySelectorAll<HTMLElement>(\"[data-bento-card]\"),\n        );\n        const same =\n          cards.length === observed.length &&\n          cards.every((card, i) => card === observed[i]);\n        if (!same && resizeObserver) {\n          for (const card of observed) resizeObserver.unobserve(card);\n          for (const card of cards) resizeObserver.observe(card);\n          observed = cards;\n        }\n        return {\n          grid: grid.getBoundingClientRect(),\n          cards: cards.map((card) => [card, card.getBoundingClientRect()]),\n        };\n      };\n\n      const paint = () => {\n        frame = 0;\n        const point = pending;\n        if (!point) return;\n        if (!cache) cache = measure();\n        const rects = cache;\n        grid.style.setProperty(\"--bento-x\", `${point.x - rects.grid.left}px`);\n        grid.style.setProperty(\"--bento-y\", `${point.y - rects.grid.top}px`);\n        grid.style.setProperty(\"--bento-fill-o\", \"1\");\n        grid.style.setProperty(\"--edge-o\", \"1\");\n        for (const [card, r] of rects.cards) {\n          card.style.setProperty(\"--edge-x\", `${point.x - r.left}px`);\n          card.style.setProperty(\"--edge-y\", `${point.y - r.top}px`);\n        }\n      };\n\n      const invalidate = () => {\n        cache = null;\n      };\n\n      const onMove = (e: PointerEvent) => {\n        pending = { x: e.clientX, y: e.clientY };\n        if (!frame) frame = requestAnimationFrame(paint);\n      };\n      const onLeave = () => {\n        if (frame) cancelAnimationFrame(frame);\n        frame = 0;\n        pending = null;\n        grid.style.setProperty(\"--bento-fill-o\", \"0\");\n        grid.style.setProperty(\"--edge-o\", \"0\");\n      };\n\n      grid.addEventListener(\"pointermove\", onMove, { passive: true });\n      grid.addEventListener(\"pointerleave\", onLeave, { passive: true });\n      window.addEventListener(\"scroll\", invalidate, {\n        passive: true,\n        capture: true,\n      });\n      return () => {\n        grid.removeEventListener(\"pointermove\", onMove);\n        grid.removeEventListener(\"pointerleave\", onLeave);\n        window.removeEventListener(\"scroll\", invalidate, { capture: true });\n        resizeObserver?.disconnect();\n        if (frame) cancelAnimationFrame(frame);\n      };\n    }, []);\n\n    return (\n      <div\n        ref={setRefs}\n        className={cn(\n          \"wash-accent group/bento relative isolate grid auto-rows-[minmax(0,auto)] grid-flow-dense gap-3 rounded-3xl p-3 sm:auto-rows-[minmax(9rem,auto)]\",\n          columns == null &&\n            \"[grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr))]\",\n          className,\n        )}\n        style={\n          columns == null\n            ? style\n            : {\n                gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,\n                ...style,\n              }\n        }\n        {...p}\n      >\n        <span aria-hidden className=\"bento-spotlight\" />\n        {children}\n      </div>\n    );\n  },\n);\nBentoGrid.displayName = \"BentoGrid\";\n\nexport interface BentoCardProps extends CardProps {\n  /**\n   * Column span. Not a position: the grid is `grid-flow-dense`, so a wide card\n   * lets narrower ones backfill ahead of it. Nothing clamps this against\n   * `columns`, so overshoot and the card overflows its track.\n   */\n  span?: number;\n  /** Row span. Rows are `minmax(9rem,auto)`, so this raises the floor, not the height. */\n  rowSpan?: number;\n  highlight?: CardHighlight;\n}\n\nexport const BentoCard = React.forwardRef<HTMLDivElement, BentoCardProps>(\n  ({ className, span, rowSpan, style, children, ...p }, ref) => (\n    <Card\n      ref={ref}\n      data-bento-card=\"\"\n      className={cn(\n        \"relative border-border bg-surface/70 transition-tint duration-base ease-soft motion-reduce:transition-none\",\n        className,\n      )}\n      style={{\n        gridColumn: span != null ? `span ${span}` : undefined,\n        gridRow: rowSpan != null ? `span ${rowSpan}` : undefined,\n        ...style,\n      }}\n      {...p}\n    >\n      <span aria-hidden className=\"edge-glow\" />\n      {children}\n    </Card>\n  ),\n);\nBentoCard.displayName = \"BentoCard\";\n\n/**\n * The three cells below are content, not containers. BentoCard supplies the\n * surface, the span, and the edge glow; a cell supplies only padding and fill.\n * They work as a child of any Card, not just a bento cell.\n */\n\nconst cellShell = \"@container flex h-full flex-col p-6\";\n\n/** Mono uppercase label beside an icon tile. Shared by BentoInfo and BentoText. */\nfunction CellLabel({\n  icon,\n  label,\n}: {\n  icon?: React.ReactNode;\n  label: React.ReactNode;\n}) {\n  return (\n    <div className=\"mb-4 flex items-center gap-2.5\">\n      {icon != null && (\n        <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-surface-2 text-accent-alt [&_svg]:size-4\">\n          {icon}\n        </span>\n      )}\n      <span className=\"font-mono text-[0.625rem] uppercase leading-none tracking-[0.18em] text-accent-alt\">\n        {label}\n      </span>\n    </div>\n  );\n}\n\nconst metricValueSizes = {\n  // Sized against the cell, not the viewport: a display number in a narrow card\n  // otherwise renders at its full viewport size and overruns its neighbour.\n  md: \"text-[clamp(2rem,18cqi,3.5rem)] font-bold text-ink/70\",\n  lg: \"text-[clamp(2.5rem,25cqi,6rem)] font-black text-ink\",\n} as const;\n\nconst NUMERIC = /^-?\\d+(\\.\\d+)?$/;\n\nfunction parseTarget(value: React.ReactNode): number | null {\n  if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n  if (typeof value === \"string\" && NUMERIC.test(value)) return Number(value);\n  return null;\n}\n\nfunction decimalsOf(value: React.ReactNode): number {\n  const text = String(value);\n  const dot = text.indexOf(\".\");\n  return dot === -1 ? 0 : text.length - dot - 1;\n}\n\nconst prefersReducedMotion = () =>\n  typeof window !== \"undefined\" &&\n  typeof window.matchMedia === \"function\" &&\n  window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nconst COUNT_UP_MS = 900;\n\n/**\n * Counts from zero to `target` on mount using rAF alone. No motion dependency, so the\n * registry-copied source has nothing extra to resolve. Runs in a layout effect so the\n * first paint is already at zero rather than flashing the final value.\n *\n * A hidden or throttled tab never delivers a frame, and a metric stuck at zero is worse\n * than one that never animated, so the count is skipped outright when the document is\n * hidden and snapped to the target if no frame arrives.\n */\nfunction useCountUp(target: number | null, decimals: number, run: boolean) {\n  const [display, setDisplay] = React.useState(target);\n  const useIsomorphicLayoutEffect =\n    typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\n  useIsomorphicLayoutEffect(() => {\n    const hidden = typeof document !== \"undefined\" && document.hidden;\n    if (!run || target == null || hidden || prefersReducedMotion()) {\n      setDisplay(target);\n      return;\n    }\n\n    setDisplay(0);\n    let frame = 0;\n    let start = 0;\n    let painted = false;\n\n    const step = (now: number) => {\n      painted = true;\n      if (!start) start = now;\n      const progress = Math.min((now - start) / COUNT_UP_MS, 1);\n      const eased = 1 - (1 - progress) ** 3;\n      setDisplay(Number((target * eased).toFixed(decimals)));\n      if (progress < 1) frame = requestAnimationFrame(step);\n    };\n\n    frame = requestAnimationFrame(step);\n    const rescue = setTimeout(() => {\n      if (!painted) setDisplay(target);\n    }, COUNT_UP_MS + 300);\n\n    return () => {\n      cancelAnimationFrame(frame);\n      clearTimeout(rescue);\n    };\n  }, [target, decimals, run]);\n\n  return display;\n}\n\nexport interface BentoMetricProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  value: React.ReactNode;\n  label: React.ReactNode;\n  icon?: React.ReactNode;\n  /** Trailing unit on the value, keyed to the alternate accent. */\n  suffix?: React.ReactNode;\n  /** Aside under the value. Flavour text, a caveat, a comparison. */\n  note?: React.ReactNode;\n  /** `lg` is the standalone stat treatment: display weight, full-strength ink. */\n  size?: keyof typeof metricValueSizes;\n  /** Count up from zero on mount. Ignored for non-numeric values. */\n  animate?: boolean;\n}\n\nexport const BentoMetric = React.forwardRef<HTMLDivElement, BentoMetricProps>(\n  (\n    {\n      className,\n      value,\n      label,\n      icon,\n      suffix,\n      note,\n      size = \"md\",\n      animate,\n      ...props\n    },\n    ref,\n  ) => {\n    const target = parseTarget(value);\n    const decimals = decimalsOf(value);\n    const counted = useCountUp(target, decimals, animate === true);\n    const shown =\n      target == null ? value : (counted ?? target).toFixed(decimals);\n\n    return (\n      <div\n        ref={ref}\n        className={cn(cellShell, \"justify-between\", className)}\n        {...props}\n      >\n        <div>\n          <p\n            className={cn(\n              \"leading-none tracking-[-0.04em] tabular-nums\",\n              metricValueSizes[size],\n            )}\n          >\n            {shown}\n            {suffix != null && (\n              <span className=\"text-accent-alt\">{suffix}</span>\n            )}\n          </p>\n          {note != null && (\n            <p className=\"mt-3 font-mono text-[0.6875rem] leading-relaxed text-muted\">\n              {note}\n            </p>\n          )}\n        </div>\n        <span className=\"mt-4 inline-flex w-fit max-w-full items-center gap-2 self-start whitespace-nowrap rounded-full bg-ink/[0.06] px-3 py-1.5 font-mono text-[0.6875rem] tracking-[0.1em] text-muted [&_svg]:size-3\">\n          {icon}\n          {label}\n        </span>\n      </div>\n    );\n  },\n);\nBentoMetric.displayName = \"BentoMetric\";\n\nexport interface BentoInfoProps extends React.HTMLAttributes<HTMLDivElement> {\n  label: React.ReactNode;\n  icon?: React.ReactNode;\n}\n\n/**\n * Label, icon tile, then anything. kajo's `variant=\"tech-stack\"` union is gone:\n * pass Chips as children instead.\n */\nexport const BentoInfo = React.forwardRef<HTMLDivElement, BentoInfoProps>(\n  ({ className, label, icon, children, ...props }, ref) => (\n    <div ref={ref} className={cn(cellShell, className)} {...props}>\n      <CellLabel icon={icon} label={label} />\n      <div className=\"text-[0.9375rem] leading-relaxed text-ink\">\n        {children}\n      </div>\n    </div>\n  ),\n);\nBentoInfo.displayName = \"BentoInfo\";\n\nexport interface BentoTextProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  label?: React.ReactNode;\n  icon?: React.ReactNode;\n  title: React.ReactNode;\n  body?: React.ReactNode;\n}\n\nexport const BentoText = React.forwardRef<HTMLDivElement, BentoTextProps>(\n  ({ className, label, icon, title, body, children, ...props }, ref) => (\n    <div ref={ref} className={cn(cellShell, className)} {...props}>\n      {label != null && <CellLabel icon={icon} label={label} />}\n      <h3 className=\"text-xl font-semibold tracking-[-0.01em] text-ink\">\n        {title}\n      </h3>\n      {body != null && (\n        <p className=\"mt-3 text-[0.9375rem] leading-relaxed text-muted\">\n          {body}\n        </p>\n      )}\n      {children}\n    </div>\n  ),\n);\nBentoText.displayName = \"BentoText\";\n"
    }
  ]
}