# Feature 08

> Six-card feature grid on hairline rails: each card carries an inline icon beside its title, a title and a description, corner crosshair marks, and a radial spotlight that follows the pointer on hover. Warm-tinted top glow per card.

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

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

## Install

```bash
npx shadcn@latest add https://hirael.com/r/feature-08.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: `badge`

## Files

### `registry/hirael/bases/radix/blocks/feature-08/feature-08.tsx`

```tsx
// Feature 08 from Hirael <https://hirael.com/blocks/features/feature-08>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import type * as React from 'react';
import { Activity, FileCode2, KeyRound, Terminal, Workflow, Zap } from 'lucide-react';

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

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`,
});

const FEATURES = [
  {
    icon: Workflow,
    title: 'A canvas, not a config file',
    description:
      'Drag steps onto a graph, wire dependencies by hand, and let the layout show fan-out and gates for you.',
  },
  {
    icon: FileCode2,
    title: 'Two-way sync',
    description: 'Edit the graph or the file, the other follows. The config in your repo stays the source of truth.',
  },
  {
    icon: Activity,
    title: 'Runs you can watch',
    description:
      'Every run replays on the same graph. The failing step turns red where it sits, with its logs one click away.',
  },
  {
    icon: KeyRound,
    title: 'Secrets per environment',
    description: 'Scope credentials to an environment, not a pipeline. Staging never sees production keys.',
  },
  {
    icon: Zap,
    title: 'Retries that target the step',
    description: 'Rerun a failed step in place with its cache intact instead of paying for the whole pipeline again.',
  },
  {
    icon: Terminal,
    title: 'API and CLI',
    description: 'Trigger runs, read statuses and export graphs from scripts. Everything the UI does has an endpoint.',
  },
];

type CrossDecorPosition = 'top-start' | 'bottom-end';

interface CrossDecorProps {
  position: CrossDecorPosition;
}

const CrossDecor = ({ position }: CrossDecorProps) => {
  return (
    <svg
      aria-hidden
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1"
      strokeLinecap="round"
      className={cn(
        'pointer-events-none absolute z-10 size-3.5 shrink-0 text-muted-foreground',
        position === 'top-start' && 'start-0 top-0 -translate-y-1/2 ltr:-translate-x-1/2 rtl:translate-x-1/2',
        position === 'bottom-end' && 'end-0 bottom-0 translate-y-1/2 ltr:translate-x-1/2 rtl:-translate-x-1/2',
      )}
    >
      <path d="M5 12h14" />
      <path d="M12 5v14" />
    </svg>
  );
};

const FeatureCard = ({ className, children, ...props }: React.ComponentProps<'div'>) => {
  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    e.currentTarget.style.setProperty('--mx', `${e.clientX - rect.left}px`);
    e.currentTarget.style.setProperty('--my', `${e.clientY - rect.top}px`);
  };

  return (
    <div
      onPointerMove={handlePointerMove}
      data-slot="feature-card"
      className={cn(
        'group relative flex h-full flex-col justify-start gap-6 bg-background px-6 pt-8 pb-6 shadow-xs',
        'bg-[radial-gradient(50%_80%_at_25%_0%,color-mix(in_oklch,var(--primary)_20%,transparent),transparent)]',
        className,
      )}
      {...props}
    >
      {/* Pointer-follow spotlight, fades in on hover along --mx/--my. */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_200px_at_var(--mx)_var(--my),color-mix(in_oklch,var(--primary)_14%,transparent),transparent_70%)] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
      />

      <div className="absolute -inset-y-4 -start-px w-px bg-border" />
      <div className="absolute -inset-y-4 -end-px w-px bg-border" />
      <div className="absolute -inset-x-4 -top-px h-px bg-border" />
      <div className="absolute -inset-x-4 -bottom-px h-px bg-border" />

      {children}
    </div>
  );
};

const Feature08 = () => {
  return (
    <section
      data-slot="feature"
      className="flex w-full flex-col justify-center gap-12 bg-background px-6 py-16 md:px-10 md:py-24"
    >
      <div data-slot="feature-header" className="mx-auto flex max-w-3xl flex-col items-center gap-4 text-center">
        <Badge variant="outline" className={ENTER}>
          Features
        </Badge>
        <h2
          style={stagger(1)}
          className={cn(ENTER, 'font-serif text-3xl font-medium tracking-tight md:text-4xl lg:text-5xl')}
        >
          Everything a pipeline needs
        </h2>
        <p style={stagger(2)} className={cn(ENTER, 'text-base leading-relaxed text-muted-foreground md:text-lg')}>
          The editor is the headline, but the platform underneath carries the boring parts: secrets, caching, retries
          and an API for the rest.
        </p>
      </div>

      <div className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
        {FEATURES.map((feature, index) => (
          <FeatureCard key={feature.title} style={stagger(index, 60, 180)} className={ENTER}>
            <CrossDecor position="top-start" />
            <CrossDecor position="bottom-end" />
            <div className="relative z-10 flex flex-col gap-2">
              <h3 className="flex items-center gap-2.5 text-base font-medium text-foreground">
                <feature.icon
                  aria-hidden
                  data-slot="feature-card-icon"
                  className="size-4 shrink-0 stroke-[1.5] text-muted-foreground transition-colors duration-150 group-hover:text-primary"
                />
                {feature.title}
              </h3>
              <p className="text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
            </div>
          </FeatureCard>
        ))}
      </div>
    </section>
  );
};

export default Feature08;
```
