# Pricing 04

> Three-tier pricing row with a monthly/yearly toggle, an uppercase plan label, a large price with billing note, a tagline, a full-width CTA and a check-marked quota list; the featured tier gets a primary border, a top radial glow and a most-popular badge.

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

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

## Install

```bash
npx shadcn@latest add https://hirael.com/r/pricing-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: `badge`, `button`, `toggle-group`

## Files

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

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

'use client';

import * as React from 'react';
import { Check } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Badge } from '@/registry/hirael/bases/radix/ui/badge';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { ToggleGroup, ToggleGroupItem } from '@/registry/hirael/bases/radix/ui/toggle-group';

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 SWAP =
  'animate-in fade-in slide-in-from-bottom-1 duration-250 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`,
});

type Billing = 'monthly' | 'yearly';

const TIERS = [
  {
    name: 'Free',
    price: { monthly: 0, yearly: 0 },
    tagline: 'For side projects and trying the editor.',
    cta: 'Start free',
    featured: false,
    quota: ['2 concurrent runs', '500 run minutes a month', '7 days of run history', 'Unlimited collaborators'],
  },
  {
    name: 'Team',
    price: { monthly: 12, yearly: 10 },
    tagline: 'For teams shipping every day.',
    cta: 'Start with Team',
    featured: true,
    quota: ['10 concurrent runs', '3,000 run minutes a month', '90 days of run history', 'Per-environment secrets'],
  },
  {
    name: 'Scale',
    price: { monthly: 29, yearly: 24 },
    tagline: 'For orgs with many repos and gates.',
    cta: 'Start with Scale',
    featured: false,
    quota: ['Unlimited concurrent runs', '10,000 run minutes a month', '1 year of run history', 'Audit log and SSO'],
  },
];

const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });

const Pricing04 = () => {
  const [billing, setBilling] = React.useState<Billing>('monthly');

  return (
    <section data-slot="pricing" className="flex w-full flex-col gap-12 bg-background px-6 py-16 md:px-10 md:py-24">
      <div data-slot="pricing-header" className="mx-auto flex max-w-3xl flex-col items-center gap-4 text-center">
        <Badge variant="outline" className={ENTER}>
          Pricing
        </Badge>
        <h2
          style={stagger(1)}
          className={cn(ENTER, 'font-serif text-3xl font-medium tracking-tight md:text-4xl lg:text-5xl')}
        >
          Priced by the minute, not the seat
        </h2>
        <p style={stagger(2)} className={cn(ENTER, 'text-base leading-relaxed text-muted-foreground md:text-lg')}>
          Every plan includes the full editor. You pay for compute and retention, and collaborators are free everywhere.
        </p>
        <ToggleGroup
          data-slot="pricing-billing"
          type="single"
          variant="outline"
          value={billing}
          onValueChange={(next) => next && setBilling(next as Billing)}
          aria-label="Billing period"
          style={stagger(3)}
          className={cn(ENTER, 'mt-4')}
        >
          <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
          <ToggleGroupItem value="yearly">Yearly</ToggleGroupItem>
        </ToggleGroup>
      </div>

      <div className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-4 md:grid-cols-3">
        {TIERS.map((tier, index) => {
          const price = tier.price[billing];

          return (
            <div
              key={tier.name}
              data-slot="pricing-tier"
              data-featured={tier.featured}
              style={stagger(index, 60, 240)}
              className={cn(
                ENTER,
                'relative isolate flex h-full flex-col gap-6 overflow-hidden rounded-xs border border-border bg-background p-7',
                tier.featured && 'border-primary/60',
              )}
            >
              {tier.featured ? (
                <>
                  <div
                    aria-hidden
                    className="absolute inset-0 -z-10 bg-[radial-gradient(120%_90%_at_50%_0%,color-mix(in_oklch,var(--primary)_20%,transparent),transparent_70%)]"
                  />
                  <Badge className="absolute end-5 top-5">Most popular</Badge>
                </>
              ) : null}

              <div className="flex flex-col gap-2">
                <h3 className="text-xs text-muted-foreground uppercase">{tier.name}</h3>
                <div key={billing} data-slot="pricing-price" className={cn(SWAP, 'flex flex-col gap-1')}>
                  <div className="flex items-baseline gap-1">
                    <span dir="ltr" className="text-5xl font-semibold tracking-tight tabular-nums">
                      {money.format(price)}
                    </span>
                    <span className="text-sm text-muted-foreground">/ month</span>
                  </div>
                  <span className="text-xs text-muted-foreground">
                    {price === 0 ? (
                      'No card needed'
                    ) : billing === 'yearly' ? (
                      <>
                        <span dir="ltr" className="tabular-nums">
                          {money.format(price * 12)}
                        </span>{' '}
                        billed once a year
                      </>
                    ) : (
                      'Billed monthly'
                    )}
                  </span>
                </div>
                <p className="text-sm text-muted-foreground">{tier.tagline}</p>
              </div>

              <Button variant={tier.featured ? 'default' : 'outline'} className="w-full" asChild>
                <a href="#">{tier.cta}</a>
              </Button>

              <ul className="flex flex-col gap-2.5 text-sm">
                {tier.quota.map((line) => (
                  <li key={line} className="flex items-start gap-2">
                    <Check aria-hidden className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
                    <span>{line}</span>
                  </li>
                ))}
              </ul>
            </div>
          );
        })}
      </div>
    </section>
  );
};

export default Pricing04;
```
