# Integrations 04

> Centered integrations section: heading and copy above a hub with 6 linked tool tiles on straight spokes, with pulses travelling in and out along each line.

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

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

## Install

```bash
npx shadcn@latest add https://hirael.com/r/integrations-04.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`
- Registry items: `button`

## Files

### `registry/hirael/bases/radix/blocks/integrations-04/integrations-04.tsx`

```tsx
// Integrations 04 from Hirael <https://hirael.com/blocks/integrations/integrations-04>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

import { ArrowRight, Cloud, Database, GitBranch, Lock, Mail, MessageCircle, type LucideIcon } from 'lucide-react';

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

interface BrandMarkProps {
  className?: string;
}

const BrandMark = ({ className }: BrandMarkProps) => {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className={className}>
      <path d="M2.3 12h2.4v10.95h6.2V14.6h4.6v8.35h6.2V12h-2.4V1.05h-6.2V9.4H8.5V1.05H2.3Z" />
    </svg>
  );
};

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, step = 60, offset = 0): React.CSSProperties => ({
  animationDelay: `${offset + index * step}ms`,
});

interface Spoke {
  name: string;
  icon: LucideIcon;
  /** Degrees clockwise from 12 o'clock. */
  angle: number;
  href: string;
}

const SPOKES: readonly Spoke[] = [
  { name: 'Postgres', icon: Database, angle: 0, href: '#' },
  { name: 'Resend', icon: Mail, angle: 60, href: '#' },
  { name: 'S3', icon: Cloud, angle: 120, href: '#' },
  { name: 'Git', icon: GitBranch, angle: 180, href: '#' },
  { name: 'Vault', icon: Lock, angle: 240, href: '#' },
  { name: 'Discord', icon: MessageCircle, angle: 300, href: '#' },
] as const;

// The viewBox is 100 x 72 and the diagram keeps that aspect ratio, so a percentage of the
// box is the same point in both the SVG and the absolutely placed tiles.
const WIDTH = 100;
const HEIGHT = 72;
const CX = WIDTH / 2;
const CY = HEIGHT / 2;
const RX = 34;
const RY = 29;

const spokePosition = (angle: number) => {
  const rad = (angle * Math.PI) / 180;

  return {
    x: Math.round((CX + RX * Math.sin(rad)) * 100) / 100,
    y: Math.round((CY - RY * Math.cos(rad)) * 100) / 100,
  };
};

const Integrations04 = () => {
  return (
    <section
      data-slot="integrations"
      className="bg-background py-20 sm:py-28"
      aria-labelledby="integrations-04-heading"
    >
      <div className="mx-auto flex w-full max-w-3xl flex-col items-center px-4 text-center">
        <h2
          id="integrations-04-heading"
          style={stagger(1)}
          className={cn(ENTER, 'font-serif text-4xl font-medium tracking-tight text-balance sm:text-5xl')}
        >
          One hub for your entire stack
        </h2>
        <p style={stagger(2)} className={cn(ENTER, 'mt-4 max-w-xl text-base text-pretty text-muted-foreground')}>
          Hirael sits in the middle of your workflow and keeps every tool you already run in step, so nothing drifts.
        </p>
      </div>

      <div
        data-slot="integrations-hub"
        dir="ltr"
        role="group"
        aria-label="Hirael at the center of its integrations"
        style={stagger(3)}
        className={cn(ENTER, 'relative mx-auto mt-12 aspect-[100/72] w-full max-w-xl px-4 sm:mt-16')}
      >
        <div className="relative size-full">
          <div aria-hidden className="absolute inset-[16%] rounded-full border border-dashed border-border/70" />
          <div
            aria-hidden
            className="absolute start-1/2 top-1/2 size-40 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/20 blur-3xl"
          />
          <svg aria-hidden viewBox={`0 0 ${WIDTH} ${HEIGHT}`} className="absolute inset-0 size-full text-border">
            {SPOKES.map((spoke, index) => {
              const { x, y } = spokePosition(spoke.angle);
              const path = index % 2 === 0 ? `M${x} ${y} L${CX} ${CY}` : `M${CX} ${CY} L${x} ${y}`;

              return (
                <g key={spoke.name}>
                  <line x1={CX} y1={CY} x2={x} y2={y} stroke="currentColor" strokeWidth="0.3" />
                  <circle r="0.9" className="fill-primary motion-reduce:hidden">
                    <animateMotion dur="3s" begin={`-${index * 0.5}s`} repeatCount="indefinite" path={path} />
                  </circle>
                </g>
              );
            })}
          </svg>

          <div className="absolute start-1/2 top-1/2 z-10 flex size-16 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-lg sm:size-20">
            <BrandMark className="size-7 sm:size-9" />
          </div>

          {SPOKES.map((spoke) => {
            const { x, y } = spokePosition(spoke.angle);

            return (
              <a
                key={spoke.name}
                href={spoke.href}
                aria-label={spoke.name}
                title={spoke.name}
                className="group absolute z-10 flex size-11 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-xl border border-border bg-card text-muted-foreground shadow-xs transition-[color,transform,box-shadow] duration-150 ease-out hover:-translate-y-[55%] hover:text-foreground hover:shadow-md focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none motion-reduce:transition-none sm:size-14"
                style={{ left: `${(x / WIDTH) * 100}%`, top: `${(y / HEIGHT) * 100}%` }}
              >
                <spoke.icon aria-hidden className="size-5 sm:size-6" />
              </a>
            );
          })}
        </div>
      </div>

      <div className="mt-10 flex justify-center px-4 sm:mt-14">
        <Button asChild style={stagger(4, 60, 200)} className={cn(ENTER, 'group')}>
          <a href="#">
            Browse integrations
            <ArrowRight className="size-4 transition-transform duration-150 ease-out group-hover:translate-x-0.5 rtl:rotate-180 rtl:group-hover:-translate-x-0.5" />
          </a>
        </Button>
      </div>
    </section>
  );
};

export default Integrations04;
```
