{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ripple-grid",
  "title": "Ripple Grid",
  "description": "A field of cells that contracts and illuminates around the pointer.",
  "registryDependencies": [
    "@rivelle/utils"
  ],
  "files": [
    {
      "path": "registry/nova/effects/ripple-grid.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype RippleGridProps = React.ComponentProps<\"div\"> & {\n  columns?: number;\n  rows?: number;\n};\n\nfunction RippleGrid({\n  className,\n  columns = 12,\n  onPointerLeave,\n  onPointerMove,\n  rows = 8,\n  ...props\n}: RippleGridProps) {\n  const [active, setActive] = React.useState<number | null>(null);\n  const cells = Array.from({ length: columns * rows });\n\n  return (\n    <div\n      className={cn(\n        \"grid overflow-hidden rounded-[2rem] border border-foreground/10 bg-background\",\n        className,\n      )}\n      data-active-index={active ?? undefined}\n      data-slot=\"ripple-grid\"\n      onPointerLeave={(event) => {\n        setActive(null);\n        onPointerLeave?.(event);\n      }}\n      onPointerMove={(event) => {\n        const bounds = event.currentTarget.getBoundingClientRect();\n        const column = Math.min(\n          columns - 1,\n          Math.max(\n            0,\n            Math.floor(\n              ((event.clientX - bounds.left) / bounds.width) * columns,\n            ),\n          ),\n        );\n        const row = Math.min(\n          rows - 1,\n          Math.max(\n            0,\n            Math.floor(((event.clientY - bounds.top) / bounds.height) * rows),\n          ),\n        );\n        setActive(row * columns + column);\n        onPointerMove?.(event);\n      }}\n      style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}\n      {...props}\n    >\n      {cells.map((_, index) => {\n        const activeRow = active === null ? 0 : Math.floor(active / columns);\n        const activeColumn = active === null ? 0 : active % columns;\n        const row = Math.floor(index / columns);\n        const column = index % columns;\n        const distance =\n          active === null\n            ? 0\n            : Math.hypot(row - activeRow, column - activeColumn);\n\n        return (\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"aspect-square border-b border-r border-foreground/[.07] bg-transparent transition-[background-color,transform,box-shadow] duration-500 ease-out will-change-transform motion-reduce:transition-none\",\n              active !== null &&\n                distance < 4.8 &&\n                \"scale-[.88] rounded-md bg-primary/[.22] shadow-[inset_0_0_28px_color-mix(in_oklch,var(--primary)_28%,transparent),0_0_24px_color-mix(in_oklch,var(--primary)_14%,transparent)]\",\n              active !== null &&\n                distance < 1.2 &&\n                \"scale-[.68] rounded-lg bg-primary/75\",\n            )}\n            key={index}\n            style={{\n              transitionDelay:\n                active === null ? \"0ms\" : `${Math.round(distance * 28)}ms`,\n            }}\n          />\n        );\n      })}\n    </div>\n  );\n}\n\nexport { RippleGrid };\nexport type { RippleGridProps };\n",
      "type": "registry:ui",
      "target": "@effects/ripple-grid.tsx"
    }
  ],
  "type": "registry:ui"
}