{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-grid",
  "type": "registry:ui",
  "title": "Bento Grid",
  "description": "A responsive editorial bento layout with dense packing, a staggered entrance and pointer-lit tiles.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/bento-grid.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\n\nexport interface BentoItem {\n  /** Stable key. */\n  id: string;\n  title: string;\n  description?: string;\n  /** Columns × rows the tile spans from two columns up. Defaults to \"1x1\". */\n  span?: \"1x1\" | \"2x1\" | \"1x2\" | \"2x2\";\n  /** Visual shown above the text (charts, illustrations, UI fragments). */\n  content?: React.ReactNode;\n  /** Turns the whole tile into a link. */\n  href?: string;\n}\n\nexport interface BentoGridProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: BentoItem[];\n  /** Columns on wide containers (≥ 768px). Narrower containers use 2, then 1. */\n  columns?: number;\n  /** Minimum row height in px. */\n  rowHeight?: number;\n  /** Gap between tiles in px. */\n  gap?: number;\n  /** Hover treatment: a pointer-following border light, a lift, both or none. */\n  hoverEffect?: \"light\" | \"lift\" | \"both\" | \"none\";\n  /** Delay between tiles in the entrance, in ms (0 = all at once). */\n  stagger?: number;\n  /** Light, link arrow and focus ring colour. */\n  accent?: string;\n  /** Tile corner radius in px. */\n  rounded?: number;\n  /** Heading level used for tile titles. */\n  headingLevel?: 2 | 3 | 4;\n}\n\n// Spans apply from the two-column breakpoint up; on one column every tile is full width.\nconst SPAN: Record<NonNullable<BentoItem[\"span\"]>, string> = {\n  \"1x1\": \"\",\n  \"2x1\": \"@md:col-span-2\",\n  \"1x2\": \"min-h-[calc(var(--bg-row)*1.5)] @md:row-span-2 @md:min-h-0\",\n  \"2x2\": \"min-h-[calc(var(--bg-row)*1.5)] @md:col-span-2 @md:row-span-2 @md:min-h-0\",\n};\n\nconst COLUMNS: Record<number, string> = {\n  2: \"@3xl:grid-cols-2\",\n  3: \"@3xl:grid-cols-3\",\n  4: \"@3xl:grid-cols-4\",\n};\n\n/** Small arrow shown on linked tiles. */\nfunction ArrowGlyph() {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-3.5\" aria-hidden=\"true\">\n      <path d=\"M5 11l6-6M6 5h5v5\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\n/**\n * An editorial bento layout on CSS grid with dense packing. Columns follow the container\n * (not the viewport), tiles stagger in once when the grid scrolls into view, and each tile\n * can light its border under the pointer or lift on hover. Linked tiles are real anchors.\n */\nexport function BentoGrid({\n  items,\n  columns = 4,\n  rowHeight = 170,\n  gap = 12,\n  hoverEffect = \"light\",\n  stagger = 70,\n  accent = \"#FF7448\",\n  rounded = 20,\n  headingLevel = 3,\n  className = \"\",\n  style,\n  ...props\n}: BentoGridProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const inView = useInView(rootRef, { once: true, amount: 0.15 });\n  const reduceMotion = useReducedMotion();\n  const Heading = `h${headingLevel}` as \"h2\" | \"h3\" | \"h4\";\n\n  const light = hoverEffect === \"light\" || hoverEffect === \"both\";\n  const lift = hoverEffect === \"lift\" || hoverEffect === \"both\";\n\n  // Writes the pointer position to CSS variables; no React state, so no re-render.\n  const trackPointer = (event: React.PointerEvent<HTMLElement>) => {\n    if (!light || event.pointerType !== \"mouse\") return;\n    const rect = event.currentTarget.getBoundingClientRect();\n    event.currentTarget.style.setProperty(\"--bg-x\", `${event.clientX - rect.left}px`);\n    event.currentTarget.style.setProperty(\"--bg-y\", `${event.clientY - rect.top}px`);\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className={`@container w-full ${className}`}\n      style={{\n        [\"--bg-accent\" as string]: accent,\n        [\"--bg-row\" as string]: `${rowHeight}px`,\n        ...style,\n      }}\n      {...props}\n    >\n      <ul\n        role=\"list\"\n        className={`grid grid-flow-row-dense grid-cols-1 auto-rows-[minmax(var(--bg-row),auto)] @md:grid-cols-2 ${COLUMNS[columns] ?? COLUMNS[4]}`}\n        style={{ gap }}\n      >\n        {items.map((item, index) => {\n          const linked = item.href !== undefined;\n          const tileClass = `group/tile relative isolate flex h-full flex-col border border-zinc-900/10 bg-white text-zinc-900 outline-none dark:border-white/10 dark:bg-[#15181b] dark:text-zinc-100 ${\n            lift\n              ? \"transition-[translate,box-shadow,border-color] duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_18px_40px_-24px_rgb(0_0_0/0.35)] motion-reduce:transition-none motion-reduce:hover:translate-y-0 dark:hover:shadow-[0_18px_40px_-20px_rgb(0_0_0/0.8)]\"\n              : \"transition-[border-color] duration-300\"\n          } ${linked ? \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--bg-accent)]\" : \"\"}`;\n\n          const body = (\n            <>\n              {light && (\n                <>\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-300 group-hover/tile:opacity-100 motion-reduce:transition-none\"\n                    style={{\n                      borderRadius: \"inherit\",\n                      background:\n                        \"radial-gradient(320px circle at var(--bg-x, 50%) var(--bg-y, 0%), color-mix(in oklab, var(--bg-accent) 9%, transparent), transparent 70%)\",\n                    }}\n                  />\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"pointer-events-none absolute -inset-px z-10 opacity-0 transition-opacity duration-300 group-hover/tile:opacity-100 motion-reduce:transition-none\"\n                    style={{\n                      borderRadius: \"inherit\",\n                      padding: 1,\n                      background:\n                        \"radial-gradient(220px circle at calc(var(--bg-x, 50%) + 1px) calc(var(--bg-y, 0%) + 1px), var(--bg-accent), transparent 70%)\",\n                      WebkitMask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n                      WebkitMaskComposite: \"xor\",\n                      mask: \"linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)\",\n                    }}\n                  />\n                </>\n              )}\n              {item.content !== undefined && <div className=\"relative min-h-0 flex-1 overflow-hidden p-5 pb-0\">{item.content}</div>}\n              <div className={`relative p-5 ${item.content === undefined ? \"mt-auto\" : \"\"}`}>\n                <Heading className={`text-[15px] font-medium tracking-tight ${linked ? \"pr-8\" : \"\"}`}>{item.title}</Heading>\n                {item.description && (\n                  <p className=\"mt-1 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400\">{item.description}</p>\n                )}\n                {linked && (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"absolute right-4 top-[17px] grid size-6 place-items-center rounded-full border border-zinc-900/10 text-zinc-500 transition-[translate,color,border-color] duration-300 group-hover/tile:-translate-y-0.5 group-hover/tile:translate-x-0.5 group-hover/tile:border-[var(--bg-accent)] group-hover/tile:text-[var(--bg-accent)] group-focus-visible/tile:text-[var(--bg-accent)] motion-reduce:transition-none dark:border-white/10 dark:text-zinc-400\"\n                  >\n                    <ArrowGlyph />\n                  </span>\n                )}\n              </div>\n            </>\n          );\n\n          const motionProps = {\n            className: tileClass,\n            style: { borderRadius: rounded },\n            onPointerMove: trackPointer,\n            initial: reduceMotion ? false : ({ opacity: 0, y: 18 } as const),\n            animate: reduceMotion || inView ? { opacity: 1, y: 0 } : undefined,\n            transition: { duration: 0.55, ease: [0.22, 1, 0.36, 1] as const, delay: (index * Math.max(stagger, 0)) / 1000 },\n          };\n\n          return (\n            <li key={item.id} className={`min-w-0 ${SPAN[item.span ?? \"1x1\"]}`}>\n              {linked ? (\n                <motion.a href={item.href} {...motionProps}>\n                  {body}\n                </motion.a>\n              ) : (\n                <motion.div {...motionProps}>{body}</motion.div>\n              )}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "cards"
  ],
  "docs": "Documentation: https://wancraft.my/components/bento-grid"
}
