{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-builder",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "bento-builder.tsx",
      "content": "import { Plus, X } from \"lucide-react\";\nimport { motion } from \"motion/react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Tile = {\n  id: string;\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  hue: number;\n  label: string;\n};\n\nconst DEFAULT_COLS = 6;\nconst DEFAULT_ROW_H = 110;\nconst DEFAULT_GAP = 12;\n/** Matches Tailwind's rounded-3xl (1.5rem), the radius this canvas always used before it became adjustable. */\nconst DEFAULT_CORNER_RADIUS = 24;\n\nconst LABELS = [\n  \"Overview\",\n  \"Analytics\",\n  \"Revenue\",\n  \"Uptime\",\n  \"Calendar\",\n  \"Team\",\n];\n\nconst makeTile = (x = 0, y = 0): Tile => ({\n  id: crypto.randomUUID(),\n  x,\n  y,\n  w: 2,\n  h: 2,\n  hue: Math.floor(Math.random() * 360),\n  label: LABELS[Math.floor(Math.random() * LABELS.length)],\n});\n\nconst DEFAULT_PATTERN: Array<{\n  w: number;\n  h: number;\n  hue: number;\n  label: string;\n}> = [\n  { w: 2, h: 2, hue: 265, label: \"Overview\" },\n  { w: 2, h: 1, hue: 200, label: \"Analytics\" },\n  { w: 1, h: 1, hue: 340, label: \"Revenue\" },\n  { w: 1, h: 2, hue: 30, label: \"Uptime\" },\n  { w: 2, h: 1, hue: 160, label: \"Calendar\" },\n  { w: 1, h: 1, hue: 290, label: \"Team\" },\n];\n\n/** Whether two grid rectangles occupy any of the same cells. */\nfunction rectsOverlap(\n  a: { x: number; y: number; w: number; h: number },\n  b: { x: number; y: number; w: number; h: number }\n) {\n  return (\n    a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y\n  );\n}\n\n/** Axis-aligned bounding box of one or more grid rects. */\nfunction boundingBox(\n  rects: Array<{ x: number; y: number; w: number; h: number }>\n) {\n  const minX = Math.min(...rects.map((r) => r.x));\n  const minY = Math.min(...rects.map((r) => r.y));\n  const maxX = Math.max(...rects.map((r) => r.x + r.w));\n  const maxY = Math.max(...rects.map((r) => r.y + r.h));\n  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };\n}\n\n/**\n * True when `tiles` exactly fill `target` — same bounding box, no overhang,\n * and no gaps (combined area equals the target area). Used so a wide tile\n * can swap with a matching-width row of smaller tiles (e.g. 3×1 ↔ 2×1+1×1).\n */\nfunction tilesExactlyFill(\n  tiles: Array<{ x: number; y: number; w: number; h: number }>,\n  target: { x: number; y: number; w: number; h: number }\n) {\n  if (tiles.length === 0) return false;\n  const box = boundingBox(tiles);\n  if (\n    box.x !== target.x ||\n    box.y !== target.y ||\n    box.w !== target.w ||\n    box.h !== target.h\n  ) {\n    return false;\n  }\n  const area = tiles.reduce((sum, t) => sum + t.w * t.h, 0);\n  return area === target.w * target.h;\n}\n\n/** Shrinks w, then h, until the rect at (x, y) no longer overlaps any other tile. */\nfunction clampSizeToAvoidOverlap(\n  others: Array<{ x: number; y: number; w: number; h: number }>,\n  x: number,\n  y: number,\n  w: number,\n  h: number\n) {\n  let clampedW = w;\n  while (\n    clampedW > 1 &&\n    others.some((t) => rectsOverlap({ x, y, w: clampedW, h }, t))\n  ) {\n    clampedW--;\n  }\n  let clampedH = h;\n  while (\n    clampedH > 1 &&\n    others.some((t) => rectsOverlap({ x, y, w: clampedW, h: clampedH }, t))\n  ) {\n    clampedH--;\n  }\n  return { w: clampedW, h: clampedH };\n}\n\n/** Lays the fixed default pattern out left-to-right, wrapping to fit however\n *  many columns the grid currently has (so smaller `cols` values never overflow). */\nfunction buildDefaultTiles(cols: number): Tile[] {\n  let x = 0;\n  let y = 0;\n  let rowSpan = 1;\n\n  return DEFAULT_PATTERN.map((tile, index) => {\n    const w = Math.min(tile.w, cols);\n\n    if (x + w > cols) {\n      x = 0;\n      y += rowSpan;\n      rowSpan = 1;\n    }\n\n    const placed: Tile = {\n      id: String(index + 1),\n      x,\n      y,\n      w,\n      h: tile.h,\n      hue: tile.hue,\n      label: tile.label,\n    };\n\n    x += w;\n    rowSpan = Math.max(rowSpan, tile.h);\n\n    return placed;\n  });\n}\n\ntype ResizeState = {\n  id: string;\n  startX: number;\n  startY: number;\n  origW: number;\n  origH: number;\n};\n\ntype BentoBuilderProps = {\n  /** Number of grid columns. */\n  cols?: number;\n  /** Height of each grid row, in pixels. */\n  rowHeight?: number;\n  /** Gap between tiles, in pixels. */\n  gap?: number;\n  /** Corner radius of every tile, in pixels. */\n  cornerRadius?: number;\n  className?: string;\n  /** Called with the generated layout snippet whenever the canvas changes. */\n  onLayoutChange?: (code: string) => void;\n};\n\nexport function BentoBuilder({\n  cols = DEFAULT_COLS,\n  rowHeight = DEFAULT_ROW_H,\n  gap = DEFAULT_GAP,\n  cornerRadius = DEFAULT_CORNER_RADIUS,\n  className,\n  onLayoutChange,\n}: BentoBuilderProps = {}) {\n  const [tiles, setTiles] = useState<Tile[]>(() => buildDefaultTiles(cols));\n  const [resize, setResize] = useState<ResizeState | null>(null);\n  const [selectedId, setSelectedId] = useState<string | null>(null);\n  const gridRef = useRef<HTMLDivElement>(null);\n\n  const rows = useMemo(\n    () => Math.max(4, ...tiles.map((t) => t.y + t.h)) + 1,\n    [tiles]\n  );\n\n  const cellSize = useCallback(() => {\n    const el = gridRef.current;\n    const w = el ? el.clientWidth : 800;\n    const cellW = (w - gap * (cols - 1)) / cols;\n    return { cellW, cellH: rowHeight };\n  }, [cols, gap, rowHeight]);\n\n  useEffect(() => {\n    onLayoutChange?.(generateCode(tiles, cols, rowHeight, gap, cornerRadius));\n  }, [tiles, cols, rowHeight, gap, cornerRadius, onLayoutChange]);\n\n  const addTile = () => {\n    // find first empty row\n    const maxY = tiles.reduce((m, t) => Math.max(m, t.y + t.h), 0);\n    const tile = makeTile(0, maxY);\n    setTiles((t) => [...t, tile]);\n    // Select it immediately so the rename field is one tap away.\n    setSelectedId(tile.id);\n  };\n  const remove = (id: string) => {\n    setTiles((t) => t.filter((x) => x.id !== id));\n    setSelectedId((current) => (current === id ? null : current));\n  };\n  const rename = (id: string, label: string) =>\n    setTiles((t) => t.map((x) => (x.id === id ? { ...x, label } : x)));\n  const selectTile = (id: string) =>\n    setSelectedId((current) => (current === id ? null : id));\n\n  const commitMove = useCallback((id: string, x: number, y: number) => {\n    setTiles((prev) => {\n      const moving = prev.find((t) => t.id === id);\n      if (!moving) return prev;\n      const movedRect = { x, y, w: moving.w, h: moving.h };\n      const overlapping = prev.filter(\n        (t) => t.id !== id && rectsOverlap(movedRect, t)\n      );\n\n      if (overlapping.length === 0) {\n        return prev.map((t) => (t.id === id ? { ...t, x, y } : t));\n      }\n\n      // 1) Single identically sized tile → simple swap.\n      const [only] = overlapping;\n      const isCleanSwap =\n        overlapping.length === 1 && only.w === moving.w && only.h === moving.h;\n\n      if (isCleanSwap) {\n        return prev.map((t) => {\n          if (t.id === id) return { ...t, x, y };\n          if (t.id === only.id) return { ...t, x: moving.x, y: moving.y };\n          return t;\n        });\n      }\n\n      // 2) Group swap: overlapping tiles exactly fill the drop rect (same\n      // footprint as the mover). Shift the whole group to the mover's old\n      // origin so relative positions stay intact — e.g. Love (3×1) ↔\n      // Calendar (2×1) + Team (1×1).\n      if (!tilesExactlyFill(overlapping, movedRect)) return prev;\n\n      const dx = moving.x - movedRect.x;\n      const dy = moving.y - movedRect.y;\n      const displaced = new Set(overlapping.map((t) => t.id));\n\n      return prev.map((t) => {\n        if (t.id === id) return { ...t, x, y };\n        if (displaced.has(t.id)) return { ...t, x: t.x + dx, y: t.y + dy };\n        return t;\n      });\n    });\n  }, []);\n\n  const onResizeStart = (e: React.PointerEvent, tile: Tile) => {\n    e.preventDefault();\n    e.stopPropagation();\n    (e.target as HTMLElement).setPointerCapture(e.pointerId);\n    setResize({\n      id: tile.id,\n      startX: e.clientX,\n      startY: e.clientY,\n      origW: tile.w,\n      origH: tile.h,\n    });\n  };\n\n  const onResizePointerMove = (e: React.PointerEvent) => {\n    if (!resize) return;\n    const { cellW, cellH } = cellSize();\n    const dx = Math.round((e.clientX - resize.startX) / (cellW + gap));\n    const dy = Math.round((e.clientY - resize.startY) / (cellH + gap));\n    setTiles((prev) => {\n      const tile = prev.find((t) => t.id === resize.id);\n      if (!tile) return prev;\n      const others = prev.filter((t) => t.id !== resize.id);\n      const wantedW = Math.max(1, Math.min(cols - tile.x, resize.origW + dx));\n      const wantedH = Math.max(1, resize.origH + dy);\n      const { w: nw, h: nh } = clampSizeToAvoidOverlap(\n        others,\n        tile.x,\n        tile.y,\n        wantedW,\n        wantedH\n      );\n      return prev.map((t) => (t.id === resize.id ? { ...t, w: nw, h: nh } : t));\n    });\n  };\n\n  const onResizePointerUp = () => setResize(null);\n\n  return (\n    <div\n      className={cn(\"relative w-full p-5 text-foreground sm:p-6\", className)}\n    >\n      <div className=\"mb-6 flex flex-wrap gap-2\">\n        <ToolBtn\n          icon={<Plus className=\"h-4 w-4\" />}\n          label=\"Add tile\"\n          onClick={addTile}\n        />\n      </div>\n\n      {/** biome-ignore lint/a11y/useKeyWithClickEvents: deselecting the background is a pointer-only convenience — tiles themselves stay keyboard-reachable via their remove/rename controls. */}\n      <div\n        className=\"relative grid select-none\"\n        onClick={(e) => {\n          // Deselect only when the empty grid background itself is tapped,\n          // not when the click bubbled up from a tile.\n          if (e.target === e.currentTarget) setSelectedId(null);\n        }}\n        onPointerCancel={onResizePointerUp}\n        onPointerMove={onResizePointerMove}\n        onPointerUp={onResizePointerUp}\n        ref={gridRef}\n        style={{\n          gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,\n          gridAutoRows: `${rowHeight}px`,\n          gap: `${gap}px`,\n          minHeight: rows * (rowHeight + gap),\n        }}\n      >\n        {tiles.map((tile) => (\n          <BentoTile\n            cellSize={cellSize}\n            cols={cols}\n            cornerRadius={cornerRadius}\n            gap={gap}\n            isResizing={resize?.id === tile.id}\n            isSelected={selectedId === tile.id}\n            key={tile.id}\n            onCommitMove={commitMove}\n            onRemove={() => remove(tile.id)}\n            onRename={(label) => rename(tile.id, label)}\n            onResizeStart={onResizeStart}\n            onSelect={() => selectTile(tile.id)}\n            tile={tile}\n          />\n        ))}\n      </div>\n\n      {tiles.length === 0 && (\n        <motion.div\n          animate={{ opacity: 1 }}\n          className=\"mt-20 text-center text-muted-foreground\"\n          initial={{ opacity: 0 }}\n        >\n          Empty canvas — press <span className=\"text-foreground\">Add</span> to\n          place a tile.\n        </motion.div>\n      )}\n\n      <p className=\"mt-6 text-muted-foreground text-xs\">\n        Tip: drag a tile to reposition it — dropping on a same-size tile swaps\n        them, or on a group that matches its footprint (e.g. 3×1 over 2×1+1×1).\n        Tap a tile to rename it, drag its corner handle to resize, and use the ×\n        to remove it — all of this works with touch too. Paste the generated\n        layout into your project; set href on a tile to make it a link, then add\n        descriptions and images.\n      </p>\n    </div>\n  );\n}\n\nfunction BentoTile({\n  tile,\n  cols,\n  cornerRadius,\n  gap,\n  cellSize,\n  isResizing,\n  isSelected,\n  onCommitMove,\n  onRemove,\n  onRename,\n  onResizeStart,\n  onSelect,\n}: {\n  tile: Tile;\n  cols: number;\n  cornerRadius: number;\n  gap: number;\n  cellSize: () => { cellW: number; cellH: number };\n  isResizing: boolean;\n  isSelected: boolean;\n  onCommitMove: (id: string, x: number, y: number) => void;\n  onRemove: () => void;\n  onRename: (label: string) => void;\n  onResizeStart: (e: React.PointerEvent, tile: Tile) => void;\n  onSelect: () => void;\n}) {\n  const [movePx, setMovePx] = useState<{ x: number; y: number } | null>(null);\n  const moveStart = useRef<{ x: number; y: number } | null>(null);\n  const isMoving = movePx !== null;\n\n  const onMovePointerDown = (e: React.PointerEvent) => {\n    const target = e.target as HTMLElement;\n    if (target.dataset.resize || target.dataset.noDrag) return;\n    e.preventDefault();\n    e.currentTarget.setPointerCapture(e.pointerId);\n    moveStart.current = { x: e.clientX, y: e.clientY };\n    setMovePx({ x: 0, y: 0 });\n  };\n\n  const onMovePointerMove = (e: React.PointerEvent) => {\n    if (!moveStart.current) return;\n    setMovePx({\n      x: e.clientX - moveStart.current.x,\n      y: e.clientY - moveStart.current.y,\n    });\n  };\n\n  const onMovePointerUp = () => {\n    const start = moveStart.current;\n    moveStart.current = null;\n    if (!(start && movePx)) {\n      setMovePx(null);\n      return;\n    }\n    const { cellW, cellH } = cellSize();\n    const dx = Math.round(movePx.x / (cellW + gap));\n    const dy = Math.round(movePx.y / (cellH + gap));\n    // Barely-moved pointers are a tap, not a drag — select the tile so its\n    // rename field shows up (this is also what makes remove/resize reachable\n    // on touch, where hover never fires).\n    const isTap = Math.abs(movePx.x) < 6 && Math.abs(movePx.y) < 6;\n    setMovePx(null);\n    if (dx !== 0 || dy !== 0) {\n      const nx = Math.max(0, Math.min(cols - tile.w, tile.x + dx));\n      const ny = Math.max(0, tile.y + dy);\n      onCommitMove(tile.id, nx, ny);\n    } else if (isTap) {\n      onSelect();\n    }\n  };\n\n  const isActive = isMoving || isResizing;\n\n  return (\n    <motion.div\n      className=\"relative\"\n      layout={!isActive}\n      style={{\n        gridColumn: `${tile.x + 1} / span ${tile.w}`,\n        gridRow: `${tile.y + 1} / span ${tile.h}`,\n        zIndex: isActive || isSelected ? 30 : 1,\n      }}\n      transition={{\n        layout: { type: \"spring\", stiffness: 260, damping: 26, mass: 0.9 },\n      }}\n    >\n      <div\n        className={cn(\n          \"group relative h-full w-full cursor-grab touch-none overflow-hidden text-left text-white shadow-[inset_0_1px_0_0_rgba(255,255,255,0.14),0_12px_28px_-16px_rgba(0,0,0,0.55)] ring-1 ring-black/5 ring-inset active:cursor-grabbing\",\n          isSelected && \"outline outline-2 outline-white/70 outline-offset-2\"\n        )}\n        onPointerCancel={onMovePointerUp}\n        onPointerDown={onMovePointerDown}\n        onPointerMove={onMovePointerMove}\n        onPointerUp={onMovePointerUp}\n        style={{\n          background: `oklch(0.55 0.2 ${tile.hue})`,\n          borderRadius: cornerRadius,\n          transform: movePx\n            ? `translate3d(${movePx.x}px, ${movePx.y}px, 0)`\n            : undefined,\n          transition: isMoving ? \"none\" : undefined,\n        }}\n      >\n        {/* Header and footer are pinned to fixed insets from the tile edges\n            (not a flex column pushed apart by height) so every tile — a\n            short 1×1 or a tall 2×2 — gets identical padding. */}\n        <button\n          aria-label=\"Remove tile\"\n          className={cn(\n            \"pointer-events-auto absolute top-3 right-3 flex h-7 w-7 items-center justify-center text-white/80 opacity-0 transition-[opacity,color,transform] duration-150 ease-out hover:text-white active:scale-90 group-hover:opacity-100 sm:top-4 sm:right-4\",\n            isSelected && \"opacity-100\"\n          )}\n          data-no-drag=\"true\"\n          onClick={onRemove}\n          onPointerDown={(e) => e.stopPropagation()}\n          title=\"Remove tile\"\n          type=\"button\"\n        >\n          <X className=\"h-4 w-4 drop-shadow-[0_1px_2px_rgba(0,0,0,0.35)]\" />\n        </button>\n\n        <div className=\"pointer-events-none absolute inset-x-3 bottom-3 flex min-w-0 flex-col gap-1.5 sm:inset-x-4 sm:bottom-4\">\n          {isSelected ? (\n            <input\n              className=\"pointer-events-auto w-full truncate rounded-md bg-transparent font-semibold text-[15px] text-white leading-tight tracking-[-0.01em] outline-none placeholder:text-white/50 focus-visible:ring-1 focus-visible:ring-white/60\"\n              data-no-drag=\"true\"\n              onChange={(e) => onRename(e.target.value)}\n              onClick={(e) => e.stopPropagation()}\n              onPointerDown={(e) => e.stopPropagation()}\n              placeholder=\"Tile name\"\n              value={tile.label}\n            />\n          ) : (\n            <div className=\"truncate font-semibold text-[15px] leading-tight tracking-[-0.01em]\">\n              {tile.label}\n            </div>\n          )}\n          <div className=\"inline-flex w-fit items-center rounded-full bg-black/15 px-2 py-0.5 font-medium text-[10px] text-white/70 tabular-nums tracking-wider\">\n            {tile.w} × {tile.h}\n          </div>\n        </div>\n\n        {/* Resize handle */}\n        <div\n          className={cn(\n            \"absolute right-2 bottom-2 flex h-6 w-6 cursor-nwse-resize items-center justify-center text-white/85 opacity-0 transition-[opacity,color] duration-150 ease-out hover:text-white group-hover:opacity-100\",\n            isSelected && \"opacity-100\"\n          )}\n          data-resize=\"true\"\n          onPointerDown={(e) => onResizeStart(e, tile)}\n          title=\"Drag to resize\"\n        >\n          <svg\n            className=\"pointer-events-none h-3.5 w-3.5 drop-shadow-[0_1px_2px_rgba(0,0,0,0.35)]\"\n            data-resize=\"true\"\n            viewBox=\"0 0 10 10\"\n          >\n            <path\n              d=\"M9 1 L1 9 M9 5 L5 9 M9 9 L9 9\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.2\"\n            />\n          </svg>\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n\nfunction generateCode(\n  tiles: Tile[],\n  cols: number,\n  rowHeight: number,\n  gap: number,\n  cornerRadius: number\n): string {\n  const items = tiles\n    .map(\n      (t) =>\n        `  { x: ${t.x}, y: ${t.y}, w: ${t.w}, h: ${t.h}, hue: ${t.hue}, label: ${JSON.stringify(t.label)} },`\n    )\n    .join(\"\\n\");\n\n  return `// Bento layout — ${cols} columns, ${rowHeight}px row height\nexport type BentoTile = {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  hue: number;\n  label: string;\n  // These aren't set by the visual builder — add them directly here (or to\n  // your own tiles array) once you've pasted this into your project.\n  description?: string;\n  image?: string;\n  /** Turns the tile into a link — renders an <a> instead of a <div>. */\n  href?: string;\n};\n\nconst defaultTiles: BentoTile[] = [\n${items}\n];\n\n// tiles/cols/rowHeight/gap/cornerRadius are props, not hardcoded — edit\n// defaultTiles above, or pass your own <Bento tiles={...} /> from anywhere\n// in your app.\nexport function Bento({\n  tiles = defaultTiles,\n  cols = ${cols},\n  rowHeight = ${rowHeight},\n  gap = ${gap},\n  cornerRadius = ${cornerRadius},\n}: {\n  tiles?: BentoTile[];\n  cols?: number;\n  rowHeight?: number;\n  gap?: number;\n  cornerRadius?: number;\n}) {\n  return (\n    <div\n      className=\"grid\"\n      style={{\n        gridTemplateColumns: \\`repeat(\\${cols}, minmax(0, 1fr))\\`,\n        gridAutoRows: \\`minmax(\\${rowHeight}px, auto)\\`,\n        gap: \\`\\${gap}px\\`,\n      }}\n    >\n      {tiles.map((t, i) => {\n        // The grid placement lives in CSS variables (set below) rather than\n        // inline grid-column/grid-row so the max-sm: overrides — plain\n        // Tailwind utilities — can win on small screens without !important\n        // hacks or an injected <style> tag: each tile collapses to a single\n        // stacked column below 640px and keeps its own content only.\n        const placement = {\n          \"--bento-col\": \\`\\${t.x + 1} / span \\${t.w}\\`,\n          \"--bento-row\": \\`\\${t.y + 1} / span \\${t.h}\\`,\n        } as React.CSSProperties;\n\n        const className =\n          \"group relative block overflow-hidden p-5 text-white no-underline shadow-[inset_0_1px_0_0_rgba(255,255,255,0.14),0_12px_28px_-16px_rgba(0,0,0,0.55)] ring-1 ring-black/5 ring-inset transition-transform duration-200 ease-out [grid-column:var(--bento-col)] [grid-row:var(--bento-row)] max-sm:[grid-column:auto]! max-sm:[grid-row:auto]! motion-safe:hover:-translate-y-1 motion-safe:hover:scale-[1.02]\";\n\n        const style = {\n          ...placement,\n          background: t.image ? undefined : \\`oklch(0.55 0.2 \\${t.hue})\\`,\n          borderRadius: cornerRadius,\n        } as React.CSSProperties;\n\n        const content = (\n          <>\n            {t.image && (\n              <img\n                alt={t.label}\n                className=\"absolute inset-0 h-full w-full object-cover\"\n                height={400}\n                src={t.image}\n                width={400}\n              />\n            )}\n            {t.image && (\n              <div className=\"absolute inset-0 bg-gradient-to-t from-black/75 via-black/15 to-transparent\" />\n            )}\n            <div className=\"relative flex h-full flex-col justify-end gap-1\">\n              <div className=\"font-semibold text-[15px] leading-tight tracking-[-0.01em]\">\n                {t.label}\n              </div>\n              {t.description && (\n                <div className=\"text-sm text-white/80 leading-snug\">\n                  {t.description}\n                </div>\n              )}\n            </div>\n          </>\n        );\n\n        // Rendered as a real <a> (not a wrapped <div>) whenever href is set,\n        // so the tile is keyboard-focusable and works with Cmd/Ctrl-click,\n        // middle-click, and screen readers — the way a link should.\n        return t.href ? (\n          <a className={className} href={t.href} key={i} style={style}>\n            {content}\n          </a>\n        ) : (\n          <div className={className} key={i} style={style}>\n            {content}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n`;\n}\n\nfunction ToolBtn({\n  onClick,\n  icon,\n  label,\n}: {\n  onClick: () => void;\n  icon: React.ReactNode;\n  label: string;\n}) {\n  return (\n    <motion.button\n      aria-label={label}\n      className=\"flex h-9 w-9 items-center justify-center rounded-xl bg-muted text-foreground transition-colors hover:bg-muted/70 active:bg-muted/60\"\n      onClick={onClick}\n      title={label}\n      type=\"button\"\n      whileHover={{ scale: 1.04 }}\n      whileTap={{ scale: 0.94 }}\n    >\n      {icon}\n    </motion.button>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "Bento Builder",
  "description": "Interactive bento-grid layout tool — drag tiles to reposition, tap to rename, drag a corner to resize, add tiles, then export the generated grid layout code to drop straight into your own project."
}
