{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bento-grid","type":"registry:ui","title":"BentoGrid","description":"Grid of framed tiles, each a hairline frame around a softly top-lit panel, where an accent glow rises from below on hover while the content lifts and the call to action slides in, and the tiles fade up in a stagger the first time the grid scrolls into view.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["cards"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/bento-grid/BentoFrame.tsx","content":"\"use client\";\n\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport \"./bento-frame.css\";\n\n/**\n * Internal: the nested double frame both BentoGridItem and BentoGridCard are\n * drawn in. Not exported from index.ts — the two public tiles own their\n * content, this owns the chrome (outer hairline frame, inner lit panel, the\n * accent glow that rises from the bottom on hover, and the bottom-edge light\n * line).\n */\nexport interface BentoFrameProps extends HTMLAttributes<HTMLDivElement> {\n\t/** Classes for the outer frame (the grid item itself). */\n\tclassName?: string;\n\t/** Classes for the inner panel. */\n\tpanelClass?: string;\n\tchildren?: ReactNode;\n}\n\nexport function BentoFrame({\n\tclassName = \"\",\n\tpanelClass = \"\",\n\tchildren,\n\t...rest\n}: BentoFrameProps) {\n\treturn (\n\t\t<div className={cn(\"bento-tile relative flex rounded-2xl p-1.5\", className)} {...rest}>\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"bento-panel relative isolate flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-[10px]\",\n\t\t\t\t\tpanelClass\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<span aria-hidden=\"true\" className=\"bento-glow\"></span>\n\t\t\t\t<span aria-hidden=\"true\" className=\"bento-edge\"></span>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/BentoFrame.tsx"},{"path":"react/src/components/bento-grid/BentoGrid.tsx","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { CSSProperties, FocusEvent, HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useInView } from \"../../internals/motion/in-view\";\nimport { useReducedMotion } from \"../../internals/motion/media-query\";\nimport { staggerDelay } from \"../../internals/motion/stagger\";\nimport \"./bento-grid.css\";\n\nexport interface BentoGridProps extends HTMLAttributes<HTMLDivElement> {\n\tclassName?: string;\n\t/** Tiles fade up in a stagger the first time the grid scrolls into view. */\n\treveal?: boolean;\n\t/** Colour of the hover glow, the lit bottom edge, the icon ring and the CTA arrow. Any CSS colour; writes `--bento-accent`. */\n\taccent?: string;\n\tchildren?: ReactNode;\n}\n\n/** ms per tile, and the total ceiling the stagger is compressed into. */\nconst STEP = 70;\nconst CAP = 420;\n\nexport function BentoGrid({\n\tclassName = \"\",\n\treveal = true,\n\taccent,\n\tchildren,\n\tstyle,\n\tonFocus,\n\t...rest\n}: BentoGridProps) {\n\tconst [node, nodeRef] = useElementRef<HTMLDivElement>();\n\tconst [shown, setShown] = useState(false);\n\n\tconst reduced = useReducedMotion();\n\tconst revealing = reveal && !reduced;\n\n\t// Index every direct element child: `--bento-i` (its position) and\n\t// `--bento-delay` (its capped stagger delay). Re-indexed when the child\n\t// list changes, so a list that grows stays in order.\n\tuseEffect(() => {\n\t\tif (!node) return;\n\t\tconst root = node;\n\t\tfunction apply() {\n\t\t\tconst kids = Array.from(root.children).filter(\n\t\t\t\t(el): el is HTMLElement => el instanceof HTMLElement\n\t\t\t);\n\t\t\tkids.forEach((el, i) => {\n\t\t\t\tel.style.setProperty(\"--bento-i\", String(i));\n\t\t\t\tel.style.setProperty(\n\t\t\t\t\t\"--bento-delay\",\n\t\t\t\t\t`${staggerDelay(i, kids.length, STEP, \"first\", CAP)}ms`\n\t\t\t\t);\n\t\t\t});\n\t\t}\n\t\tapply();\n\t\tconst mo = new MutationObserver(apply);\n\t\tmo.observe(root, { childList: true });\n\t\treturn () => {\n\t\t\tmo.disconnect();\n\t\t\tfor (const el of Array.from(root.children)) {\n\t\t\t\tif (el instanceof HTMLElement) {\n\t\t\t\t\tel.style.removeProperty(\"--bento-i\");\n\t\t\t\t\tel.style.removeProperty(\"--bento-delay\");\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}, [node]);\n\n\tuseInView(node, {\n\t\tenabled: revealing && !shown,\n\t\tonce: true,\n\t\t// Any pixel, not a fraction: a grid taller than ~12 viewports could never\n\t\t// show 8% of itself at once, and would stay hidden for good.\n\t\tthreshold: 0,\n\t\trootMargin: \"0px 0px -6% 0px\",\n\t\tonChange: (v) => {\n\t\t\tif (v) setShown(true);\n\t\t},\n\t});\n\n\t// Focus arriving inside a still-hidden grid reveals it at once, so a\n\t// keyboard user never tabs into invisible tiles. React's onFocus is the\n\t// bubbling focusin.\n\tfunction handleFocus(event: FocusEvent<HTMLDivElement>) {\n\t\tsetShown(true);\n\t\tonFocus?.(event);\n\t}\n\n\tconst mergedStyle =\n\t\taccent === undefined ? style : ({ ...style, \"--bento-accent\": accent } as CSSProperties);\n\n\treturn (\n\t\t<div\n\t\t\t{...rest}\n\t\t\tref={nodeRef}\n\t\t\tclassName={cn(\n\t\t\t\t\"fancy-bento mx-auto grid max-w-7xl grid-cols-1 gap-4 md:auto-rows-[18rem] md:grid-cols-3\",\n\t\t\t\trevealing && \"bento-reveal\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tdata-state={\n\t\t\t\trevealing\n\t\t\t\t\t? shown\n\t\t\t\t\t\t? \"shown\"\n\t\t\t\t\t\t: \"armed\"\n\t\t\t\t\t: (rest as Record<string, string | undefined>)[\"data-state\"]\n\t\t\t}\n\t\t\tstyle={mergedStyle}\n\t\t\tonFocus={handleFocus}\n\t\t>\n\t\t\t{children}\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/BentoGrid.tsx"},{"path":"react/src/components/bento-grid/BentoGridCard.tsx","content":"\"use client\";\n\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { BentoFrame } from \"./BentoFrame\";\n\nexport interface BentoGridCardProps extends HTMLAttributes<HTMLDivElement> {\n\tname: string;\n\tdescription: string;\n\thref: string;\n\tcta: string;\n\tclassName?: string;\n\ticon?: ReactNode;\n\tbackground?: ReactNode;\n}\n\nexport function BentoGridCard({\n\tname,\n\tdescription: desc,\n\thref,\n\tcta,\n\tclassName = \"\",\n\ticon,\n\tbackground,\n\t...rest\n}: BentoGridCardProps) {\n\treturn (\n\t\t<BentoFrame\n\t\t\tclassName={[\"group col-span-3\", className].join(\" \")}\n\t\t\tpanelClass=\"justify-end\"\n\t\t\t{...rest}\n\t\t>\n\t\t\t{/* Not aria-hidden and not inert: the slot can hold real, interactive content, as it always could. */}\n\t\t\t{background && <div className=\"absolute inset-0 -z-10 overflow-hidden\">{background}</div>}\n\n\t\t\t<div className=\"bento-lift pointer-events-none relative z-10 flex flex-col gap-1 p-6\">\n\t\t\t\t{icon && <div className=\"bento-icon mb-3 size-11 [&_svg]:size-5\">{icon}</div>}\n\t\t\t\t<h3 className=\"text-lg font-medium tracking-tight text-neutral-900 dark:text-neutral-100\">\n\t\t\t\t\t{name}\n\t\t\t\t</h3>\n\t\t\t\t<p className=\"max-w-lg text-sm leading-relaxed text-neutral-500 dark:text-neutral-400\">\n\t\t\t\t\t{desc}\n\t\t\t\t</p>\n\t\t\t\t<a\n\t\t\t\t\thref={href}\n\t\t\t\t\tclassName=\"bento-cta pointer-events-auto mt-3 inline-flex w-fit items-center gap-1.5 rounded-md text-sm font-medium text-neutral-900 outline-none focus-visible:ring-2 focus-visible:ring-[var(--bento-accent,#8e9cff)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent dark:text-neutral-100\"\n\t\t\t\t>\n\t\t\t\t\t{cta}\n\t\t\t\t\t<svg\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\t\t\tclassName=\"size-3.5 text-[var(--bento-accent,#8e9cff)]\"\n\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\tstrokeWidth=\"1.6\"\n\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<path d=\"M3 8h9.5M8.5 4l4 4-4 4\" />\n\t\t\t\t\t</svg>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t</BentoFrame>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/BentoGridCard.tsx"},{"path":"react/src/components/bento-grid/BentoGridItem.tsx","content":"\"use client\";\n\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { BentoFrame } from \"./BentoFrame\";\n\nexport interface BentoGridItemProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n\tclassName?: string;\n\theader?: ReactNode;\n\ticon?: ReactNode;\n\ttitle?: ReactNode;\n\tdescription?: ReactNode;\n}\n\nexport function BentoGridItem({\n\tclassName = \"\",\n\theader,\n\ticon,\n\ttitle,\n\tdescription,\n\t...rest\n}: BentoGridItemProps) {\n\treturn (\n\t\t<BentoFrame\n\t\t\tclassName={[\"group/bento row-span-1\", className].join(\" \")}\n\t\t\tpanelClass=\"justify-between gap-4 p-4\"\n\t\t\t{...rest}\n\t\t>\n\t\t\t{header}\n\t\t\t<div className=\"bento-lift relative\">\n\t\t\t\t{(icon || title) && (\n\t\t\t\t\t<div className=\"mb-1.5 flex items-center gap-2.5\">\n\t\t\t\t\t\t{icon && <div className=\"bento-icon size-8 [&_svg]:size-4\">{icon}</div>}\n\t\t\t\t\t\t{title && (\n\t\t\t\t\t\t\t<div className=\"min-w-0 font-sans text-[15px] font-medium tracking-tight text-neutral-900 dark:text-neutral-100\">\n\t\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t{description && (\n\t\t\t\t\t<div className=\"font-sans text-[13px] leading-relaxed text-neutral-500 dark:text-neutral-400\">\n\t\t\t\t\t\t{description}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</BentoFrame>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/BentoGridItem.tsx"},{"path":"react/src/components/bento-grid/bento-frame.css","content":"/*\n * Ported from BentoFrame.svelte's <style> block. Every rule is anchored on\n * the tile's own root class (.bento-tile). The source's compiler-scoped\n * `.bento-panel`, `.bento-glow` and `.bento-edge` selectors are written here\n * as descendants of `.bento-tile` (port-added anchor); `:global(...)` parts\n * become plain selectors, since this file is global CSS to begin with.\n */\n\n/*\n * Colour plumbing. `--_accent` reads the public `--bento-accent` (written by\n * BentoGrid's `accent` prop, or set by a consumer on any ancestor via class)\n * with a soft periwinkle fallback. `--_core` is the white-hot centre of the\n * glow, mixed from the accent.\n */\n.bento-tile {\n\t--_accent: var(--bento-accent, #8e9cff);\n\t--_core: color-mix(in oklab, var(--_accent) 45%, white);\n\t--_frame-bg: var(--bento-frame-bg, #f6f6f7);\n\t--_frame-line: var(--bento-frame-line, rgba(0, 0, 0, 0.08));\n\t--_panel-bg: var(--bento-panel-bg, #fdfdfd);\n\t--_panel-line: var(--bento-panel-line, rgba(0, 0, 0, 0.07));\n\t--_panel-top: rgba(255, 255, 255, 0.95);\n\t--_panel-top-edge: rgba(255, 255, 255, 1);\n\t--_glow-peak: 0.3;\n\n\tbackground: var(--_frame-bg);\n\tborder: 1px solid var(--_frame-line);\n\tbox-shadow:\n\t\t0 1px 2px rgba(15, 15, 20, 0.04),\n\t\t0 12px 32px -18px rgba(15, 15, 20, 0.14);\n\ttransition: border-color 300ms cubic-bezier(0.4, 0, 0.2, 1); /* EASINGS.inout */\n}\n\n.dark .bento-tile {\n\t--_frame-bg: var(--bento-frame-bg, #0f0f10);\n\t--_frame-line: var(--bento-frame-line, rgba(255, 255, 255, 0.08));\n\t--_panel-bg: var(--bento-panel-bg, #141416);\n\t--_panel-line: var(--bento-panel-line, rgba(255, 255, 255, 0.06));\n\t--_panel-top: rgba(255, 255, 255, 0.06);\n\t--_panel-top-edge: rgba(255, 255, 255, 0.08);\n\t--_glow-peak: 0.35;\n\tbox-shadow:\n\t\t0 1px 2px rgba(0, 0, 0, 0.4),\n\t\t0 18px 40px -22px rgba(0, 0, 0, 0.7);\n}\n\n/* Inner panel: its own hairline plus a soft top light. */\n.bento-tile .bento-panel {\n\tbackground: linear-gradient(180deg, var(--_panel-top), transparent 40%), var(--_panel-bg);\n\tborder: 1px solid var(--_panel-line);\n\tbox-shadow: inset 0 1px 0 var(--_panel-top-edge);\n}\n\n/* The glow: a blurred accent radial parked below the panel's bottom edge,\n   white-hot at its centre. Hover raises it 40% → 0 and fades it in. */\n.bento-tile .bento-glow {\n\tposition: absolute;\n\tz-index: -1;\n\tleft: -10%;\n\tright: -10%;\n\tbottom: -22%;\n\theight: 78%;\n\tpointer-events: none;\n\tbackground: radial-gradient(\n\t\t65% 85% at 50% 100%,\n\t\tvar(--_core) 0%,\n\t\tvar(--_accent) 30%,\n\t\tcolor-mix(in oklab, var(--_accent) 40%, transparent) 55%,\n\t\ttransparent 80%\n\t);\n\tfilter: blur(24px);\n\topacity: 0;\n\ttransform: translate3d(0, 40%, 0);\n\ttransition:\n\t\topacity 500ms cubic-bezier(0.4, 0, 0.2, 1),\n\t\ttransform 700ms cubic-bezier(0.16, 1, 0.3, 1); /* EASINGS.out */\n}\n\n/* The bottom edge catches the light as the glow arrives. */\n.bento-tile .bento-edge {\n\tposition: absolute;\n\tleft: 14%;\n\tright: 14%;\n\tbottom: 0;\n\theight: 1px;\n\tpointer-events: none;\n\tbackground: linear-gradient(\n\t\t90deg,\n\t\ttransparent,\n\t\tvar(--_accent) 25%,\n\t\tvar(--_core) 50%,\n\t\tvar(--_accent) 75%,\n\t\ttransparent\n\t);\n\topacity: 0;\n\ttransition: opacity 500ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.bento-tile:hover,\n.bento-tile:focus-within {\n\t--_frame-line: var(--bento-frame-line-hover, rgba(0, 0, 0, 0.12));\n}\n.dark .bento-tile:hover,\n.dark .bento-tile:focus-within {\n\t--_frame-line: var(--bento-frame-line-hover, rgba(255, 255, 255, 0.13));\n}\n\n.bento-tile:hover .bento-glow,\n.bento-tile:focus-within .bento-glow {\n\topacity: var(--_glow-peak);\n\ttransform: translate3d(0, 0, 0);\n}\n.bento-tile:hover .bento-edge,\n.bento-tile:focus-within .bento-edge {\n\topacity: 0.85;\n}\n\n/*\n * Shared content parts, styled here so Item and Card stay identical.\n * They live in the tiles' own markup.\n */\n\n/* Content lift. */\n.bento-tile .bento-lift {\n\ttransition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* The icon sits in a small inset tile: darker well, inset hairline. */\n.bento-tile .bento-icon {\n\tdisplay: grid;\n\tplace-items: center;\n\tflex: none;\n\tborder-radius: 10px;\n\tcolor: #71717a;\n\tbackground: #f0f0f2;\n\tborder: 1px solid rgba(0, 0, 0, 0.07);\n\tbox-shadow:\n\t\tinset 0 1px 2px rgba(0, 0, 0, 0.06),\n\t\t0 1px 0 rgba(255, 255, 255, 0.9);\n\ttransition:\n\t\tcolor 300ms cubic-bezier(0.4, 0, 0.2, 1),\n\t\tborder-color 300ms cubic-bezier(0.4, 0, 0.2, 1),\n\t\tbackground-color 300ms cubic-bezier(0.4, 0, 0.2, 1),\n\t\tbox-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n.dark .bento-tile .bento-icon {\n\tcolor: #8a8a93;\n\tbackground: #0b0b0c;\n\tborder-color: rgba(255, 255, 255, 0.07);\n\tbox-shadow:\n\t\tinset 0 1px 3px rgba(0, 0, 0, 0.6),\n\t\t0 1px 0 rgba(255, 255, 255, 0.04);\n}\n.bento-tile:is(:hover, :focus-within) .bento-icon {\n\tcolor: #18181b;\n\tbackground: #fff;\n\tborder-color: color-mix(in oklab, var(--_accent) 45%, rgba(0, 0, 0, 0.08));\n\tbox-shadow:\n\t\tinset 0 1px 2px rgba(0, 0, 0, 0.04),\n\t\t0 0 0 3px color-mix(in oklab, var(--_accent) 14%, transparent);\n}\n.dark .bento-tile:is(:hover, :focus-within) .bento-icon {\n\tcolor: #fafafa;\n\tbackground: #19191c;\n\tborder-color: color-mix(in oklab, var(--_accent) 40%, rgba(255, 255, 255, 0.1));\n\tbox-shadow:\n\t\tinset 0 1px 0 rgba(255, 255, 255, 0.06),\n\t\t0 0 14px -4px color-mix(in oklab, var(--_accent) 55%, transparent);\n}\n\n/* The CTA slides in from the left and fades up. */\n.bento-tile .bento-cta {\n\topacity: 0;\n\ttransform: translate3d(-8px, 0, 0);\n\ttransition:\n\t\topacity 400ms cubic-bezier(0.4, 0, 0.2, 1),\n\t\ttransform 500ms cubic-bezier(0.16, 1, 0.3, 1),\n\t\tcolor 300ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n.bento-tile:is(:hover, :focus-within) .bento-cta {\n\topacity: 1;\n\ttransform: translate3d(0, 0, 0);\n}\n/* No hover on touch screens: keep the CTA visible. */\n@media (hover: none) {\n\t.bento-tile .bento-cta {\n\t\topacity: 1;\n\t\ttransform: none;\n\t}\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.bento-tile:is(:hover, :focus-within) .bento-lift {\n\t\ttransform: translate3d(0, -6px, 0);\n\t}\n}\n\n/* Reduced motion: the glow still arrives, but only as a fade — it sits in\n   its final place from the start. */\n@media (prefers-reduced-motion: reduce) {\n\t.bento-tile .bento-glow {\n\t\ttransform: none;\n\t\ttransition: opacity 300ms linear;\n\t}\n\t.bento-tile .bento-cta {\n\t\ttransform: none;\n\t\ttransition: opacity 300ms linear;\n\t}\n\t.bento-tile .bento-lift {\n\t\ttransition: none;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/bento-frame.css"},{"path":"react/src/components/bento-grid/bento-grid.css","content":"/*\n * Ported from BentoGrid.svelte's <style> block. The rules were already\n * anchored on the grid's own root class (.fancy-bento) there; Svelte's\n * `:global(*)` child selector becomes a plain `*`.\n *\n * The hidden state exists only for viewers who allow motion and whose\n * browser runs scripts — everyone else gets the resting, visible grid\n * without waiting on JS. Transforms and opacity only.\n */\n@media (prefers-reduced-motion: no-preference) and (scripting: enabled) {\n\t.fancy-bento.bento-reveal[data-state=\"armed\"] > * {\n\t\topacity: 0;\n\t\ttransform: translate3d(0, 18px, 0) scale(0.985);\n\t}\n\t.fancy-bento.bento-reveal[data-state=\"shown\"] > * {\n\t\ttransition:\n\t\t\topacity 600ms cubic-bezier(0.4, 0, 0.2, 1),\n\t\t\ttransform 800ms cubic-bezier(0.16, 1, 0.3, 1),\n\t\t\tborder-color 300ms cubic-bezier(0.4, 0, 0.2, 1); /* DURATIONS.entrance, EASINGS.out */\n\t\ttransition-delay: var(--bento-delay, 0ms), var(--bento-delay, 0ms), 0ms;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/bento-grid.css"},{"path":"react/src/components/bento-grid/index.ts","content":"\"use client\";\n\nexport { BentoGrid } from \"./BentoGrid\";\nexport type { BentoGridProps } from \"./BentoGrid\";\nexport { BentoGridItem } from \"./BentoGridItem\";\nexport type { BentoGridItemProps } from \"./BentoGridItem\";\nexport { BentoGridCard } from \"./BentoGridCard\";\nexport type { BentoGridCardProps } from \"./BentoGridCard\";\n","type":"registry:ui","target":"@ui/fancy/components/bento-grid/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/bento-grid?framework=react"}}