{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "interactive-grid-pattern",
  "type": "registry:ui",
  "title": "Interactive Grid Pattern",
  "description": "A decorative grid of `columns × rows` SVG squares (24 × 24 by default) that fills the cell under the pointer. Cells inherit `currentColor`, so `className=\"text-border\"` sets the hue and `lineOpacity` / `hoverOpacity` tune it.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "decorative",
    "pattern"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/patterns/interactive-grid-pattern.tsx",
      "target": "components/ui/patterns/interactive-grid-pattern.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  useCallback,\n  useId,\n  useState,\n} from \"react\";\n\n// @interlace/interactive-grid-pattern v1.2.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/interactive-grid-pattern\n// What changed since: https://ds.interlace.tools/c/interactive-grid-pattern#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — InteractiveGridPattern\n *\n * A decorative grid of `columns × rows` SVG squares (24 × 24 by default) that\n * fills the cell under the pointer. Cells inherit `currentColor`, so\n * `className=\"text-border\"` sets the hue and `lineOpacity` / `hoverOpacity`\n * tune it.\n *\n * The hovered cell is controllable via `hoveredSquare` + `onHoveredSquareChange`.\n *\n * Our reimplementation of the effect Magic UI publishes as\n * `InteractiveGridPattern`. Ours is what differs: token-driven colour instead\n * of hard-coded grays, a controllable/observable hover cell instead of trapped\n * `useState`, a per-cell `squareProps` seam, and a reduced-motion contract.\n *\n * ## Anatomy\n *\n *   InteractiveGridPattern           (svg — data-slot=\"interactive-grid-pattern\")\n *     ├─ title                       (tooling only — the root is aria-hidden)\n *     └─ rect × columns·rows         (data-slot=\"interactive-grid-square\")\n *                                    (data-active on the hovered cell)\n *\n * ## Motion\n *\n * CSS transitions, but JS-gated as well. The hover fade is\n * `transition-[fill] duration-100` in, `duration-1000` out; under\n * `prefers-reduced-motion: reduce` `useReducedMotion` swaps the whole class\n * for `transition-none`, so the fill snaps instantly rather than relying on\n * the `transition-duration: 0.01ms` clamp in `styles/preflight.css`. Nothing\n * here animates on its own — motion happens only while a pointer moves.\n *\n * ## The `<title>` is not an accessible name\n *\n * The root is `aria-hidden=\"true\"`, so the `label` prop reaches no assistive\n * technology. It exists for tooling that opens the SVG on its own. This used\n * to be `role=\"presentation\"` + `aria-labelledby`, which axe flags as\n * `presentation-role-conflict` — a global ARIA attribute cancels the\n * presentational role and re-exposes the grid as a labelled image.\n *\n * | Rule | Concept                     | Where in this file                                       |\n * | ---- | --------------------------- | -------------------------------------------------------- |\n * | R4   | Extends native el           | `Omit<ComponentPropsWithoutRef<'svg'>, 'width'\\|'height'>` |\n * | R5   | testid required, no default | `'data-testid': string` → `{value}-cell-{index}`          |\n * | R6   | data-slot per part          | `interactive-grid-pattern` / `interactive-grid-square`    |\n * | R9   | Noun-first change event     | `onHoveredSquareChange(index, details)`                   |\n * | R10  | Composition seam            | `squareProps(index, {row, column})` merges onto each rect |\n * | R14  | Controlled + uncontrolled   | `hoveredSquare` / `defaultHoveredSquare`                  |\n * | R18  | Inline style is CSS vars    | `--grid-line-opacity` / `--grid-hover-opacity` only       |\n * | R19  | Tokens only                 | `text-border` + `stroke-current` / `fill-current`         |\n * | R25  | Client component            | `useState` + `useId` + `useReducedMotion`                 |\n */\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\n\n// -----------------------------------------------------------------------------\n// API parity (R13 / R17)\n// -----------------------------------------------------------------------------\n// Inspired by Magic UI's `InteractiveGridPattern`\n// (https://magicui.design/docs/components/interactive-grid-pattern), re-authored\n// to the Interlace floor. Deviations from upstream, all documented:\n//   • Color is token-driven, not hard-coded gray literals — stroke/fill inherit\n//     `currentColor`, opacity is exposed as props mapped to CSS variables, so a\n//     consumer drives the palette from any design token via `className`/`color`\n//     (R19, no-raw-color-literal).\n//   • The hover fade respects `prefers-reduced-motion` (R26) by collapsing the\n//     transition to an instant swap — upstream animated unconditionally.\n//   • Hover state is controllable (`hoveredSquare` + `onHoveredSquareChange` +\n//     `defaultHoveredSquare`) so a parent can drive / observe the active cell\n//     (R14), instead of being trapped in internal `useState`.\n//   • Pointer affordances are pluggable per cell via `squareProps`, and\n//     `aria-hidden` keeps the decorative grid out of the a11y tree without an\n//     axe suppression (R20 / R26).\n// -----------------------------------------------------------------------------\n\n/**\n * Per-square render hook. Receives the flat index plus its grid coordinates and\n * returns extra SVG `<rect>` props (e.g. `data-*`, `onClick`) merged onto the\n * cell. Lets a consumer wire interactivity without forking the component (R10).\n */\nexport type InteractiveGridSquareProps = (\n  index: number,\n  position: { row: number; column: number },\n) => ComponentPropsWithoutRef<\"rect\"> | undefined;\n\nexport interface InteractiveGridPatternProps\n  extends Omit<ComponentPropsWithoutRef<\"svg\">, \"width\" | \"height\"> {\n  /**\n   * Stable selector hook for tests. Required — no runtime default so an\n   * omission surfaces in review rather than silently masking it (R5). Each\n   * `<rect>` derives `{value}-cell-{index}`.\n   */\n  \"data-testid\": string;\n  /**\n   * Width of a single square, in user-space units.\n   * @default 40\n   */\n  squareWidth?: number;\n  /**\n   * Height of a single square, in user-space units.\n   * @default 40\n   */\n  squareHeight?: number;\n  /**\n   * Number of columns in the grid (horizontal squares).\n   * @default 24\n   */\n  columns?: number;\n  /**\n   * Number of rows in the grid (vertical squares).\n   * @default 24\n   */\n  rows?: number;\n  /**\n   * Stroke opacity of every cell's border, 0–1. Combined with the inherited\n   * `currentColor` so the consumer controls the hue via a token.\n   * @default 0.3\n   */\n  lineOpacity?: number;\n  /**\n   * Fill opacity applied to the hovered cell, 0–1. Combined with the inherited\n   * `currentColor` so the consumer controls the hue via a token.\n   * @default 0.3\n   */\n  hoverOpacity?: number;\n  /**\n   * Controlled active cell. Pass `null` for \"none hovered\". When set, the\n   * component is controlled and `onHoveredSquareChange` is the source of truth\n   * (R14). Leave `undefined` to let the component manage hover internally.\n   * @default undefined\n   */\n  hoveredSquare?: number | null;\n  /**\n   * Initial active cell for the uncontrolled mode (R14).\n   * @default null\n   */\n  defaultHoveredSquare?: number | null;\n  /**\n   * Fires when the hovered cell changes — `index` is the flat cell index or\n   * `null` when the pointer leaves the grid (R9, noun-first change event).\n   */\n  onHoveredSquareChange?: (\n    index: number | null,\n    details: { row: number; column: number } | null,\n  ) => void;\n  /**\n   * Extra props merged onto every `<rect>`, computed per cell (R10).\n   */\n  squareProps?: InteractiveGridSquareProps;\n  /**\n   * Class applied to every `<rect>` cell — merged after the base classes.\n   */\n  squaresClassName?: string;\n  /**\n   * Title written into the SVG's `<title>` element.\n   *\n   * NOT an accessible name: the grid is `aria-hidden`, so nothing here\n   * reaches assistive tech. It exists for tooling that reads the SVG on its\n   * own — a design handoff, or the file opened directly in a browser.\n   * @default \"Decorative interactive grid\"\n   */\n  label?: string;\n}\n\n/**\n * `InteractiveGridPattern` — a decorative, pointer-reactive grid of squares,\n * sized to fill its positioned parent. Each cell highlights on hover; the fade\n * honors `prefers-reduced-motion`.\n *\n * Color is token-driven: cells inherit `currentColor`, so set the hue with a\n * text-color token (e.g. `className=\"text-border\"`) and tune visibility with\n * `lineOpacity` / `hoverOpacity`. Place inside a `relative` container.\n *\n * @example\n * ```tsx\n * <div className=\"relative h-64 overflow-hidden\">\n *   <InteractiveGridPattern\n *     data-testid=\"hero-grid\"\n *     className=\"text-border\"\n *     columns={20}\n *     rows={12}\n *   />\n * </div>\n * ```\n */\nexport const InteractiveGridPattern = forwardRef<\n  SVGSVGElement,\n  InteractiveGridPatternProps\n>(function InteractiveGridPattern(\n  {\n    \"data-testid\": dataTestid,\n    squareWidth = 40,\n    squareHeight = 40,\n    columns = 24,\n    rows = 24,\n    lineOpacity = 0.3,\n    hoverOpacity = 0.3,\n    hoveredSquare: hoveredSquareProp,\n    defaultHoveredSquare = null,\n    onHoveredSquareChange,\n    squareProps,\n    className,\n    squaresClassName,\n    label = \"Decorative interactive grid\",\n    ...props\n  },\n  ref,\n) {\n  const reducedMotion = useReducedMotion();\n  const titleId = useId();\n\n  const [uncontrolledHovered, setUncontrolledHovered] = useState<number | null>(\n    defaultHoveredSquare,\n  );\n  const isControlled = hoveredSquareProp !== undefined;\n  const hoveredSquare = isControlled ? hoveredSquareProp : uncontrolledHovered;\n\n  const setHovered = useCallback(\n    (index: number | null) => {\n      const details =\n        index === null\n          ? null\n          : { row: Math.floor(index / columns), column: index % columns };\n      if (!isControlled) setUncontrolledHovered(index);\n      onHoveredSquareChange?.(index, details);\n    },\n    [columns, isControlled, onHoveredSquareChange],\n  );\n\n  return (\n    <svg\n      ref={ref}\n      data-slot=\"interactive-grid-pattern\"\n      data-testid={dataTestid}\n      // Decorative, so it leaves the a11y tree entirely — matching the\n      // DotPattern / GridPattern siblings.\n      //\n      // This used to be `role=\"presentation\"` + `aria-labelledby`, which axe\n      // flags as `presentation-role-conflict`: a global ARIA attribute on a\n      // presentational element cancels the role, and the grid gets re-exposed\n      // as a labelled image. The `<title>` below stays for tooling that reads\n      // the SVG on its own (design handoff, an SVG opened directly), but it is\n      // no longer wired into the a11y tree.\n      aria-hidden=\"true\"\n      width={squareWidth * columns}\n      height={squareHeight * rows}\n      // Dynamic, computed CSS-variable overrides — the one R18-sanctioned use\n      // of inline style. These feed the token-driven opacities into the class\n      // system below; they are not static styling.\n      style={{\n        // @ts-expect-error -- CSS custom properties are valid inline style keys.\n        \"--grid-line-opacity\": lineOpacity,\n        \"--grid-hover-opacity\": hoverOpacity,\n      }}\n      className={cn(\n        \"pointer-events-none absolute inset-0 h-full w-full text-border\",\n        className,\n      )}\n      {...props}\n    >\n      <title id={titleId}>{label}</title>\n      {Array.from({ length: columns * rows }).map((_, index) => {\n        const column = index % columns;\n        const row = Math.floor(index / columns);\n        const x = column * squareWidth;\n        const y = row * squareHeight;\n        const active = hoveredSquare === index;\n        const { className: extraClassName, ...extra } =\n          squareProps?.(index, { row, column }) ?? {};\n\n        return (\n          <rect\n            key={index}\n            // Consumer extras spread first so the component keeps ownership of\n            // the slot, hover tracking, and geometry below (R10 without footgun).\n            {...extra}\n            data-slot=\"interactive-grid-square\"\n            data-testid={`${dataTestid}-cell-${index}`}\n            data-active={active || undefined}\n            x={x}\n            y={y}\n            width={squareWidth}\n            height={squareHeight}\n            className={cn(\n              \"pointer-events-auto fill-transparent stroke-current [stroke-opacity:var(--grid-line-opacity)]\",\n              // Reduced motion → instant swap; otherwise a quick fade in and a\n              // slow fade out, matching the upstream feel (R26).\n              reducedMotion\n                ? \"transition-none\"\n                : \"transition-[fill] duration-100 ease-in-out [&:not([data-active])]:duration-1000\",\n              active && \"fill-current [fill-opacity:var(--grid-hover-opacity)]\",\n              squaresClassName,\n              extraClassName,\n            )}\n            onPointerEnter={(event) => {\n              extra.onPointerEnter?.(event);\n              setHovered(index);\n            }}\n            onPointerLeave={(event) => {\n              extra.onPointerLeave?.(event);\n              setHovered(null);\n            }}\n          />\n        );\n      })}\n    </svg>\n  );\n});\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": true,
    "minViewport": null,
    "loading": false,
    "version": "1.2.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/interactive-grid-pattern\n\nInstalled to `components/ui/patterns/interactive-grid-pattern.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/interactive-grid-pattern';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/interactive-grid-pattern\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
