{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-grid",
  "title": "Bento Grid",
  "description": "A responsive tiling layout of mixed-size feature cards, with a pointer-tracked glow and honest click affordances.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/bento-grid.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUpRight } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\"\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(REDUCED_MOTION)\n  mq.addEventListener(\"change\", callback)\n  return () => mq.removeEventListener(\"change\", callback)\n}\n\nfunction useReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION).matches,\n    () => false,\n  )\n}\n\ntype Columns = 2 | 3 | 4\ntype Span = 1 | 2 | 3\n\n// Tailwind only sees class names that exist as literal strings in the source,\n// so every column count and span lives in a lookup table — `col-span-${n}`\n// would compile away to nothing.\nconst COLUMNS_CLASS: Record<Columns, string> = {\n  2: \"grid-cols-1 sm:grid-cols-2\",\n  3: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n  4: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4\",\n}\n\n// Spans only kick in once there is more than one column: on phones every card\n// is full width anyway, and a span wider than the grid would just be clamped.\nconst COL_SPAN_CLASS: Record<Span, string> = {\n  1: \"\",\n  2: \"sm:col-span-2\",\n  3: \"sm:col-span-2 lg:col-span-3\",\n}\n\nconst ROW_SPAN_CLASS: Record<Span, string> = {\n  1: \"\",\n  2: \"sm:row-span-2\",\n  3: \"sm:row-span-3\",\n}\n\nexport interface BentoGridProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Column count from the lg breakpoint up; always 1 on phones and 2 on tablets. */\n  columns?: Columns\n  /** Gutter in px, applied to both axes. */\n  gap?: number\n}\n\n/** The tiling container: a responsive grid with content-sized rows. */\nexport const BentoGrid = React.forwardRef<HTMLDivElement, BentoGridProps>(\n  ({ columns = 3, gap = 16, className, style, children, ...props }, ref) => (\n    <div\n      className={cn(\"grid auto-rows-[minmax(8rem,auto)]\", COLUMNS_CLASS[columns], className)}\n      ref={ref}\n      style={{ gap, ...style }}\n      {...props}\n    >\n      {children}\n    </div>\n  ),\n)\n\nBentoGrid.displayName = \"BentoGrid\"\n\n// `title` is widened from the native tooltip attribute to a full ReactNode:\n// on a bento tile it is the card's heading, not a hover string.\nexport interface BentoCardProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n  title: React.ReactNode\n  description?: React.ReactNode\n  /** Decorative glyph, rendered in a muted chip above the title. */\n  icon?: React.ReactNode\n  colSpan?: Span\n  rowSpan?: Span\n  /** Renders the card as a real <a>. Takes priority over onClick. */\n  href?: string\n  /** Free content: charts, images, or an `absolute inset-0 -z-10` background layer. */\n  children?: React.ReactNode\n}\n\n/**\n * One tile. It only becomes clickable when it is actually given somewhere to\n * go — `href` renders a real anchor, `onClick` an activatable card with the\n * corner arrow; with neither it stays a plain static panel and never pretends\n * to be pressable.\n */\nexport const BentoCard = React.forwardRef<HTMLElement, BentoCardProps>(\n  (\n    {\n      title,\n      description,\n      icon,\n      colSpan = 1,\n      rowSpan = 1,\n      href,\n      onClick,\n      onKeyDown,\n      onPointerMove,\n      className,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const reduced = useReducedMotion()\n    const glowRef = React.useRef<HTMLSpanElement>(null)\n    const clickable = Boolean(href) || Boolean(onClick)\n\n    // The root tag changes with the props, so the ref is forwarded through a\n    // callback typed on HTMLElement — no casting between anchor and div refs.\n    const setRef = (node: HTMLElement | null) => {\n      if (typeof forwardedRef === \"function\") forwardedRef(node)\n      else if (forwardedRef) forwardedRef.current = node\n    }\n\n    // Pointer coordinates are written straight into CSS custom properties, so\n    // a fast move stream repaints the light layer without re-rendering the card.\n    const handlePointerMove = (e: React.PointerEvent<HTMLElement>) => {\n      if (!reduced) {\n        const rect = e.currentTarget.getBoundingClientRect()\n        glowRef.current?.style.setProperty(\"--bento-x\", `${e.clientX - rect.left}px`)\n        glowRef.current?.style.setProperty(\"--bento-y\", `${e.clientY - rect.top}px`)\n      }\n      onPointerMove?.(e)\n    }\n\n    // Only keys pressed on the tile itself activate it: a button or link inside\n    // the card keeps its own native Enter/Space activation.\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {\n      if (onClick && e.target === e.currentTarget && (e.key === \"Enter\" || e.key === \" \")) {\n        e.preventDefault()\n        e.currentTarget.click()\n      }\n      onKeyDown?.(e)\n    }\n\n    const rootClass = cn(\n      \"group relative isolate flex flex-col overflow-hidden rounded-xl border bg-card p-5\",\n      \"transition-colors duration-300 hover:border-primary/40 motion-reduce:transition-none\",\n      clickable &&\n        \"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      COL_SPAN_CLASS[colSpan],\n      ROW_SPAN_CLASS[rowSpan],\n      className,\n    )\n\n    const content = (\n      <>\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-300\",\n            \"group-hover:opacity-100 motion-reduce:transition-none [@media(hover:none)]:hidden\",\n          )}\n          ref={glowRef}\n          style={{\n            background:\n              \"radial-gradient(220px circle at var(--bento-x, 50%) var(--bento-y, 50%), color-mix(in oklab, var(--primary) 14%, transparent), transparent 70%)\",\n          }}\n        />\n\n        {icon && (\n          <div\n            aria-hidden=\"true\"\n            className=\"mb-3 flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground\"\n          >\n            {icon}\n          </div>\n        )}\n\n        <h3 className={cn(\"text-sm font-medium text-card-foreground\", clickable && \"pr-6\")}>{title}</h3>\n        {description && <p className=\"mt-1.5 text-sm text-muted-foreground\">{description}</p>}\n        {children && <div className=\"mt-4 min-h-0 flex-1\">{children}</div>}\n\n        {clickable && (\n          <ArrowUpRight\n            aria-hidden=\"true\"\n            className=\"absolute top-5 right-5 size-4 text-muted-foreground transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 motion-reduce:transition-none\"\n          />\n        )}\n      </>\n    )\n\n    if (href) {\n      return (\n        <a\n          className={cn(rootClass, \"no-underline\")}\n          href={href}\n          onClick={onClick}\n          onKeyDown={onKeyDown}\n          onPointerMove={handlePointerMove}\n          ref={setRef}\n          {...props}\n        >\n          {content}\n        </a>\n      )\n    }\n\n    return (\n      <div\n        className={rootClass}\n        onClick={onClick}\n        onKeyDown={handleKeyDown}\n        onPointerMove={handlePointerMove}\n        ref={setRef}\n        role={onClick ? \"button\" : undefined}\n        tabIndex={onClick ? 0 : undefined}\n        {...props}\n      >\n        {content}\n      </div>\n    )\n  },\n)\n\nBentoCard.displayName = \"BentoCard\"\n\nexport default BentoGrid\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}