{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"interactive-grid-hero","title":"Interactive Grid Hero","author":"CoDesign-Spa27","description":"A full-page field of square cells that locally soften and round around a fine pointer.","dependencies":["motion"],"files":[{"path":"components/ui-components/interactive-grid-hero.tsx","content":"\"use client\";\n\nimport {\n  type MotionValue,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type InteractiveGridHeroEffects = {\n  /** Cell opacity at the strongest point of the pointer response. */\n  activeOpacity?: number;\n  /** Maximum corner radius, in pixels. */\n  cornerRadius?: number;\n  /** Maximum inset applied to an active cell, as a percentage. */\n  inset?: number;\n  /** Cell opacity outside the pointer response. */\n  restingOpacity?: number;\n};\n\nexport type InteractiveGridHeroSpring = {\n  bounce?: number;\n  damping?: number;\n  mass?: number;\n  stiffness?: number;\n  visualDuration?: number;\n};\n\nexport type InteractiveGridHeroProps = {\n  /** Approximate resting size of each square, in pixels. */\n  cellSize?: number;\n  className?: string;\n  effects?: InteractiveGridHeroEffects;\n  /** Space between cells, in pixels. */\n  gap?: number;\n  interactive?: boolean;\n  /** Radius of the cursor response, measured in grid cells. */\n  proximity?: number;\n  spring?: InteractiveGridHeroSpring;\n};\n\ntype GridMetrics = {\n  cellSize: number;\n  columns: number;\n  rows: number;\n};\n\ntype GridCellProps = {\n  effects: Required<InteractiveGridHeroEffects>;\n  centerX: number;\n  centerY: number;\n  pointerActive: MotionValue<number>;\n  pointerX: MotionValue<number>;\n  pointerY: MotionValue<number>;\n  radius: number;\n};\n\nconst DEFAULT_CELL_SIZE = 72;\nconst DEFAULT_PROXIMITY = 4.35;\nconst DEFAULT_GAP = 4;\nconst DEFAULT_EFFECTS = {\n  activeOpacity: 1,\n  cornerRadius: 20,\n  inset: 5.5,\n  restingOpacity: 0.72,\n} satisfies Required<InteractiveGridHeroEffects>;\nconst DEFAULT_SPRING = {\n  damping: 38,\n  mass: 1,\n  stiffness: 520,\n} satisfies Required<Pick<InteractiveGridHeroSpring, \"damping\" | \"mass\" | \"stiffness\">>;\nconst INITIAL_METRICS: GridMetrics = {\n  cellSize: DEFAULT_CELL_SIZE,\n  columns: 12,\n  rows: 10,\n};\n\nfunction getGridMetrics(\n  width: number,\n  height: number,\n  targetCellSize: number,\n  gap: number,\n): GridMetrics {\n  const safeWidth = Math.max(1, width);\n  const safeHeight = Math.max(1, height);\n  const columns = Math.max(1, Math.ceil((safeWidth + gap) / (targetCellSize + gap)));\n  const cellSize = Math.max(1, (safeWidth - gap * (columns - 1)) / columns);\n  const rows = Math.max(1, Math.ceil((safeHeight + gap) / (cellSize + gap)));\n\n  return { cellSize, columns, rows };\n}\n\nfunction smoothstep(value: number) {\n  return value * value * (3 - 2 * value);\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nfunction resolveSpring(spring: InteractiveGridHeroSpring | undefined) {\n  if (spring?.visualDuration !== undefined || spring?.bounce !== undefined) {\n    return {\n      bounce: spring.bounce ?? 0,\n      visualDuration: spring.visualDuration ?? 0.3,\n    };\n  }\n\n  return {\n    damping: spring?.damping ?? DEFAULT_SPRING.damping,\n    mass: spring?.mass ?? DEFAULT_SPRING.mass,\n    stiffness: spring?.stiffness ?? DEFAULT_SPRING.stiffness,\n  };\n}\n\nfunction GridCell({\n  centerX,\n  centerY,\n  effects,\n  pointerActive,\n  pointerX,\n  pointerY,\n  radius,\n}: GridCellProps) {\n  const influence = useTransform(() => {\n    const distance = Math.hypot(pointerX.get() - centerX, pointerY.get() - centerY);\n    const proximity = Math.max(0, 1 - distance / radius);\n\n    return smoothstep(proximity) * pointerActive.get();\n  });\n  const clipPath = useTransform(\n    influence,\n    (value) => `inset(${value * effects.inset}% round ${value * effects.cornerRadius}px)`,\n  );\n  const opacity = useTransform(influence, [0, 1], [effects.restingOpacity, effects.activeOpacity]);\n\n  return (\n    <motion.div\n      className=\"rounded-sm border-r border-b border-border/70 bg-background\"\n      style={{ clipPath, opacity }}\n    />\n  );\n}\n\nexport function InteractiveGridHero({\n  cellSize: targetCellSize = DEFAULT_CELL_SIZE,\n  className,\n  effects,\n  gap = DEFAULT_GAP,\n  interactive = true,\n  proximity = DEFAULT_PROXIMITY,\n  spring,\n}: InteractiveGridHeroProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const pointerX = useMotionValue(0);\n  const pointerY = useMotionValue(0);\n  const pointerActive = useMotionValue(0);\n  const smoothPointerActive = useSpring(pointerActive, resolveSpring(spring));\n  const shouldReduceMotion = useReducedMotion();\n  const activeValue = shouldReduceMotion ? pointerActive : smoothPointerActive;\n  const [metrics, setMetrics] = React.useState(INITIAL_METRICS);\n  const resolvedEffects = {\n    activeOpacity: clamp(effects?.activeOpacity ?? DEFAULT_EFFECTS.activeOpacity, 0, 1),\n    cornerRadius: Math.max(0, effects?.cornerRadius ?? DEFAULT_EFFECTS.cornerRadius),\n    inset: Math.max(0, effects?.inset ?? DEFAULT_EFFECTS.inset),\n    restingOpacity: clamp(effects?.restingOpacity ?? DEFAULT_EFFECTS.restingOpacity, 0, 1),\n  };\n  const resolvedGap = Math.max(0, gap);\n\n  React.useEffect(() => {\n    const container = containerRef.current;\n\n    if (!container) {\n      return;\n    }\n\n    const updateMetrics = () => {\n      const rect = container.getBoundingClientRect();\n      setMetrics(\n        getGridMetrics(rect.width, rect.height, Math.max(32, targetCellSize), resolvedGap),\n      );\n    };\n\n    const resizeObserver = new ResizeObserver(updateMetrics);\n    resizeObserver.observe(container);\n    updateMetrics();\n\n    return () => resizeObserver.disconnect();\n  }, [resolvedGap, targetCellSize]);\n\n  React.useEffect(() => {\n    if (!interactive) {\n      pointerActive.set(0);\n    }\n  }, [interactive, pointerActive]);\n\n  const handlePointerMove = React.useCallback(\n    (event: React.PointerEvent<HTMLDivElement>) => {\n      if (\n        !interactive ||\n        event.pointerType !== \"mouse\" ||\n        !window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches\n      ) {\n        return;\n      }\n\n      const rect = event.currentTarget.getBoundingClientRect();\n      pointerX.set(event.clientX - rect.left);\n      pointerY.set(event.clientY - rect.top);\n      pointerActive.set(1);\n    },\n    [interactive, pointerActive, pointerX, pointerY],\n  );\n\n  const handlePointerLeave = React.useCallback(() => {\n    pointerActive.set(0);\n  }, [pointerActive]);\n\n  const cellCount = metrics.columns * metrics.rows;\n  const cells = React.useMemo(\n    () =>\n      Array.from({ length: cellCount }, (_, index) => ({\n        centerX: ((index % metrics.columns) + 0.5) * metrics.cellSize,\n        centerY: (Math.floor(index / metrics.columns) + 0.5) * metrics.cellSize,\n        id: `${Math.floor(index / metrics.columns)}-${index % metrics.columns}`,\n      })),\n    [cellCount, metrics.cellSize, metrics.columns],\n  );\n  const responseRadius = metrics.cellSize * Math.max(1, proximity);\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={cn(\"relative isolate min-h-svh w-full overflow-hidden bg-accent p-5\", className)}\n      onPointerLeave={handlePointerLeave}\n      onPointerMove={handlePointerMove}\n      ref={containerRef}\n    >\n      <div\n        className=\"absolute inset-0 grid content-start\"\n        style={{\n          gap: `${resolvedGap}px`,\n          gridAutoRows: `${metrics.cellSize}px`,\n          gridTemplateColumns: `repeat(${metrics.columns}, ${metrics.cellSize}px)`,\n        }}\n      >\n        {cells.map((cell) => (\n          <GridCell\n            centerX={cell.centerX}\n            centerY={cell.centerY}\n            effects={resolvedEffects}\n            key={cell.id}\n            pointerActive={activeValue}\n            pointerX={pointerX}\n            pointerY={pointerY}\n            radius={responseRadius}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui"}],"categories":["hero","interaction","grid"],"type":"registry:ui"}