{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "network-hero",
  "title": "Network Hero",
  "description": "Landing-page hero matching the live Landbou24 home page: a full-bleed photo that slowly drifts under dark scrims, a farm network that lights up around the mouse pointer, and over it a pill eyebrow, a very large heading, a subtitle and two actions (solid secondary and outlined). The copy fades up on load; the drift, the entrance and the network all stand still for reduced motion, and the network skips touch. Presentational; actions use the host's buttonVariants, hosts swap the photo for next/image via mediaContent, pass their router link through renderLink, and set the height and gutter to sit under their own header.",
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "default/network-hero/network-hero.tsx",
      "content": "import { useId, type ReactNode } from \"react\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\n\nimport { NetworkCanvas } from \"./network-canvas\"\n\nexport type NetworkHeroLinkProps = {\n  href: string\n  className?: string\n  children: ReactNode\n}\n\nexport type NetworkHeroAction = {\n  label: string\n  href: string\n}\n\nexport type NetworkHeroMedia = {\n  /** Background photo URL. */\n  src: string\n  /** Leave empty: the photo sits behind the copy as decoration. */\n  alt?: string\n}\n\nexport type NetworkHeroProps = {\n  /** Short line in the pill above the heading, e.g. \"Smart digital solutions for your farm\". */\n  eyebrow?: string | null\n  /** Rendered as the page's h1. */\n  title: string\n  description?: string | null\n  /** Solid button, in the host's secondary colour. */\n  primaryAction?: NetworkHeroAction | null\n  /** Outlined button beside it, e.g. a jump to a section further down. */\n  secondaryAction?: NetworkHeroAction | null\n  media?: NetworkHeroMedia | null\n  /**\n   * Replaces the default <img> background, e.g. with next/image. It should\n   * fill the section (absolute inset-0, object-cover); pass\n   * `networkHeroBackgroundClass` to keep the slow drift.\n   */\n  mediaContent?: ReactNode\n  /** The pointer-revealed farm network over the photo. On by default. */\n  network?: boolean\n  /** Swaps the plain anchor for the host's router link (next/link, i18n Link). */\n  renderLink?: (props: NetworkHeroLinkProps) => ReactNode\n  /**\n   * Height of the section. Defaults to the full screen; a host with a header\n   * above it passes e.g. \"min-h-[calc(100svh-72px)]\".\n   */\n  heightClassName?: string\n  /** Inner container: width and gutter of the copy. */\n  contentClassName?: string\n  className?: string\n}\n\n/** Slow zoom-and-pan for the background photo, as on Landbou24. */\nexport const networkHeroBackgroundClass = \"network-hero-background\"\n\n// The entrance and drift from Landbou24's globals.css, shipped with the block\n// so hosts need no stylesheet changes. Both stop for reduced motion.\nconst heroStyles = `\n@keyframes network-hero-enter {\n  from { opacity: 0; transform: translateY(18px); }\n  to { opacity: 1; transform: translateY(0); }\n}\n@keyframes network-hero-background-drift {\n  from { transform: scale(1.04) translateX(-0.5%); }\n  to { transform: scale(1.12) translateX(0.5%); }\n}\n.network-hero-enter {\n  animation: network-hero-enter 700ms cubic-bezier(0.23, 1, 0.32, 1) 500ms both;\n}\n@media (prefers-reduced-motion: no-preference) {\n  .network-hero-background {\n    animation: network-hero-background-drift 30s ease-in-out infinite alternate;\n  }\n}\n@media (prefers-reduced-motion: reduce) {\n  .network-hero-enter { animation: none; opacity: 1; transform: none; }\n}\n`\n\n// Actions take the host's own Button styles, sized up to Landbou24's 48px.\nconst actionSize = \"h-12 px-8 text-base w-full sm:w-auto\"\nconst primaryLinkClass = cx(\n  buttonVariants({ variant: \"secondary\", size: \"lg\" }),\n  actionSize,\n  \"shadow-lg shadow-black/10\"\n)\nconst secondaryLinkClass = cx(\n  buttonVariants({ variant: \"outline\", size: \"lg\" }),\n  actionSize,\n  \"border-background/50 bg-transparent text-background hover:bg-background/10 hover:text-background\"\n)\n\nfunction cx(...classes: Array<string | false | null | undefined>) {\n  return classes.filter(Boolean).join(\" \")\n}\n\nfunction defaultLink({ href, className, children }: NetworkHeroLinkProps) {\n  return (\n    <a href={href} className={className}>\n      {children}\n    </a>\n  )\n}\n\n/**\n * Landing-page hero matching the live Landbou24 home page\n * (landbou24.com/af, agrista/landbou24 `farmer-home-page.tsx`): a full-bleed\n * photo that slowly drifts under dark scrims, a farm network that lights up\n * around the pointer, and over it a pill eyebrow, a very large heading, a\n * subtitle and two actions. The copy fades up on load. Presentational; the\n * host passes copy, links and its router link.\n */\nexport function NetworkHero({\n  eyebrow,\n  title,\n  description,\n  primaryAction,\n  secondaryAction,\n  media,\n  mediaContent,\n  network = true,\n  renderLink = defaultLink,\n  heightClassName = \"min-h-[100svh]\",\n  contentClassName = \"mx-auto w-full max-w-[1440px] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16\",\n  className,\n}: NetworkHeroProps) {\n  const headingId = useId()\n  const background =\n    mediaContent ??\n    (media ? (\n      <img\n        src={media.src}\n        alt={media.alt ?? \"\"}\n        className={cx(\n          networkHeroBackgroundClass,\n          \"absolute inset-0 h-full w-full object-cover object-center\"\n        )}\n      />\n    ) : null)\n\n  return (\n    <section\n      data-slot=\"network-hero\"\n      aria-labelledby={headingId}\n      className={cx(\n        \"relative isolate flex items-center overflow-hidden bg-foreground text-background\",\n        heightClassName,\n        className\n      )}\n    >\n      <style>{heroStyles}</style>\n      {background}\n      <div className=\"absolute inset-0 bg-foreground/40\" aria-hidden=\"true\" />\n      <div\n        className=\"absolute inset-0 bg-gradient-to-r from-foreground/70 via-foreground/40 to-foreground/5\"\n        aria-hidden=\"true\"\n      />\n      <div\n        className=\"absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-foreground/30 to-transparent\"\n        aria-hidden=\"true\"\n      />\n      {network ? <NetworkCanvas /> : null}\n\n      <div\n        className={cx(\n          \"relative z-10 flex items-center py-28 sm:py-32 lg:py-20\",\n          contentClassName\n        )}\n      >\n        <div className=\"network-hero-enter max-w-3xl\">\n          {eyebrow ? (\n            <p\n              data-slot=\"network-hero-eyebrow\"\n              className=\"mb-7 inline-flex max-w-full rounded-full bg-background/90 px-5 py-3 text-center text-xs font-semibold uppercase tracking-[0.16em] text-foreground shadow-sm backdrop-blur-sm sm:px-6\"\n            >\n              {eyebrow}\n            </p>\n          ) : null}\n          <h1\n            id={headingId}\n            className=\"max-w-3xl text-5xl font-semibold leading-[1.02] tracking-tight text-background sm:text-7xl xl:text-8xl\"\n          >\n            {title}\n          </h1>\n          {description ? (\n            <p className=\"mt-7 max-w-2xl text-base leading-relaxed text-background/80 sm:text-xl\">\n              {description}\n            </p>\n          ) : null}\n          {primaryAction || secondaryAction ? (\n            <div className=\"mt-8 flex flex-col gap-3 sm:flex-row\">\n              {primaryAction\n                ? renderLink({\n                    href: primaryAction.href,\n                    className: primaryLinkClass,\n                    children: primaryAction.label,\n                  })\n                : null}\n              {secondaryAction\n                ? renderLink({\n                    href: secondaryAction.href,\n                    className: secondaryLinkClass,\n                    children: secondaryAction.label,\n                  })\n                : null}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "default/network-hero/network-canvas.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ntype NetworkPoint = {\n  x: number\n  y: number\n  baseVx: number\n  baseVy: number\n  vx: number\n  vy: number\n  radius: number\n}\n\n/**\n * The drifting farm network behind the Landbou24 home hero\n * (agrista/landbou24 `farmer-home-page.tsx`). Points drift slowly across the\n * parent; where the pointer is, the nearby points light up and link to each\n * other and to the pointer. Draws nothing until a mouse or pen hovers, skips\n * touch, and stands still for reduced motion. Strokes use the host's\n * `--background` token, so it reads on the dark hero in any theme.\n */\nexport function NetworkCanvas({ className }: { className?: string }) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const host = canvas?.parentElement\n    const context = canvas?.getContext?.(\"2d\")\n\n    if (!canvas || !host || !context) return\n\n    const reducedMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches\n    const pointer = { active: false, x: 0, y: 0 }\n    const points: NetworkPoint[] = []\n    const backgroundToken =\n      getComputedStyle(canvas).getPropertyValue(\"--background\").trim() ||\n      \"0 0% 100%\"\n    let animationFrame = 0\n    let width = 0\n    let height = 0\n\n    const color = (opacity: number) => `hsl(${backgroundToken} / ${opacity})`\n\n    const draw = () => {\n      context.clearRect(0, 0, width, height)\n      context.lineCap = \"round\"\n\n      if (!pointer.active) return\n\n      const revealRadius = Math.min(260, Math.max(150, width * 0.16))\n      const focusFor = (point: NetworkPoint) => {\n        const dx = point.x - pointer.x\n        const dy = point.y - pointer.y\n        const distance = Math.sqrt(dx * dx + dy * dy)\n        return Math.max(0, 1 - distance / revealRadius)\n      }\n      const maxDistance = Math.min(180, Math.max(110, width * 0.13))\n\n      for (let index = 0; index < points.length; index += 1) {\n        const point = points[index]\n        const pointFocus = focusFor(point)\n\n        if (pointFocus <= 0) continue\n\n        for (\n          let otherIndex = index + 1;\n          otherIndex < points.length;\n          otherIndex += 1\n        ) {\n          const otherPoint = points[otherIndex]\n          const otherFocus = focusFor(otherPoint)\n\n          if (otherFocus <= 0) continue\n\n          const dx = point.x - otherPoint.x\n          const dy = point.y - otherPoint.y\n          const distance = Math.sqrt(dx * dx + dy * dy)\n\n          if (distance > maxDistance) continue\n\n          context.beginPath()\n          const edgeFocus = Math.min(pointFocus, otherFocus)\n          context.strokeStyle = color(\n            Math.max(0.04, 0.28 * edgeFocus * (1 - distance / maxDistance))\n          )\n          context.lineWidth = 1\n          context.moveTo(point.x, point.y)\n          context.lineTo(otherPoint.x, otherPoint.y)\n          context.stroke()\n        }\n\n        const dx = point.x - pointer.x\n        const dy = point.y - pointer.y\n        const distance = Math.sqrt(dx * dx + dy * dy)\n\n        if (distance < maxDistance * 1.25) {\n          context.beginPath()\n          context.strokeStyle = color(\n            Math.max(\n              0.05,\n              0.28 * pointFocus * (1 - distance / (maxDistance * 1.25))\n            )\n          )\n          context.lineWidth = 1\n          context.moveTo(point.x, point.y)\n          context.lineTo(pointer.x, pointer.y)\n          context.stroke()\n        }\n\n        const pointerOpacity = pointFocus * pointFocus\n\n        context.beginPath()\n        context.strokeStyle = color(Math.max(0.06, 0.2 * pointerOpacity))\n        context.lineWidth = 1\n        context.arc(point.x, point.y, point.radius * 2.4, 0, Math.PI * 2)\n        context.stroke()\n        context.beginPath()\n        context.fillStyle = color(Math.max(0.12, 0.65 * pointerOpacity))\n        context.arc(point.x, point.y, point.radius, 0, Math.PI * 2)\n        context.fill()\n      }\n    }\n\n    const update = () => {\n      points.forEach((point) => {\n        point.vx += (point.baseVx - point.vx) * 0.004\n        point.vy += (point.baseVy - point.vy) * 0.004\n        point.x += point.vx\n        point.y += point.vy\n\n        if (point.x < -20) point.x = width + 20\n        if (point.x > width + 20) point.x = -20\n        if (point.y < -20) point.y = height + 20\n        if (point.y > height + 20) point.y = -20\n      })\n    }\n\n    const animate = () => {\n      update()\n      draw()\n      animationFrame = window.requestAnimationFrame(animate)\n    }\n\n    const resize = () => {\n      const rect = host.getBoundingClientRect()\n      const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2)\n\n      width = rect.width\n      height = rect.height\n      canvas.width = width * devicePixelRatio\n      canvas.height = height * devicePixelRatio\n      context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)\n      points.length = 0\n\n      const pointCount = Math.min(\n        76,\n        Math.max(24, Math.round((width * height) / 25000))\n      )\n\n      for (let index = 0; index < pointCount; index += 1) {\n        const baseVx = (Math.random() - 0.5) * 0.18\n        const baseVy = (Math.random() - 0.5) * 0.18\n\n        points.push({\n          x: Math.random() * width,\n          y: Math.random() * height,\n          baseVx,\n          baseVy,\n          vx: baseVx,\n          vy: baseVy,\n          radius: Math.random() * 2 + 1.5,\n        })\n      }\n\n      draw()\n    }\n\n    const handlePointerMove = (event: PointerEvent) => {\n      if (event.pointerType === \"touch\") return\n\n      const rect = host.getBoundingClientRect()\n      pointer.active = true\n      pointer.x = event.clientX - rect.left\n      pointer.y = event.clientY - rect.top\n      draw()\n    }\n\n    const handlePointerLeave = () => {\n      pointer.active = false\n      draw()\n    }\n\n    const observer =\n      typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(resize)\n    observer?.observe(host)\n    host.addEventListener(\"pointermove\", handlePointerMove, { passive: true })\n    host.addEventListener(\"pointerleave\", handlePointerLeave)\n    window.addEventListener(\"resize\", resize)\n    resize()\n\n    if (!reducedMotion) {\n      animationFrame = window.requestAnimationFrame(animate)\n    }\n\n    return () => {\n      window.cancelAnimationFrame(animationFrame)\n      window.removeEventListener(\"resize\", resize)\n      host.removeEventListener(\"pointermove\", handlePointerMove)\n      host.removeEventListener(\"pointerleave\", handlePointerLeave)\n      observer?.disconnect()\n    }\n  }, [])\n\n  return (\n    <canvas\n      ref={canvasRef}\n      data-slot=\"network-hero-canvas\"\n      className={\n        className ??\n        \"pointer-events-none absolute inset-0 h-full w-full opacity-80\"\n      }\n      aria-hidden=\"true\"\n    />\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}