{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "platform-grid",
  "title": "Platform Grid",
  "author": "AlgorizeTech",
  "description": "Everything a product offers as one hairline grid: small icon-and-name tiles and a few 2×2 features that show a piece of the product on a tilting card. Reveals as a wave from the centre, then a spotlight follows the pointer. Use it as the 'full platform' section of a product site.",
  "dependencies": [],
  "registryDependencies": [
    "https://avaraui.com/r/icons.json",
    "https://avaraui.com/r/tilted-card.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/PlatformGridBlock.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { TiltedCard } from \"./TiltedCardBlock\";\n\n// ==========================================\n// PLATFORM GRID\n// Everything a product offers, as one hairline grid: small tiles with an icon\n// and a name, and a few 2×2 features that show a piece of the product on a\n// tilting card. Tiles pack densely, so the features stay square at every\n// column count. The grid reveals once as a wave from its centre, then a\n// spotlight follows the pointer across it.\n// ==========================================\n\n// ------------------------------------------\n// Tile — an icon and a name\n// ------------------------------------------\nexport interface PlatformTileProps extends React.HTMLAttributes<HTMLElement> {\n  icon: React.ReactNode;\n  label: string;\n  /** Render as a link. */\n  href?: string;\n  /** Swap the anchor for a framework Link. */\n  linkComponent?: React.ElementType;\n}\n\nfunction Tile({ icon, label, href, linkComponent, className, ...props }: PlatformTileProps) {\n  const Comp: React.ElementType = href ? (linkComponent ?? \"a\") : \"div\";\n  return (\n    <Comp\n      data-tile=\"\"\n      {...(href ? { href } : {})}\n      className={cn(\n        \"group relative flex aspect-square flex-col items-center justify-center gap-2.5 border-b border-r border-foreground/[0.08] px-2 text-center outline-none transition-colors duration-300 hover:bg-foreground/[0.03] focus-visible:bg-foreground/[0.03]\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"grid h-6 w-6 place-items-center text-foreground/40 transition-all duration-300 ease-out [&>svg]:h-[18px] [&>svg]:w-[18px] group-hover:-translate-y-0.5 group-hover:scale-110 group-hover:text-primary group-focus-visible:text-primary\">\n        {icon}\n      </span>\n      <span className=\"text-[11px] font-medium leading-tight text-foreground/50 transition-colors duration-300 group-hover:text-foreground\">\n        {label}\n      </span>\n    </Comp>\n  );\n}\n\n// ------------------------------------------\n// Feature — a 2×2 tile with a tilting card\n// ------------------------------------------\nexport interface PlatformFeatureProps extends React.HTMLAttributes<HTMLElement> {\n  icon: React.ReactNode;\n  label: string;\n  href?: string;\n  linkComponent?: React.ElementType;\n  /** What sits on the card — see `PlatformGrid.Mock` for the frame. */\n  children: React.ReactNode;\n}\n\nfunction Feature({ icon, label, href, linkComponent, className, children, ...props }: PlatformFeatureProps) {\n  const Comp: React.ElementType = href ? (linkComponent ?? \"a\") : \"div\";\n  return (\n    <Comp\n      data-tile=\"\"\n      {...(href ? { href } : {})}\n      className={cn(\n        \"group relative col-span-2 row-span-2 flex aspect-square flex-col items-center justify-between border-b border-r border-foreground/[0.08] bg-gradient-to-b from-muted to-background px-2 pb-4 pt-6 outline-none transition-colors duration-300\",\n        className,\n      )}\n      {...props}\n    >\n      {/* The card leans until the pointer arrives, then straightens and tilts with it. */}\n      <div className=\"flex w-full flex-1 items-center justify-center\">\n        <div className=\"w-[76%] max-w-[15rem] -rotate-[4deg] transition-transform duration-500 ease-out group-hover:rotate-0\">\n          <TiltedCard tiltIntensity={9} glareOpacity={0.12} scaleOnHover={1.03} className=\"h-auto\">\n            {children}\n          </TiltedCard>\n        </div>\n      </div>\n      <span className=\"flex items-center gap-2.5 text-[15px] font-semibold tracking-tight text-foreground\">\n        <span className=\"grid h-6 w-fit place-items-center [&>svg]:h-5 [&>svg]:w-5\">{icon}</span>\n        {label}\n      </span>\n    </Comp>\n  );\n}\n\n// ------------------------------------------\n// Mock and Pill — what goes on a feature's card\n// ------------------------------------------\nfunction Mock({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\n        \"space-y-1.5 rounded-xl border border-foreground/[0.08] bg-card p-3 text-left text-card-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction Pill({\n  tone = \"ink\",\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement> & { tone?: \"ink\" | \"accent\" | \"soft\" }) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex shrink-0 items-center rounded-full px-1.5 py-[3px] text-[7.5px] font-semibold leading-none\",\n        tone === \"ink\" && \"bg-foreground text-background\",\n        tone === \"accent\" && \"bg-primary text-primary-foreground\",\n        tone === \"soft\" && \"bg-foreground/[0.08] font-medium text-foreground/60\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\n// ------------------------------------------\n// The reveal: each tile rises in after its own delay (`--tile-at`), with\n// GSAP's power3.out as a CSS easing.\n// ------------------------------------------\nconst EASE_OUT =\n  \"linear(0, .1945, .3591, .4971, .6115, .7052, .7808, .8409, .8877, .9233, .9497, .9686, .9816, .9901, .9952, .998, .9994, .9999, 1, 1)\";\n\nconst REVEAL_CSS = `\n@keyframes platform-tile-in { from { opacity: 0; transform: translateY(10px) scale(0.9); } }\n[data-tile-reveal] [data-tile] { animation: platform-tile-in 0.7s ${EASE_OUT} var(--tile-at, 0s) backwards; }\n`;\n\n/**\n * Delays for a wave from the centre of a grid, `each` seconds apart: GSAP's\n * `stagger: { each, grid: \"auto\", from: \"center\" }`, step for step. The\n * column count is read from where the tiles sit — the first tile that is not\n * further right than the one before it starts the second row.\n */\nfunction centreWave(tiles: HTMLElement[], each: number) {\n  const l = tiles.length;\n  if (!l) return [];\n  let w = 0;\n  let max = -Infinity;\n  while (max < (max = tiles[w++].getBoundingClientRect().left) && w < l) {\n    // walk along the first row\n  }\n  if (w < l) w--;\n  const originX = Math.min(w, l) * 0.5 - 0.5;\n  const originY = (l * 0.5) / w - 0.5;\n  const d = tiles.map((_, j) => Math.hypot((j % w) - originX, originY - Math.floor(j / w)));\n  const min = Math.min(...d);\n  const range = Math.max(...d) - min;\n  const total = each * (w > l ? l - 1 : Math.max(w, l / w));\n  return d.map((dist) => Math.round((((dist - min) / range || 0) * total) * 1e7) / 1e7);\n}\n\n// ------------------------------------------\n// Root\n// ------------------------------------------\nexport interface PlatformGridProps extends React.HTMLAttributes<HTMLElement> {\n  eyebrow?: React.ReactNode;\n  heading: React.ReactNode;\n  intro?: React.ReactNode;\n  /** Accent for hovered icons, pills and the spotlight; defaults to the theme's primary. */\n  accent?: string;\n  /** Play the reveal when the section enters the viewport (default) or immediately. */\n  reveal?: \"in-view\" | \"immediate\";\n  /** Fade the outer columns, so the grid reads as a window onto something larger. */\n  fadeEdges?: boolean;\n  children: React.ReactNode;\n}\n\nexport function PlatformGrid({\n  eyebrow,\n  heading,\n  intro,\n  accent,\n  reveal = \"in-view\",\n  fadeEdges = true,\n  className,\n  style,\n  children,\n  ...props\n}: PlatformGridProps) {\n  const sectionRef = useRef<HTMLElement>(null);\n  const gridRef = useRef<HTMLDivElement>(null);\n  const [inView, setInView] = useState(reveal === \"immediate\");\n\n  useEffect(() => {\n    if (inView) return;\n    const el = sectionRef.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(\n      (entries) => {\n        if (entries.some((e) => e.isIntersecting)) {\n          setInView(true);\n          io.disconnect();\n        }\n      },\n      { threshold: 0.15 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [inView]);\n\n  // The reveal: every tile rises in, in a wave from the centre of the grid.\n  // Tiles render opaque on the server; this only runs once the page is live.\n  useEffect(() => {\n    if (!inView) return;\n    const grid = gridRef.current;\n    if (!grid) return;\n    const reduced =\n      typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n    if (reduced) return;\n    const tiles = Array.from(grid.querySelectorAll<HTMLElement>(\"[data-tile]\"));\n    const delays = centreWave(tiles, 0.08);\n    tiles.forEach((tile, i) => tile.style.setProperty(\"--tile-at\", `${delays[i]}s`));\n    // Written here rather than rendered, so it lands together with the start\n    // times above and React never has to re-render to start the reveal.\n    grid.setAttribute(\"data-tile-reveal\", \"\");\n  }, [inView]);\n\n  // The spotlight is driven by CSS variables written straight to the grid, so\n  // moving the pointer never re-renders the tiles.\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = e.currentTarget;\n    const r = el.getBoundingClientRect();\n    el.style.setProperty(\"--spot-x\", `${e.clientX - r.left}px`);\n    el.style.setProperty(\"--spot-y\", `${e.clientY - r.top}px`);\n    el.style.setProperty(\"--spot-on\", \"1\");\n  };\n  const onLeave = (e: React.MouseEvent<HTMLDivElement>) => {\n    e.currentTarget.style.setProperty(\"--spot-on\", \"0\");\n  };\n\n  return (\n    <section\n      ref={sectionRef}\n      className={cn(\"w-full bg-background text-foreground\", className)}\n      style={accent ? ({ ...style, \"--primary\": accent } as React.CSSProperties) : style}\n      {...props}\n    >\n      {(eyebrow || heading || intro) && (\n        <header className=\"mx-auto max-w-2xl px-6 pb-10 pt-14 text-center sm:pb-12 sm:pt-16\">\n          {eyebrow && (\n            <p className=\"text-[13px] font-medium uppercase tracking-[0.12em] text-foreground/55\">{eyebrow}</p>\n          )}\n          <h2 className=\"mt-3 font-sans text-[2rem] font-medium leading-[1.1] tracking-[-0.03em] sm:text-[2.5rem]\">\n            {heading}\n          </h2>\n          {intro && (\n            <p className=\"mx-auto mt-4 max-w-[30rem] text-[15px] leading-[1.7] text-foreground/60\">{intro}</p>\n          )}\n        </header>\n      )}\n\n      <div\n        className={cn(\n          \"relative mx-auto max-w-[1180px]\",\n          fadeEdges &&\n            \"lg:[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)] lg:[-webkit-mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]\",\n        )}\n      >\n        <style>{REVEAL_CSS}</style>\n        <div\n          ref={gridRef}\n          onMouseMove={onMove}\n          onMouseLeave={onLeave}\n          className=\"relative grid grid-cols-2 border-l border-t border-foreground/[0.08] [grid-auto-flow:dense] sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-8 xl:grid-cols-8\"\n        >\n          {/* The spotlight: a soft disc of the accent under the pointer. */}\n          <div\n            aria-hidden\n            className=\"pointer-events-none absolute inset-0 z-0 opacity-[var(--spot-on,0)] transition-opacity duration-500 motion-reduce:hidden\"\n            style={{\n              background:\n                \"radial-gradient(240px circle at var(--spot-x, 50%) var(--spot-y, 50%), color-mix(in oklab, var(--primary) 9%, transparent), transparent 70%)\",\n            }}\n          />\n          {children}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nPlatformGrid.Tile = Tile;\nPlatformGrid.Feature = Feature;\nPlatformGrid.Mock = Mock;\nPlatformGrid.Pill = Pill;\n\nexport default PlatformGrid;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/PlatformGridBlock.tsx"
    }
  ],
  "docs": "Named export: PlatformGrid, with parts Tile, Feature, Mock and Pill. Composed from the tilted-card item, which installs alongside it. Cells pack densely (grid-auto-flow: dense) so features stay square at 8, 6, 4, 3 or 2 columns; `reveal` is in-view or immediate; `fadeEdges` masks the outer columns; `accent` pins the hover/spotlight colour, otherwise it follows --primary. Tiles with `href` are links (`linkComponent` for a router Link).",
  "categories": [
    "marketing",
    "navigation"
  ],
  "type": "registry:component"
}
