# Hero Liquid Metal

> Split-layout hero section for React and shadcn/ui with a chrome-like LiquidMetal WebGL shader, CTA buttons, and badges.

Source: https://www.cult-ui.com/docs/components/hero-liquid-metal

## Example

```tsx title="hero-liquid-metal-demo.tsx"
"use client"

import {
  AISDKIcon,
  HeroLiquidMetal,
  HeroLiquidMetalActions,
  HeroLiquidMetalBadges,
  HeroLiquidMetalContainer,
  HeroLiquidMetalContent,
  HeroLiquidMetalDescription,
  HeroLiquidMetalHeading,
  HeroLiquidMetalMobileVisual,
  HeroLiquidMetalRoot,
  HeroLiquidMetalVisual,
  NextjsIcon,
} from "../ui/hero-liquid-metal"

export default function HeroLiquidMetalDemo() {
  return (
    <div className="space-y-16">
      <div className="space-y-4">
        <h3 className="flex items-center justify-center text-lg font-medium">
          Default
        </h3>
        <HeroLiquidMetal />
      </div>

      <div className="space-y-4">
        <h3 className="flex items-center justify-center text-lg font-medium">
          Root-level shader props
        </h3>
        <HeroLiquidMetal
          width={1280}
          height={720}
          image="https://shaders.paper.design/images/logos/diamond.svg"
          colorBack="#ffffff00"
          colorTint="#2c5d72"
          shape={undefined}
          repetition={6}
          softness={0.8}
          shiftRed={1}
          shiftBlue={-1}
          distortion={0.4}
          contour={0.4}
          angle={0}
          speed={1}
          scale={0.6}
          fit="contain"
        />
      </div>

      <div className="space-y-4">
        <h3 className="flex items-center justify-center text-lg font-medium">
          Configured via props
        </h3>
        <HeroLiquidMetalRoot
          ctaProps={{
            href: "#",
            label: "Try NewCopy for marketing",
          }}
          description="Human-in-the-loop marketing copy software that helps teams draft, review, and ship high-converting campaigns with confidence."
          desktopShaderProps={{
            scale: 0.8,
            speed: 0.7,
            repetition: 7,
            softness: 0.9,
            distortion: 0.3,
            colorTint: "#4f46e5",
          }}
          image="https://shaders.paper.design/images/logos/diamond.svg"
          mobileShaderProps={{
            speed: 0.65,
            scale: 0.78,
            colorTint: "#0ea5e9",
          }}
          srTitle="Composable hero"
          subtitle="Marketing"
          techStack={[
            { name: "Next.js", version: "v16" },
            { name: "AI SDK", version: "v6" },
            { name: "Tailwind", version: "v4" },
          ]}
          title="Cursor for"
        >
          <HeroLiquidMetalContainer>
            <HeroLiquidMetalContent>
              <HeroLiquidMetalHeading />
              <HeroLiquidMetalDescription />
              <HeroLiquidMetalActions />
              <div
                className="hidden justify-center lg:flex lg:justify-start"
                data-slot="hero-liquid-metal-badges-wrap"
              >
                <HeroLiquidMetalBadges
                  techStack={[
                    { name: "Next.js", version: "v16", icon: NextjsIcon },
                    { name: "AI SDK", version: "v6", icon: AISDKIcon },
                  ]}
                />
              </div>
            </HeroLiquidMetalContent>
            <HeroLiquidMetalVisual />
          </HeroLiquidMetalContainer>
          <HeroLiquidMetalMobileVisual />
        </HeroLiquidMetalRoot>
      </div>
    </div>
  )
}
```

Hero Liquid Metal is a React hero section for shadcn/ui built on @paper-design/shaders-react. Use it to open a landing page or product announcement with a chrome-like visual.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/hero-liquid-metal
```

### Manual

**Install required dependencies.**

```bash
npm install @paper-design/shaders-react
```

**Install required registry dependencies if they are not already in your
  project.**

```bash
npx shadcn@latest add button badge
```

**Copy and paste the component source into your project.**

```tsx title="hero-liquid-metal.tsx"
"use client"

import * as React from "react"
import type { SVGProps } from "react"
import { LiquidMetal } from "@paper-design/shaders-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"

const MemoizedLiquidMetal = React.memo(LiquidMetal)

type LiquidMetalProps = React.ComponentProps<typeof LiquidMetal>
type LiquidMetalIcon = React.ComponentType<SVGProps<SVGSVGElement>>

export interface HeroLiquidMetalTechItem {
  name: string
  version?: string
  icon?: LiquidMetalIcon
}

export interface HeroLiquidMetalCTAProps {
  label: React.ReactNode
  href: string
  target?: React.HTMLAttributeAnchorTarget
  rel?: string
  onClick?: React.MouseEventHandler<HTMLAnchorElement>
  className?: string
  buttonClassName?: string
}

/** LiquidMetal shader props that can be passed at the root for convenience */
export type HeroLiquidMetalShaderOverrides = Partial<
  Pick<
    LiquidMetalProps,
    | "width"
    | "height"
    | "image"
    | "colorBack"
    | "colorTint"
    | "shape"
    | "repetition"
    | "softness"
    | "shiftRed"
    | "shiftBlue"
    | "distortion"
    | "contour"
    | "angle"
    | "speed"
    | "frame"
    | "scale"
    | "rotation"
    | "offsetX"
    | "offsetY"
    | "fit"
    | "originX"
    | "originY"
    | "minPixelRatio"
    | "maxPixelCount"
  >
>

export interface HeroLiquidMetalRootProps
  extends Omit<React.ComponentPropsWithoutRef<"section">, "title">,
    HeroLiquidMetalShaderOverrides {
  srTitle?: string
  title?: React.ReactNode
  subtitle?: React.ReactNode
  description?: React.ReactNode
  showCta?: boolean
  ctaProps?: Partial<HeroLiquidMetalCTAProps>
  renderCta?: (defaultCta: React.ReactNode) => React.ReactNode
  showBadges?: boolean
  techStack?: HeroLiquidMetalTechItem[]
  renderBadge?: (
    tech: HeroLiquidMetalTechItem,
    index: number,
    defaultBadge: React.ReactNode
  ) => React.ReactNode
  desktopShaderProps?: Partial<LiquidMetalProps>
  mobileShaderProps?: Partial<LiquidMetalProps>
}

export interface HeroLiquidMetalHeadingProps
  extends Omit<React.ComponentPropsWithoutRef<"div">, "title"> {
  title?: React.ReactNode
  subtitle?: React.ReactNode
  headingClassName?: string
}

export interface HeroLiquidMetalDescriptionProps
  extends React.ComponentPropsWithoutRef<"div"> {
  description?: React.ReactNode
  descriptionClassName?: string
}

export interface HeroLiquidMetalActionsProps
  extends React.ComponentPropsWithoutRef<"div"> {
  showCta?: boolean
  ctaProps?: Partial<HeroLiquidMetalCTAProps>
  renderCta?: (defaultCta: React.ReactNode) => React.ReactNode
}

export interface HeroLiquidMetalBadgesProps
  extends React.ComponentPropsWithoutRef<"div"> {
  showBadges?: boolean
  techStack?: HeroLiquidMetalTechItem[]
  renderBadge?: (
    tech: HeroLiquidMetalTechItem,
    index: number,
    defaultBadge: React.ReactNode
  ) => React.ReactNode
}

export interface HeroLiquidMetalVisualProps
  extends React.ComponentPropsWithoutRef<"div"> {
  desktopShaderProps?: Partial<LiquidMetalProps>
  desktopClassName?: string
}

export interface HeroLiquidMetalMobileVisualProps
  extends React.ComponentPropsWithoutRef<"div"> {
  mobileShaderProps?: Partial<LiquidMetalProps>
}

export interface HeroLiquidMetalProps extends HeroLiquidMetalRootProps {
  containerClassName?: string
  contentClassName?: string
  headingWrapClassName?: string
  headingClassName?: string
  descriptionWrapClassName?: string
  descriptionClassName?: string
  ctaWrapClassName?: string
  badgesWrapClassName?: string
  visualClassName?: string
  mobileVisualClassName?: string
}

interface HeroLiquidMetalContextValue {
  srTitle: string
  title: React.ReactNode
  subtitle: React.ReactNode
  description: React.ReactNode
  showCta: boolean
  mergedCtaProps: HeroLiquidMetalCTAProps
  renderCta?: (defaultCta: React.ReactNode) => React.ReactNode
  showBadges: boolean
  techStack: HeroLiquidMetalTechItem[]
  renderBadge?: HeroLiquidMetalBadgesProps["renderBadge"]
  mergedDesktopShaderProps: Partial<LiquidMetalProps>
  mergedMobileShaderProps: Partial<LiquidMetalProps>
}

const defaultDesktopShaderProps: Partial<LiquidMetalProps> = {
  width: 1280,
  height: 720,
  image: "/cult-icon.svg",
  colorBack: "#ffffff00",
  colorTint: "#2c5d72",
  shape: undefined,
  repetition: 6,
  softness: 0.8,
  shiftRed: 1,
  shiftBlue: -1,
  distortion: 0.4,
  contour: 0.4,
  angle: 0,
  speed: 1,
  scale: 0.6,
  fit: "contain",
}

const defaultMobileShaderProps: Partial<LiquidMetalProps> = {
  image: "/cult-icon.svg",
  colorBack: "#ffffff00",
  colorTint: "#2c5d72",
  shape: undefined,
  repetition: 6,
  softness: 0.8,
  shiftRed: 1,
  shiftBlue: -1,
  distortion: 0.4,
  contour: 0.4,
  angle: 0,
  speed: 1,
  scale: 0.68,
  fit: "contain",
  style: { height: "100%", width: "100%" },
}

const defaultCtaProps: HeroLiquidMetalCTAProps = {
  label: "Check it out today",
  href: "https://aisdkagents.com",
  target: "_blank",
  rel: "noopener noreferrer",
}

const defaultDescription = (
  <>
    Full-stack vercel ai sdk patterns for workflows, tool calling, and agent
    orchestration. Built with{" "}
    <span className="font-medium tracking-tight">ai sdk v6</span> and{" "}
    <span className="font-medium tracking-tight">shadcn/ui</span>.
    <span className="hidden sm:inline"> Headless, themable, practical.</span>
  </>
)

const defaultTechStack: HeroLiquidMetalTechItem[] = [
  {
    name: "Next.js",
    version: "v16",
    icon: NextjsIcon,
  },
  {
    name: "AI SDK",
    version: "v6",
    icon: AISDKIcon,
  },
]

const HeroLiquidMetalContext = React.createContext<
  HeroLiquidMetalContextValue | undefined
>(undefined)

function useHeroLiquidMetalContext() {
  const context = React.useContext(HeroLiquidMetalContext)
  if (!context) {
    throw new Error(
      "HeroLiquidMetal components must be used within HeroLiquidMetalRoot"
    )
  }
  return context
}

export function useHeroLiquidMetal() {
  return useHeroLiquidMetalContext()
}

export const HeroLiquidMetalRoot = React.forwardRef<
  HTMLElement,
  HeroLiquidMetalRootProps
>(({ className, children, srTitle = "AI SDK Agents", title = <span className="">
      AI SDK Agents
    </span>, subtitle = "Copy and Paste", description = defaultDescription, showCta = true, ctaProps, renderCta, showBadges = true, techStack = defaultTechStack, renderBadge, desktopShaderProps, mobileShaderProps, width, height, image, colorBack, colorTint, shape, repetition, softness, shiftRed, shiftBlue, distortion, contour, angle, speed, frame, scale, rotation, offsetX, offsetY, fit, originX, originY, minPixelRatio, maxPixelCount, ...props }, ref) => {
  const mergedCtaProps = React.useMemo(
    () => ({
      ...defaultCtaProps,
      ...ctaProps,
    }),
    [ctaProps]
  )

  const shaderOverrides = React.useMemo((): Partial<LiquidMetalProps> => {
    const overrides: Partial<LiquidMetalProps> = {}
    if (width !== undefined) overrides.width = width
    if (height !== undefined) overrides.height = height
    if (image !== undefined) overrides.image = image
    if (colorBack !== undefined) overrides.colorBack = colorBack
    if (colorTint !== undefined) overrides.colorTint = colorTint
    if (shape !== undefined) overrides.shape = shape
    if (repetition !== undefined) overrides.repetition = repetition
    if (softness !== undefined) overrides.softness = softness
    if (shiftRed !== undefined) overrides.shiftRed = shiftRed
    if (shiftBlue !== undefined) overrides.shiftBlue = shiftBlue
    if (distortion !== undefined) overrides.distortion = distortion
    if (contour !== undefined) overrides.contour = contour
    if (angle !== undefined) overrides.angle = angle
    if (speed !== undefined) overrides.speed = speed
    if (frame !== undefined) overrides.frame = frame
    if (scale !== undefined) overrides.scale = scale
    if (rotation !== undefined) overrides.rotation = rotation
    if (offsetX !== undefined) overrides.offsetX = offsetX
    if (offsetY !== undefined) overrides.offsetY = offsetY
    if (fit !== undefined) overrides.fit = fit
    if (originX !== undefined) overrides.originX = originX
    if (originY !== undefined) overrides.originY = originY
    if (minPixelRatio !== undefined) overrides.minPixelRatio = minPixelRatio
    if (maxPixelCount !== undefined) overrides.maxPixelCount = maxPixelCount
    return overrides
  }, [
    width,
    height,
    image,
    colorBack,
    colorTint,
    shape,
    repetition,
    softness,
    shiftRed,
    shiftBlue,
    distortion,
    contour,
    angle,
    speed,
    frame,
    scale,
    rotation,
    offsetX,
    offsetY,
    fit,
    originX,
    originY,
    minPixelRatio,
    maxPixelCount,
  ])

  const mergedDesktopShaderProps = React.useMemo(
    () => ({
      ...defaultDesktopShaderProps,
      ...shaderOverrides,
      ...desktopShaderProps,
    }),
    [shaderOverrides, desktopShaderProps]
  )

  const mergedMobileShaderProps = React.useMemo(
    () => ({
      ...defaultMobileShaderProps,
      ...shaderOverrides,
      ...mobileShaderProps,
      style: {
        ...(defaultMobileShaderProps.style as React.CSSProperties),
        ...(mobileShaderProps?.style as React.CSSProperties | undefined),
      },
    }),
    [shaderOverrides, mobileShaderProps]
  )

  const contextValue = React.useMemo<HeroLiquidMetalContextValue>(
    () => ({
      srTitle,
      title,
      subtitle,
      description,
      showCta,
      mergedCtaProps,
      renderCta,
      showBadges,
      techStack,
      renderBadge,
      mergedDesktopShaderProps,
      mergedMobileShaderProps,
    }),
    [
      srTitle,
      title,
      subtitle,
      description,
      showCta,
      mergedCtaProps,
      renderCta,
      showBadges,
      techStack,
      renderBadge,
      mergedDesktopShaderProps,
      mergedMobileShaderProps,
    ]
  )

  return (
    <HeroLiquidMetalContext.Provider value={contextValue}>
      <section
        className={cn("relative h-full w-full overflow-hidden", className)}
        data-slot="hero-liquid-metal-root"
        ref={ref}
        {...props}
      >
        <h1 className="sr-only">{srTitle}</h1>
        {children}
      </section>
    </HeroLiquidMetalContext.Provider>
  )
})
HeroLiquidMetalRoot.displayName = "HeroLiquidMetalRoot"

export function HeroLiquidMetalContainer({
  className,
  ...props
}: React.ComponentPropsWithoutRef<"div">) {
  return (
    <div
      className={cn(
        "relative z-10 container grid gap-6 pb-16 sm:gap-8 sm:pb-20 lg:grid-cols-[1fr_minmax(300px,500px)] lg:items-center lg:gap-12 lg:pb-24 xl:grid-cols-[1fr_1fr]",
        className
      )}
      data-slot="hero-liquid-metal-container"
      {...props}
    />
  )
}

export function HeroLiquidMetalContent({
  className,
  ...props
}: React.ComponentPropsWithoutRef<"div">) {
  return (
    <div
      className={cn(
        "flex flex-col justify-center gap-4 text-balance sm:gap-5 sm:px-4 md:px-8 lg:gap-6 lg:pr-0 lg:pl-4 xl:pl-8 2xl:pl-0",
        className
      )}
      data-slot="hero-liquid-metal-content"
      {...props}
    />
  )
}

export function HeroLiquidMetalHeading({
  className,
  title,
  subtitle,
  headingClassName,
  children,
  ...props
}: HeroLiquidMetalHeadingProps) {
  const context = useHeroLiquidMetalContext()
  const resolvedTitle = title ?? context.title
  const resolvedSubtitle = subtitle ?? context.subtitle

  return (
    <div
      className={cn("pt-4 text-center sm:pt-6 lg:pt-0 lg:text-left", className)}
      data-slot="hero-liquid-metal-heading-wrap"
      {...props}
    >
      {children ?? (
        <div className="relative">
          <h2
            className={cn(
              "relative mb-0 text-3xl font-medium tracking-[-0.04em] text-balance sm:text-4xl md:text-5xl lg:tracking-[-0.06em] xl:text-6xl 2xl:text-7xl",
              headingClassName
            )}
            data-slot="hero-liquid-metal-heading"
          >
            {resolvedTitle} <br />
            {resolvedSubtitle}
          </h2>
        </div>
      )}
    </div>
  )
}

export function HeroLiquidMetalDescription({
  className,
  description,
  descriptionClassName,
  children,
  ...props
}: HeroLiquidMetalDescriptionProps) {
  const context = useHeroLiquidMetalContext()
  const resolvedDescription = description ?? context.description

  return (
    <div
      className={cn(
        "mx-auto max-w-xl pb-2 text-center sm:pb-4 lg:mx-0 lg:max-w-none lg:pb-0 lg:text-left",
        className
      )}
      data-slot="hero-liquid-metal-description-wrap"
      {...props}
    >
      {children ?? (
        <p
          className={cn(
            "text-foreground/70 md:text-foreground/80 mt-0 mb-0 font-sans text-sm sm:text-base lg:text-lg xl:text-xl",
            descriptionClassName
          )}
          data-slot="hero-liquid-metal-description"
        >
          {resolvedDescription}
        </p>
      )}
    </div>
  )
}

export function HeroLiquidMetalActions({
  className,
  showCta,
  ctaProps,
  renderCta,
  children,
  ...props
}: HeroLiquidMetalActionsProps) {
  const context = useHeroLiquidMetalContext()
  const shouldShowCta = showCta ?? context.showCta
  const resolvedCtaProps = { ...context.mergedCtaProps, ...ctaProps }
  const resolvedRenderCta = renderCta ?? context.renderCta

  if (!shouldShowCta) {
    return null
  }

  const defaultCta = <HeroLiquidMetalCTA {...resolvedCtaProps} />

  return (
    <div
      className={cn("flex justify-center lg:justify-start", className)}
      data-slot="hero-liquid-metal-cta-wrap"
      {...props}
    >
      {children ??
        (resolvedRenderCta ? resolvedRenderCta(defaultCta) : defaultCta)}
    </div>
  )
}

export function HeroLiquidMetalCTA({
  label,
  href,
  target,
  rel,
  onClick,
  className,
  buttonClassName,
}: HeroLiquidMetalCTAProps) {
  return (
    <div
      className={cn(
        "flex items-center justify-center gap-4 pb-4 md:pb-0",
        className
      )}
      data-slot="hero-liquid-metal-cta"
    >
      <Button asChild className={cn("", buttonClassName)} size="lg">
        <a href={href} onClick={onClick} rel={rel} target={target}>
          {label}
        </a>
      </Button>
    </div>
  )
}

export function HeroLiquidMetalBadges({
  className,
  showBadges,
  techStack,
  renderBadge,
  ...props
}: HeroLiquidMetalBadgesProps) {
  const context = useHeroLiquidMetalContext()
  const shouldShowBadges = showBadges ?? context.showBadges
  const resolvedTechStack = techStack ?? context.techStack
  const resolvedRenderBadge = renderBadge ?? context.renderBadge

  if (!shouldShowBadges) {
    return null
  }

  return (
    <div
      className={cn(
        "flex flex-wrap items-center justify-center gap-2.5",
        className
      )}
      data-slot="hero-liquid-metal-badges"
      {...props}
    >
      {resolvedTechStack.map((tech, index) => {
        const Icon = tech.icon
        const defaultBadge = (
          <Badge
            className={cn(
              "group relative px-3.5 py-1.5 font-medium transition-all duration-150",
              "border-border/50 bg-card text-card-foreground border",
              "shadow-[0_1px_3px_rgba(0,0,0,0.08)] dark:shadow-[0_1px_3px_rgba(0,0,0,0.3)]",
              "hover:-translate-y-px hover:shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:hover:shadow-[0_2px_8px_rgba(0,0,0,0.4)]"
            )}
            data-slot="hero-liquid-metal-badge"
            key={tech.name}
            variant="outline"
          >
            {Icon ? <Icon className="mr-1 size-3.5 opacity-80" /> : null}
            <span className="font-semibold tracking-tight">{tech.name}</span>
            {tech.version ? (
              <span className="font-mono text-xs opacity-50">
                {tech.version}
              </span>
            ) : null}
          </Badge>
        )

        if (resolvedRenderBadge) {
          return (
            <React.Fragment key={tech.name}>
              {resolvedRenderBadge(tech, index, defaultBadge)}
            </React.Fragment>
          )
        }

        return defaultBadge
      })}
    </div>
  )
}

export function HeroLiquidMetalVisual({
  className,
  desktopClassName,
  desktopShaderProps,
  ...props
}: HeroLiquidMetalVisualProps) {
  const context = useHeroLiquidMetalContext()
  const resolvedDesktopShaderProps = {
    ...context.mergedDesktopShaderProps,
    ...desktopShaderProps,
  }

  return (
    <div
      className={cn(
        "relative hidden h-[350px] lg:block lg:h-[400px] xl:h-[500px]",
        className
      )}
      data-slot="hero-liquid-metal-visual"
      {...props}
    >
      <div
        className={cn(
          "absolute inset-0 flex items-center justify-center overflow-hidden rounded-full",
          desktopClassName
        )}
        data-slot="hero-liquid-metal-desktop"
      >
        <MemoizedLiquidMetal
          {...resolvedDesktopShaderProps}
          image={
            resolvedDesktopShaderProps.image ??
            (defaultDesktopShaderProps.image as string)
          }
        />
      </div>
    </div>
  )
}

export function HeroLiquidMetalMobileVisual({
  className,
  mobileShaderProps,
  ...props
}: HeroLiquidMetalMobileVisualProps) {
  const context = useHeroLiquidMetalContext()
  const resolvedMobileShaderProps = {
    ...context.mergedMobileShaderProps,
    ...mobileShaderProps,
    style: {
      ...(context.mergedMobileShaderProps.style as React.CSSProperties),
      ...(mobileShaderProps?.style as React.CSSProperties | undefined),
    },
  }

  return (
    <div
      className={cn(
        "pointer-events-none absolute inset-x-0 -bottom-24 -z-10 h-[360px] overflow-hidden lg:hidden",
        className
      )}
      data-slot="hero-liquid-metal-mobile"
      {...props}
    >
      <div className="from-background via-background/95 absolute inset-x-0 top-0 z-10 h-56 bg-gradient-to-b to-transparent" />
      <MemoizedLiquidMetal
        {...resolvedMobileShaderProps}
        image={
          resolvedMobileShaderProps.image ??
          (defaultMobileShaderProps.image as string)
        }
      />
    </div>
  )
}

export function HeroLiquidMetal({
  containerClassName,
  contentClassName,
  headingWrapClassName,
  headingClassName,
  descriptionWrapClassName,
  descriptionClassName,
  ctaWrapClassName,
  badgesWrapClassName,
  visualClassName,
  mobileVisualClassName,
  ...props
}: HeroLiquidMetalProps) {
  return (
    <HeroLiquidMetalRoot {...props}>
      <HeroLiquidMetalContainer className={containerClassName}>
        <HeroLiquidMetalContent className={contentClassName}>
          <HeroLiquidMetalHeading
            className={headingWrapClassName}
            headingClassName={headingClassName}
          />
          <HeroLiquidMetalDescription
            className={descriptionWrapClassName}
            descriptionClassName={descriptionClassName}
          />
          <HeroLiquidMetalActions className={ctaWrapClassName} />
          <div
            className={cn(
              "hidden justify-center lg:flex lg:justify-start",
              badgesWrapClassName
            )}
            data-slot="hero-liquid-metal-badges-wrap"
          >
            <HeroLiquidMetalBadges />
          </div>
        </HeroLiquidMetalContent>
        <HeroLiquidMetalVisual className={visualClassName} />
      </HeroLiquidMetalContainer>
      <HeroLiquidMetalMobileVisual className={mobileVisualClassName} />
    </HeroLiquidMetalRoot>
  )
}

export function AISDKIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      color="currentcolor"
      data-testid="geist-icon"
      height="1em"
      strokeLinejoin="round"
      viewBox="0 0 16 16"
      width="1em"
      {...props}
    >
      <title>AI SDK</title>
      <path
        d="M2.5.5V0h1v.5a2 2 0 002 2H6v1h-.5a2 2 0 00-2 2V6h-1v-.5a2 2 0 00-2-2H0v-1h.5a2 2 0 002-2zM14.5 4.5V5h-1v-.5a1 1 0 00-1-1H12v-1h.5a1 1 0 001-1V1h1v.5a1 1 0 001 1h.5v1h-.5a1 1 0 00-1 1zM8.407 4.93L8.5 4h1l.093.93a5 5 0 004.478 4.477L15 9.5v1l-.93.093a5 5 0 00-4.477 4.478L9.5 16h-1l-.093-.93a5 5 0 00-4.478-4.477L3 10.5v-1l.93-.093A5 5 0 008.406 4.93z"
        fill="currentColor"
      />
    </svg>
  )
}

export function NextjsIcon(props: SVGProps<SVGSVGElement>) {
  const id = React.useId()
  const maskId = `${id}-mask`
  const paint0Id = `${id}-paint0`
  const paint1Id = `${id}-paint1`

  return (
    <svg
      fill="none"
      height="1em"
      viewBox="0 0 180 180"
      width="1em"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Next.js</title>
      <mask
        height={180}
        id={maskId}
        maskUnits="userSpaceOnUse"
        style={{
          maskType: "alpha",
        }}
        width={180}
        x={0}
        y={0}
      >
        <circle cx={90} cy={90} fill="black" r={90} />
      </mask>
      <g mask={`url(#${maskId})`}>
        <circle
          cx={90}
          cy={90}
          fill="black"
          r={87}
          stroke="white"
          strokeWidth={6}
        />
        <path
          d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
          fill={`url(#${paint0Id})`}
        />
        <rect
          fill={`url(#${paint1Id})`}
          height={72}
          width={12}
          x={115}
          y={54}
        />
      </g>
      <defs>
        <linearGradient
          gradientUnits="userSpaceOnUse"
          id={paint0Id}
          x1={109}
          x2={144.5}
          y1={116.5}
          y2={160.5}
        >
          <stop stopColor="white" />
          <stop offset={1} stopColor="white" stopOpacity={0} />
        </linearGradient>
        <linearGradient
          gradientUnits="userSpaceOnUse"
          id={paint1Id}
          x1={121}
          x2={120.799}
          y1={54}
          y2={106.875}
        >
          <stop stopColor="white" />
          <stop offset={1} stopColor="white" stopOpacity={0} />
        </linearGradient>
      </defs>
    </svg>
  )
}

export default HeroLiquidMetal
```

**Optionally copy the demo as a starting point.**

```tsx title="hero-liquid-metal-demo.tsx"
"use client"

import {
  AISDKIcon,
  HeroLiquidMetal,
  HeroLiquidMetalActions,
  HeroLiquidMetalBadges,
  HeroLiquidMetalContainer,
  HeroLiquidMetalContent,
  HeroLiquidMetalDescription,
  HeroLiquidMetalHeading,
  HeroLiquidMetalMobileVisual,
  HeroLiquidMetalRoot,
  HeroLiquidMetalVisual,
  NextjsIcon,
} from "../ui/hero-liquid-metal"

export default function HeroLiquidMetalDemo() {
  return (
    <div className="space-y-16">
      <div className="space-y-4">
        <h3 className="flex items-center justify-center text-lg font-medium">
          Default
        </h3>
        <HeroLiquidMetal />
      </div>

      <div className="space-y-4">
        <h3 className="flex items-center justify-center text-lg font-medium">
          Root-level shader props
        </h3>
        <HeroLiquidMetal
          width={1280}
          height={720}
          image="https://shaders.paper.design/images/logos/diamond.svg"
          colorBack="#ffffff00"
          colorTint="#2c5d72"
          shape={undefined}
          repetition={6}
          softness={0.8}
          shiftRed={1}
          shiftBlue={-1}
          distortion={0.4}
          contour={0.4}
          angle={0}
          speed={1}
          scale={0.6}
          fit="contain"
        />
      </div>

      <div className="space-y-4">
        <h3 className="flex items-center justify-center text-lg font-medium">
          Configured via props
        </h3>
        <HeroLiquidMetalRoot
          ctaProps={{
            href: "#",
            label: "Try NewCopy for marketing",
          }}
          description="Human-in-the-loop marketing copy software that helps teams draft, review, and ship high-converting campaigns with confidence."
          desktopShaderProps={{
            scale: 0.8,
            speed: 0.7,
            repetition: 7,
            softness: 0.9,
            distortion: 0.3,
            colorTint: "#4f46e5",
          }}
          image="https://shaders.paper.design/images/logos/diamond.svg"
          mobileShaderProps={{
            speed: 0.65,
            scale: 0.78,
            colorTint: "#0ea5e9",
          }}
          srTitle="Composable hero"
          subtitle="Marketing"
          techStack={[
            { name: "Next.js", version: "v16" },
            { name: "AI SDK", version: "v6" },
            { name: "Tailwind", version: "v4" },
          ]}
          title="Cursor for"
        >
          <HeroLiquidMetalContainer>
            <HeroLiquidMetalContent>
              <HeroLiquidMetalHeading />
              <HeroLiquidMetalDescription />
              <HeroLiquidMetalActions />
              <div
                className="hidden justify-center lg:flex lg:justify-start"
                data-slot="hero-liquid-metal-badges-wrap"
              >
                <HeroLiquidMetalBadges
                  techStack={[
                    { name: "Next.js", version: "v16", icon: NextjsIcon },
                    { name: "AI SDK", version: "v6", icon: AISDKIcon },
                  ]}
                />
              </div>
            </HeroLiquidMetalContent>
            <HeroLiquidMetalVisual />
          </HeroLiquidMetalContainer>
          <HeroLiquidMetalMobileVisual />
        </HeroLiquidMetalRoot>
      </div>
    </div>
  )
}
```

**Update import paths to match your project structure.**

## Usage

### Quick start with the composite component

Pass shader props directly on `HeroLiquidMetal` for a one-line hero:

```tsx
import { HeroLiquidMetal } from "@/components/ui/hero-liquid-metal";

export default function Page() {
  return (
    <main className="min-h-screen">
      <HeroLiquidMetal
        width={1280}
        height={720}
        image="https://shaders.paper.design/images/logos/diamond.svg"
        colorBack="#ffffff00"
        colorTint="#2c5d72"
        shape={undefined}
        repetition={6}
        softness={0.8}
        shiftRed={1}
        shiftBlue={-1}
        distortion={0.4}
        contour={0.4}
        angle={0}
        speed={1}
        scale={0.6}
        fit="contain"
      />
    </main>
  );
}
```

### Compose with primitives

For full control over layout and copy, use the root and slot components:

```tsx
import {
  HeroLiquidMetalRoot,
  HeroLiquidMetalContainer,
  HeroLiquidMetalContent,
  HeroLiquidMetalHeading,
  HeroLiquidMetalDescription,
  HeroLiquidMetalActions,
  HeroLiquidMetalBadges,
  HeroLiquidMetalVisual,
  HeroLiquidMetalMobileVisual,
} from "@/components/ui/hero-liquid-metal";
```

```tsx
export default function Page() {
  return (
    <main className="min-h-screen">
      <HeroLiquidMetalRoot>
        <HeroLiquidMetalContainer>
          <HeroLiquidMetalContent>
            <HeroLiquidMetalHeading />
            <HeroLiquidMetalDescription />
            <HeroLiquidMetalActions />
            <div
              className="flex justify-center lg:justify-start"
              data-slot="hero-liquid-metal-badges-wrap"
            >
              <HeroLiquidMetalBadges />
            </div>
          </HeroLiquidMetalContent>
          <HeroLiquidMetalVisual />
        </HeroLiquidMetalContainer>
        <HeroLiquidMetalMobileVisual />
      </HeroLiquidMetalRoot>
    </main>
  );
}
```

## What You Get

- Split hero layout with copy and CTA on the left and LiquidMetal shader visual on the right.
- Memoized `LiquidMetal` render path for stable re-renders.
- Desktop and mobile shader variants with configurable tint, repetition, softness, dispersion, and distortion.
- **Root-level shader props**: pass shader/common props directly on `HeroLiquidMetal` or `HeroLiquidMetalRoot` for convenience.
- Built-in badge row for highlighting stack metadata.
- Works as a full-width section and can be dropped into landing pages directly.

## API Reference

### Exports

| Export | Type | Description |
| --- | --- | --- |
| `HeroLiquidMetalRoot` | `HeroLiquidMetalRootProps` | Context provider and root section primitive |
| `HeroLiquidMetalContainer` | `div` primitive | Main hero grid wrapper |
| `HeroLiquidMetalContent` | `div` primitive | Left-side content column primitive |
| `HeroLiquidMetalHeading` | `HeroLiquidMetalHeadingProps` | Heading block primitive (title + subtitle) |
| `HeroLiquidMetalDescription` | `HeroLiquidMetalDescriptionProps` | Description block primitive |
| `HeroLiquidMetalActions` | `HeroLiquidMetalActionsProps` | CTA wrapper primitive |
| `HeroLiquidMetalCTA` | `HeroLiquidMetalCTAProps` | Reusable CTA slot component |
| `HeroLiquidMetalBadges` | `HeroLiquidMetalBadgesProps` | Reusable badge row primitive |
| `HeroLiquidMetalVisual` | `HeroLiquidMetalVisualProps` | Desktop visual shader wrapper primitive |
| `HeroLiquidMetalMobileVisual` | `HeroLiquidMetalMobileVisualProps` | Mobile visual shader wrapper primitive |
| `HeroLiquidMetal` / `default` | `HeroLiquidMetalProps` | Convenience assembled version built from primitives |
| `useHeroLiquidMetal` | `() => HeroLiquidMetalContextValue` | Hook for context-driven custom composition |
| `HeroLiquidMetalShaderOverrides` | type | Shader/common props that can be passed at the root |

### Props

`HeroLiquidMetalRoot` owns shared defaults, normalized shader props, and composition state.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `srTitle` | `string` | `"AI SDK Agents"` | Screen-reader-only heading text |
| `title` | `ReactNode` | `"AI SDK Agents"` | First line of visible heading |
| `subtitle` | `ReactNode` | `"Copy and Paste"` | Second line of visible heading |
| `description` | `ReactNode` | built-in marketing copy | Supporting paragraph content |
| `showCta` | `boolean` | `true` | Toggle CTA rendering |
| `ctaProps` | `Partial<HeroLiquidMetalCTAProps>` | built-in CTA | Override CTA label/link/target/rel/click handler |
| `renderCta` | `(defaultCta) => ReactNode` | `undefined` | Wrap or replace default CTA node |
| `showBadges` | `boolean` | `true` | Toggle tech badges |
| `techStack` | `HeroLiquidMetalTechItem[]` | Next.js + AI SDK | Badge data source |
| `renderBadge` | `(tech, index, defaultBadge) => ReactNode` | `undefined` | Custom badge rendering hook |
| `desktopShaderProps` | `Partial<LiquidMetalProps>` | built-in desktop shader settings | Override desktop shader values |
| `mobileShaderProps` | `Partial<LiquidMetalProps>` | built-in mobile shader settings | Override mobile shader values |
| `className` | `string` | `undefined` | Root section class override |

### Root-level shader/common props (HeroLiquidMetalShaderOverrides)

You can pass these on `HeroLiquidMetal` or `HeroLiquidMetalRoot`; they are merged into both desktop and mobile shader props (before `desktopShaderProps` / `mobileShaderProps`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number \| string` | `1280` | CSS width style of the shader |
| `height` | `number \| string` | `720` | CSS height style of the shader |
| `image` | `string \| HTMLImageElement` | `"/cult-icon.svg"` | Optional effect mask image (transparent background recommended) |
| `shape` | `"none" \| "circle" \| "daisy" \| "metaballs"` | `undefined` | Fallback predefined shape when no `image` is used |
| `colorBack` | `string` | `"#ffffff00"` | Background color |
| `colorTint` | `string` | `"#2c5d72"` | Overlay tint color (color burn blend) |
| `repetition` | `number` | `6` | Density of pattern stripes (1-10) |
| `softness` | `number` | `0.8` | Color transition sharpness (0-1) |
| `shiftRed` | `number` | `1` | R-channel dispersion (-1 to 1) |
| `shiftBlue` | `number` | `-1` | B-channel dispersion (-1 to 1) |
| `distortion` | `number` | `0.4` | Noise distortion over pattern (0-1) |
| `contour` | `number` | `0.4` | Distortion strength on shape edges (0-1) |
| `angle` | `number` | `0` | Pattern animation direction (0-360) |
| `speed` | `number` | `1` | Animation speed (`0` pauses animation) |
| `frame` | `number` | `undefined` | Starting point of animation |
| `scale` | `number` | `0.6` (desktop) | Overall zoom level (0.01-4) |
| `rotation` | `number` | `undefined` | Overall rotation angle (0-360) |
| `offsetX` | `number` | `undefined` | Horizontal center offset (-1 to 1) |
| `offsetY` | `number` | `undefined` | Vertical center offset (-1 to 1) |
| `fit` | `"contain" \| "cover"` | `"contain"` | Fit mode for rendering into canvas |
| `originX` | `number` | `undefined` | Reference point for world width in canvas (0-1) |
| `originY` | `number` | `undefined` | Reference point for world height in canvas (0-1) |
| `minPixelRatio` | `number` | shader default | Minimum render pixel ratio |
| `maxPixelCount` | `number` | shader default | Maximum pixel count to process |

## Customization Patterns

### Tune LiquidMetal via root-level props

Use the composite component and pass shader props directly:

```tsx
<HeroLiquidMetal
  width={1280}
  height={720}
  image="https://shaders.paper.design/images/logos/diamond.svg"
  colorBack="#ffffff00"
  colorTint="#2c5d72"
  repetition={6}
  softness={0.8}
  shiftRed={1}
  shiftBlue={-1}
  distortion={0.4}
  contour={0.4}
  angle={0}
  speed={1}
  scale={0.6}
  fit="contain"
/>
```

### Override desktop and mobile separately

Use `desktopShaderProps` and `mobileShaderProps` when composing with primitives:

```tsx
<HeroLiquidMetalRoot
  desktopShaderProps={{
    speed: 0.7,
    scale: 0.8,
    repetition: 7,
    softness: 0.9,
    distortion: 0.3,
    colorTint: "#4f46e5",
  }}
  mobileShaderProps={{
    speed: 0.65,
    scale: 0.78,
    colorTint: "#0ea5e9",
  }}
>
  {/* compose primitives */}
</HeroLiquidMetalRoot>
```

### Swap hero copy and CTA

Replace copy and CTA target via props:

```tsx
<HeroLiquidMetalRoot
  title="Your Product"
  subtitle="Ship faster"
  description="Reusable hero primitive with configurable LiquidMetal visuals."
  ctaProps={{
    label: "Get started",
    href: "/signup",
    target: "_self",
  }}
>
  {/* compose primitives */}
</HeroLiquidMetalRoot>
```

### Compose with slot primitives

Use primitive exports for custom layout composition:

```tsx
<HeroLiquidMetalRoot>
  <HeroLiquidMetalContainer>
    <HeroLiquidMetalContent>
      <HeroLiquidMetalHeading />
      <HeroLiquidMetalDescription />
      <HeroLiquidMetalActions />
      <HeroLiquidMetalBadges />
    </HeroLiquidMetalContent>
    <HeroLiquidMetalVisual
      desktopShaderProps={{ colorTint: "#0f766e", distortion: 0.28 }}
    />
  </HeroLiquidMetalContainer>
  <HeroLiquidMetalMobileVisual />
</HeroLiquidMetalRoot>
```

## Performance Notes

- `LiquidMetal` is wrapped with `memo` to avoid unnecessary rerenders.
- Mobile and desktop effects are rendered as separate sections so each layout can be tuned independently.
- Keep the hero in client-rendered regions only (`"use client"` is required).

## Accessibility Notes

- Includes a screen-reader-only heading (`sr-only`) so page structure remains meaningful.
- CTA link includes `rel="noopener noreferrer"` when opened in a new tab.
- Ensure visible contrast remains sufficient if you change shader colors or text opacity.

## Troubleshooting

### Shader does not render

- Confirm `@paper-design/shaders-react` is installed.
- Verify this component runs in a client context.
- Confirm parent containers are not clipping to zero height.
- Ensure `image` is a valid URL or loaded image if you are not using a predefined `shape`.

### Layout looks crowded on small screens

- Increase the mobile shader container height.
- Reduce heading size classes or copy length.
- Move badges under the fold for dense hero content.
