{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bento-grid","type":"registry:block","title":"Bento Grid","description":"Bento layout of feature tiles on a container-query grid that collapses to one column in narrow containers, with per-card column and row spans, icon, title, description and a visual slot that lifts and zooms on hover, a radial spotlight that follows the cursor across the border and surface, a CTA whose arrow nudges on hover, and cards that stagger in when scrolled into view.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/bento-grid.tsx","type":"registry:component","target":"@components/motion/bento-grid.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/bento-grid\n\nimport { ArrowRight } from \"lucide-react\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport { type MouseEvent, type ReactNode, useState } from \"react\";\nimport { EASE_OUT, EASE_OUT_CSS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport type BentoSpan = 1 | 2 | 3 | 4;\n\nexport interface BentoGridProps {\n  children: ReactNode;\n  /** Columns once the grid's own container is at least 32rem wide. Default 3. */\n  columns?: 2 | 3 | 4;\n  /** Stagger the cards in when the grid scrolls into view. Default true. */\n  animateOnView?: boolean;\n  className?: string;\n}\n\n// Static class maps: Tailwind only ships classes it can see written out.\nconst GRID_COLS: Record<2 | 3 | 4, string> = {\n  2: \"@lg:grid-cols-2\",\n  3: \"@lg:grid-cols-3\",\n  4: \"@lg:grid-cols-4\",\n};\nconst COL_SPAN: Record<BentoSpan, string> = {\n  1: \"\",\n  2: \"@lg:col-span-2\",\n  3: \"@lg:col-span-3\",\n  4: \"@lg:col-span-4\",\n};\nconst ROW_SPAN: Record<BentoSpan, string> = {\n  1: \"\",\n  2: \"@lg:row-span-2\",\n  3: \"@lg:row-span-3\",\n  4: \"@lg:row-span-4\",\n};\n\nconst STAGGER = 0.06;\n\n/**\n * Responsive bento layout. The grid reads its own container width (not the\n * viewport), so it collapses to one column inside a narrow sidebar or card\n * just as it does on a phone. Cards stagger in once when scrolled into view.\n */\nexport function BentoGrid({\n  children,\n  columns = 3,\n  animateOnView = true,\n  className,\n}: BentoGridProps) {\n  return (\n    <div className={cn(\"@container w-full\", className)}>\n      <motion.div\n        initial={animateOnView ? \"hidden\" : false}\n        whileInView=\"show\"\n        viewport={{ once: true, amount: 0.2 }}\n        variants={{ show: { transition: { staggerChildren: STAGGER } } }}\n        className={cn(\n          \"grid grid-cols-1 auto-rows-[minmax(9rem,auto)] gap-3\",\n          GRID_COLS[columns],\n        )}\n      >\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n\nexport interface BentoCardProps {\n  title: ReactNode;\n  description?: ReactNode;\n  /** Small icon shown in a tile above the title. */\n  icon?: ReactNode;\n  /** Decorative visual (mini chart, illustration). Lifts and zooms on hover. */\n  visual?: ReactNode;\n  /** Makes the whole card a link via the CTA. */\n  href?: string;\n  /** Makes the whole card a button via the CTA (ignored when `href` is set). */\n  onClick?: () => void;\n  /** CTA label. Default \"Learn more\". Only shown with `href` or `onClick`. */\n  cta?: ReactNode;\n  /** Columns spanned at wide container sizes. Default 1. */\n  colSpan?: BentoSpan;\n  /** Rows spanned at wide container sizes. Default 1. */\n  rowSpan?: BentoSpan;\n  /** Spotlight color (any CSS color). Default `var(--primary)`. */\n  spotlightColor?: string;\n  className?: string;\n}\n\nconst SPOT_SIZE = 280; // px diameter of the cursor spotlight\n\n/**\n * A bento tile. On hover-capable devices a radial spotlight follows the\n * cursor, lighting the border and faintly washing the surface; the visual\n * slot lifts slightly. With `href`/`onClick` the CTA is the real link or\n * button and stretches over the whole card, so the tile stays one tab stop.\n */\nexport function BentoCard({\n  title,\n  description,\n  icon,\n  visual,\n  href,\n  onClick,\n  cta = \"Learn more\",\n  colSpan = 1,\n  rowSpan = 1,\n  spotlightColor = \"var(--primary)\",\n  className,\n}: BentoCardProps) {\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // The spotlight tracks the cursor; it is decorative, so touch devices\n  // (phantom hover) skip it. Reduced motion keeps it: it follows the pointer\n  // 1:1 with no travel of its own, so nothing moves that the user didn't move.\n  const spotlight = canHover;\n  const lift = canHover && !reduce;\n  const [hovered, setHovered] = useState(false);\n  // Spotlight position as transforms on the glow layers, not a re-rendered\n  // gradient: the circles translate, so the browser only composites.\n  const x = useMotionValue(-SPOT_SIZE);\n  const y = useMotionValue(-SPOT_SIZE);\n\n  const onMove = (event: MouseEvent<HTMLElement>) => {\n    if (!spotlight) return;\n    const rect = event.currentTarget.getBoundingClientRect();\n    x.set(event.clientX - rect.left - SPOT_SIZE / 2);\n    y.set(event.clientY - rect.top - SPOT_SIZE / 2);\n  };\n\n  const item: Variants = {\n    hidden: { opacity: 0, y: reduce ? 0 : 14 },\n    show: {\n      opacity: 1,\n      y: 0,\n      transition: { duration: 0.45, ease: EASE_OUT },\n    },\n  };\n\n  const interactive = Boolean(href || onClick);\n  const ctaClass =\n    // Stretched link: the CTA stays static so its ::after covers the card\n    // (the nearest positioned ancestor), making the whole tile clickable.\n    \"mt-3 inline-flex w-fit items-center gap-1 rounded-md text-sm font-medium text-foreground outline-none after:absolute after:inset-0 after:z-30 after:rounded-[inherit] after:content-[''] focus-visible:ring-2 focus-visible:ring-primary\";\n  const ctaInner = (\n    <>\n      {cta}\n      <ArrowRight\n        aria-hidden\n        className={cn(\n          \"size-3.5 transition-transform duration-200\",\n          !reduce && \"group-hover/bento:translate-x-0.5 group-focus-within/bento:translate-x-0.5\",\n        )}\n        style={{ transitionTimingFunction: EASE_OUT_CSS }}\n      />\n    </>\n  );\n\n  const spot = (opacityClass: string) => (\n    <motion.span\n      aria-hidden\n      style={{\n        x,\n        y,\n        width: SPOT_SIZE,\n        height: SPOT_SIZE,\n        background: `radial-gradient(circle closest-side, ${spotlightColor}, transparent)`,\n      }}\n      className={cn(\"pointer-events-none absolute left-0 top-0 rounded-full\", opacityClass)}\n    />\n  );\n\n  return (\n    <motion.article\n      variants={item}\n      onMouseMove={onMove}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      className={cn(\n        \"group/bento relative isolate flex flex-col overflow-hidden rounded-2xl border border-border bg-card p-4 text-card-foreground\",\n        interactive && \"cursor-pointer\",\n        COL_SPAN[colSpan],\n        ROW_SPAN[rowSpan],\n        className,\n      )}\n    >\n      {spotlight ? (\n        <>\n          {/* Surface wash */}\n          <span\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute inset-0 -z-10 overflow-hidden transition-opacity duration-300\",\n              hovered ? \"opacity-[0.07]\" : \"opacity-0\",\n            )}\n          >\n            {spot(\"\")}\n          </span>\n          {/* Border light: the same circle, masked down to a 1px ring. */}\n          <span\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute inset-0 z-20 overflow-hidden rounded-[inherit] p-px transition-opacity duration-300 [mask-composite:exclude] [mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)]\",\n              hovered ? \"opacity-100\" : \"opacity-0\",\n            )}\n          >\n            {spot(\"opacity-80\")}\n          </span>\n        </>\n      ) : null}\n\n      {visual ? (\n        <div\n          aria-hidden\n          className={cn(\n            \"relative mb-3 flex min-h-0 flex-1 items-center justify-center transition-transform duration-500\",\n            lift && \"group-hover/bento:-translate-y-1 group-hover/bento:scale-[1.03]\",\n          )}\n          style={{ transitionTimingFunction: EASE_OUT_CSS }}\n        >\n          {visual}\n        </div>\n      ) : null}\n\n      <div className={cn(\"flex flex-col\", !visual && \"mt-auto\")}>\n        <div className=\"flex items-center gap-2\">\n          {icon ? (\n            <span className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-lg border border-border bg-muted text-foreground [&_svg]:size-3.5\">\n              {icon}\n            </span>\n          ) : null}\n          <h3 className=\"text-sm font-semibold leading-snug\">{title}</h3>\n        </div>\n        {description ? (\n          <p className=\"mt-1 text-xs leading-relaxed text-muted-foreground\">\n            {description}\n          </p>\n        ) : null}\n        {href ? (\n          <a href={href} className={ctaClass}>\n            {ctaInner}\n          </a>\n        ) : onClick ? (\n          <button type=\"button\" onClick={onClick} className={ctaClass}>\n            {ctaInner}\n          </button>\n        ) : null}\n      </div>\n    </motion.article>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}