# CTA 03

> Full-bleed CTA card with an animated dithered shader backdrop, an eyebrow pill, a serif headline and a pill action button.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/cta-03).

- Page, with a live preview: https://shadcn.exchange/r/hirael/cta-03
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/cta-03-light-41525a58cccac595.png
- Registry item: https://hirael.com/r/cta-03.json
- Categories: blocks, cta
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/cta-03.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `shaders`
- Registry items: `button`, `https://hirael.com/r/use-reduced-motion.json`

## Files

### `registry/hirael/bases/radix/blocks/cta-03/cta-03.tsx`

```tsx
// CTA 03 from Hirael <https://hirael.com/blocks/cta/cta-03>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import dynamic from 'next/dynamic';
import { ArrowRight, PenLine } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';

const Cta03Backdrop = dynamic(() => import('./cta-03-backdrop'), {
  ssr: false,
  loading: () => <div className="size-full bg-muted/20" />,
});

const ENTER =
  'animate-in fade-in slide-in-from-bottom-4 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';

const stagger = (index: number): React.CSSProperties => ({ animationDelay: `${index * 70}ms` });

const Cta03 = () => {
  const [active, setActive] = React.useState(false);

  return (
    <section data-slot="cta" className="flex w-full items-center justify-center px-4 py-12 md:px-6">
      <div
        className="relative w-full max-w-7xl"
        onMouseEnter={() => setActive(true)}
        onMouseLeave={() => setActive(false)}
      >
        <div
          data-slot="cta-panel"
          className="relative flex min-h-[600px] flex-col items-center justify-center overflow-hidden rounded-3xl border border-border bg-card shadow-sm md:rounded-[48px]"
        >
          <div
            aria-hidden
            data-slot="cta-backdrop"
            className="pointer-events-none absolute inset-0 opacity-40 mix-blend-multiply dark:opacity-30 dark:mix-blend-screen"
          >
            <Cta03Backdrop active={active} />
          </div>

          <div
            data-slot="cta-body"
            className="relative z-10 mx-auto flex max-w-4xl flex-col items-center px-6 py-16 text-center"
          >
            <span
              data-slot="cta-eyebrow"
              className={cn(
                ENTER,
                'mb-8 inline-flex items-center gap-2 rounded-full border border-primary/10 bg-primary/5 px-4 py-1.5 text-sm font-medium text-foreground backdrop-blur-sm',
              )}
            >
              <PenLine aria-hidden className="size-3.5" />
              AI writing
            </span>

            <h2
              data-slot="cta-title"
              style={stagger(1)}
              className={cn(
                ENTER,
                'mb-8 font-serif text-5xl leading-[1.05] font-medium tracking-tight text-foreground md:text-7xl lg:text-8xl',
              )}
            >
              Your words,
              <br />
              <span className="text-foreground/80">only sharper.</span>
            </h2>

            <p
              data-slot="cta-description"
              style={stagger(2)}
              className={cn(ENTER, 'mb-12 max-w-2xl text-lg leading-relaxed text-muted-foreground md:text-xl')}
            >
              An AI editor that keeps your voice and tightens everything else. It drafts, trims, and proofs while you
              type.
            </p>

            <div data-slot="cta-actions" style={stagger(3)} className={ENTER}>
              <Button asChild size="lg" className="group">
                <a href="#">
                  Start writing
                  <ArrowRight className="size-5 transition-transform duration-150 group-hover:translate-x-1 rtl:rotate-180 rtl:group-hover:-translate-x-1" />
                </a>
              </Button>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export default Cta03;
```

### `registry/hirael/bases/radix/blocks/cta-03/cta-03-backdrop.tsx`

```tsx
// CTA 03 from Hirael <https://hirael.com/blocks/cta/cta-03>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import { Dither, Shader, Swirl } from 'shaders/react';
import { useReducedMotion } from '@/registry/hirael/hooks/use-reduced-motion';

// The shader can't read `var()`, so tokens are resolved to concrete colors through the DOM.
const resolveColor = (value: string) => {
  const probe = document.createElement('span');
  probe.style.color = value;
  document.body.append(probe);
  const resolved = getComputedStyle(probe).color;
  probe.remove();

  return resolved;
};

const readPalette = () => ({
  base: resolveColor('var(--background)'),
  ink: resolveColor('color-mix(in oklab, var(--foreground) 65%, var(--background))'),
  halftone: resolveColor('color-mix(in oklab, var(--foreground) 50%, var(--background))'),
});

type Palette = ReturnType<typeof readPalette>;

const usePalette = () => {
  const [palette, setPalette] = React.useState<Palette | null>(() =>
    typeof document === 'undefined' ? null : readPalette(),
  );

  React.useEffect(() => {
    const observer = new MutationObserver(() => setPalette(readPalette()));
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class'],
    });

    return () => observer.disconnect();
  }, []);

  return palette;
};

interface Cta03BackdropProps {
  active?: boolean;
}

const Cta03Backdrop = ({ active = false }: Cta03BackdropProps) => {
  const palette = usePalette();
  const reduced = useReducedMotion();
  if (!palette) return null;

  return (
    <Shader style={{ width: '100%', height: '100%' }}>
      <Swirl colorA={palette.base} colorB={palette.ink} speed={reduced ? 0 : active ? 0.6 : 0.2} detail={1.6} />
      <Dither colorMode="custom" colorA="transparent" colorB={palette.halftone} pattern="bayer4" pixelSize={3} />
    </Shader>
  );
};

export default Cta03Backdrop;
```
