{"name":"tilted-grid-hero","title":"Tilted Grid Hero","description":"","type":"section","dependencies":["clsx","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/ui/TiltedGridHero.tsx","type":"component","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst SLICES = 16;\nconst CAMERA = 1.6;\n\nfunction arc(bend: number) {\n  return (CAMERA - 1 + Math.cos(bend)) / (2 * CAMERA * Math.sin(bend));\n}\n\nconst MAX_WIDTH = 55;\n\nfunction measure(\n  width: number,\n  height: number,\n  tile: number,\n  aspect: number,\n  gap: number,\n  bend: number,\n) {\n  const h = Math.min(\n    (tile / 100) * height,\n    ((MAX_WIDTH / 100) * width) / aspect,\n  );\n  const radius = width * arc(bend);\n  if (!(h > 0) || !(radius > 0))\n    return { columns: 2, sweep: 0, unit: 0, limit: 0 };\n  const deg = (rad: number) => (rad * 180) / Math.PI;\n  const unit = deg(h / radius);\n  const pitch = (aspect + gap / 100) * unit;\n  const limit = deg(bend) + (aspect * unit) / 2;\n  const columns = Math.min(60, Math.max(2, Math.ceil((2 * limit) / pitch)));\n  const round = (n: number) => +n.toFixed(4);\n  const sweep = round((columns * pitch) / 2);\n  return {\n    columns,\n    sweep,\n    unit: round(unit),\n    limit: round(Math.min(sweep, limit)),\n  };\n}\n\nexport type TiltedGridImage = {\n  src: string;\n  alt?: string;\n};\n\nexport type TiltedGridHeroProps = {\n  images?: TiltedGridImage[];\n  speed?: number;\n  tileHeight?: number;\n  aspectRatio?: number;\n  gap?: number;\n  axis?: number;\n  curve?: number;\n  fade?: number;\n  title?: string;\n  subtitleTop?: string;\n  subtitleBottom?: string;\n  primaryBtnText?: string;\n  secondaryBtnText?: string;\n  onPrimaryClick?: () => void;\n  onSecondaryClick?: () => void;\n  children?: React.ReactNode;\n  className?: string;\n};\n\nconst DEFAULT_IMAGES: TiltedGridImage[] = [\n  { src: \"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1200&auto=format&fit=crop&q=80\", alt: \"Abstract 1\" },\n  { src: \"https://images.unsplash.com/photo-1634017839464-5c339ebe3cb4?w=1200&auto=format&fit=crop&q=80\", alt: \"Abstract 2\" },\n  { src: \"https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=1200&auto=format&fit=crop&q=80\", alt: \"Abstract 3\" },\n  { src: \"https://images.unsplash.com/photo-1635070041078-e363dbe005cb?w=1200&auto=format&fit=crop&q=80\", alt: \"Abstract 4\" },\n];\n\nexport function TiltedGridHero({\n  images = DEFAULT_IMAGES,\n  speed = 4,\n  tileHeight = 26,\n  aspectRatio = 16 / 9,\n  gap = 6,\n  axis = 52,\n  curve = 80,\n  fade = 12,\n  title = \"Esplora la Nuova Dimensione\",\n  subtitleTop = \"DESIGN E TECNOLOGIA\",\n  subtitleBottom = \"Un'esperienza visiva fluida e interattiva progettata per catturare l'attenzione.\",\n  primaryBtnText = \"Inizia Ora\",\n  secondaryBtnText = \"Scopri di più\",\n  onPrimaryClick,\n  onSecondaryClick,\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & TiltedGridHeroProps) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const id = React.useId().replace(/[^a-zA-Z0-9]/g, \"\");\n  const orbit = `tgh-o-${id}`;\n  const tile = `tgh-t-${id}`;\n  const bend = (Math.min(85, Math.max(5, curve)) * Math.PI) / 180;\n\n  const [layout, setLayout] = React.useState<ReturnType<typeof measure> | null>(\n    null,\n  );\n\n  const [shown, setShown] = React.useState<Record<number, number>>({});\n\n  React.useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect;\n      const next = measure(width, height, tileHeight, aspectRatio, gap, bend);\n      setLayout((prev) =>\n        prev?.columns === next.columns &&\n        prev.sweep === next.sweep &&\n        prev.unit === next.unit\n          ? prev\n          : next,\n      );\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [tileHeight, aspectRatio, gap, bend]);\n\n  React.useEffect(() => {\n    for (const { src } of images) {\n      if (typeof window !== \"undefined\") {\n        const img = new Image();\n        img.src = src;\n      }\n    }\n  }, [images]);\n\n  const { columns, sweep, unit, limit } =\n    layout ?? measure(1200, 800, tileHeight, aspectRatio, gap, bend);\n\n  const u = (n: number) =>\n    `calc(${+n.toFixed(4)} * min(${tileHeight}cqh, ${+(\n      MAX_WIDTH / aspectRatio\n    ).toFixed(4)}cqw))`;\n\n  const r = 100 * arc(bend);\n  const radius = `${+r.toFixed(3)}cqw`;\n\n  const turn = (deg: number) =>\n    `translateZ(${radius}) rotateY(${+deg.toFixed(4)}deg) translateZ(-${radius})`;\n\n  const hide = +(((sweep - limit) / (2 * sweep || 1)) * 100).toFixed(4);\n  const css =\n    `@keyframes ${orbit}{` +\n    `from{transform:${turn(-sweep)}}to{transform:${turn(sweep)}}` +\n    `0%,${hide}%,${100 - hide}%,100%{visibility:hidden}` +\n    `${hide + 0.001}%,${100 - hide - 0.001}%{visibility:visible}}` +\n    `@media(prefers-reduced-motion:reduce){.${tile}{animation-play-state:paused}}`;\n\n  const share = aspectRatio / SLICES;\n  const duration = columns * speed;\n  const mask = `linear-gradient(90deg,transparent,#000 ${fade}%,#000 ${\n    100 - fade\n  }%,transparent)`;\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        // Utilizziamo 'bg-background' e 'text-foreground' in modo che risponda a Dark, Light e Transparent\n        \"relative flex min-h-screen h-[100dvh] w-full flex-col justify-between overflow-hidden bg-background p-6 text-foreground sm:p-12\",\n        className\n      )}\n      {...props}\n      style={{ containerType: \"size\", ...props.style }}\n    >\n      <style>{css}</style>\n\n      {/* ── BANDA 3D ORIGINALE ── */}\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 transition-opacity duration-500\"\n        style={{\n          opacity: layout ? 1 : 0,\n          perspective: `${+(r * CAMERA).toFixed(3)}cqw`,\n          perspectiveOrigin: `50% ${axis}%`,\n          maskImage: mask,\n          WebkitMaskImage: mask,\n        }}\n      >\n        {Array.from({ length: columns }, (_, t) => {\n          const first = columns - 1 - t;\n          const img = images[(shown[t] ?? first) % Math.max(images.length, 1)];\n          return (\n            <div\n              key={t}\n              className={cn(tile, \"absolute\")}\n              style={{\n                left: `calc(50% - ${u(aspectRatio / 2)})`,\n                top: `calc(${axis}% - ${u(0.5)})`,\n                width: u(aspectRatio),\n                height: u(1),\n                transformStyle: \"preserve-3d\",\n                animation: `${orbit} ${duration}s linear ${-t * speed}s infinite`,\n              }}\n              onAnimationIteration={(e) => {\n                const lap = Math.round(e.elapsedTime / duration);\n                setShown((prev) => {\n                  const next = lap * columns + first;\n                  return prev[t] === next ? prev : { ...prev, [t]: next };\n                });\n              }}\n            >\n              {Array.from({ length: SLICES }, (_, k) => (\n                <div\n                  key={k}\n                  className={cn(\n                    \"absolute top-0 overflow-hidden\",\n                    k === 0 && \"rounded-l-lg\",\n                    k === SLICES - 1 && \"rounded-r-lg\"\n                  )}\n                  style={{\n                    left: u((aspectRatio - share) / 2),\n                    width:\n                      k === SLICES - 1 ? u(share) : `calc(${u(share)} + 1px)`,\n                    height: u(1),\n                    transform: turn(\n                      (aspectRatio / 2 - (k + 0.5) * share) * unit\n                    ),\n                    backfaceVisibility: \"hidden\",\n                    WebkitBackfaceVisibility: \"hidden\",\n                  }}\n                >\n                  {img ? (\n                    <img\n                      src={img.src}\n                      alt={k === 0 ? (img.alt ?? \"\") : \"\"}\n                      draggable={false}\n                      className=\"absolute top-0 max-w-none object-cover\"\n                      style={{\n                        left: u(-k * share),\n                        width: u(aspectRatio),\n                        height: u(1),\n                        backfaceVisibility: \"hidden\",\n                        WebkitBackfaceVisibility: \"hidden\",\n                        willChange: \"transform\",\n                        transform: \"translateZ(0)\",\n                      }}\n                    />\n                  ) : null}\n                </div>\n              ))}\n            </div>\n          );\n        })}\n      </div>\n\n      {/* ── VIGNETTATURA ADATTABILE TEMA (from-background to-background) ── */}\n      <div className=\"pointer-events-none absolute inset-0 bg-gradient-to-b from-background/90 via-transparent to-background/90\" />\n\n      {/* ── OVERLAY CON TESTI E BOTTONI SEMANTICI ── */}\n      {children ? (\n        <div className=\"relative z-10 flex h-full items-center justify-center\">\n          {children}\n        </div>\n      ) : (\n        <>\n          {/* Titolo e frase SOPRA */}\n          <div className=\"relative z-10 flex flex-col items-center text-center pt-4\">\n            <span className=\"mb-3 inline-flex items-center rounded-full border border-primary/20 bg-primary/10 px-4 py-1.5 text-xs font-semibold tracking-wider text-primary uppercase backdrop-blur-md\">\n              {subtitleTop}\n            </span>\n            <h1 className=\"max-w-4xl text-4xl font-extrabold tracking-tight text-foreground drop-shadow-lg sm:text-6xl lg:text-7xl\">\n              {title}\n            </h1>\n          </div>\n\n          {/* Frase e bottoni SOTTO */}\n          <div className=\"relative z-10 flex flex-col items-center text-center pb-4\">\n            <p className=\"mb-6 max-w-xl text-base text-muted-foreground drop-shadow sm:text-lg\">\n              {subtitleBottom}\n            </p>\n\n            <div className=\"flex flex-wrap items-center justify-center gap-4\">\n              <button\n                onClick={onPrimaryClick}\n                className=\"rounded-xl bg-primary px-7 py-3.5 text-base font-semibold text-primary-foreground shadow-xl transition-all hover:opacity-90 active:scale-95\"\n              >\n                {primaryBtnText}\n              </button>\n              <button\n                onClick={onSecondaryClick}\n                className=\"rounded-xl border border-border bg-secondary/80 px-7 py-3.5 text-base font-semibold text-secondary-foreground backdrop-blur-md transition-all hover:bg-secondary active:scale-95\"\n              >\n                {secondaryBtnText}\n              </button>\n            </div>\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n\nexport default TiltedGridHero;"}],"examples":[],"cssRequirements":[{"kind":"keyframes","name":"s","detail":"@keyframes s is referenced but not defined in the component"}],"tailwindRequirements":[{"kind":"color","name":"background","detail":"Theme token \"background\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["bg-background","from-background/90","to-background/90"],"providedByHost":true},{"kind":"color","name":"border","detail":"Theme token \"border\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["border-border"],"providedByHost":true},{"kind":"color","name":"foreground","detail":"Theme token \"foreground\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["text-foreground"],"providedByHost":true},{"kind":"color","name":"muted-foreground","detail":"Theme token \"muted-foreground\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["text-muted-foreground"],"providedByHost":true},{"kind":"color","name":"primary","detail":"Theme token \"primary\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["bg-primary","bg-primary/10","border-primary/20","text-primary"],"providedByHost":true},{"kind":"color","name":"primary-foreground","detail":"Theme token \"primary-foreground\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["text-primary-foreground"],"providedByHost":true},{"kind":"color","name":"secondary","detail":"Theme token \"secondary\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["bg-secondary/80","hover:bg-secondary"],"providedByHost":true},{"kind":"color","name":"secondary-foreground","detail":"Theme token \"secondary-foreground\" (e.g. theme.extend.colors) — not in the default Tailwind theme","tokens":["text-secondary-foreground"],"providedByHost":true}],"meta":{"componentName":"TiltedGridHero","importPath":"@/components/ui/TiltedGridHero","tags":["interactive","gradient","glass","dark","responsive","hover","marketing"],"sourceUrl":null,"sourceName":null,"author":null,"license":null,"publishedAt":"2026-10-13T12:06:37.047Z","updatedAt":"2026-10-13T12:14:09.151Z"}}