Skip to content
ilinxa/pro-ui

Registration Card

alphav0.2.0

Event registration status card — capacity progress, spots-left counter, status-aware call to action, and a share slot.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

Context

Use for event registration sidebars, course / webinar signup cards, training enrollment, conference tickets, beta / waitlist signups, fundraising goal cards. Architecturally independent of event timing — host owns date logic and passes `closed: true` when registration window has ended. Migration origin: kasder kas-social-front-v0 events/[id]/page.tsx Kayıt Durumu sidebar card. Composes naturally with event-card helpers + share-bar in the actions slot.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/registration-card

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/registration-card-fixtures

Preview

Kayıt Durumu

Kontenjan77 yer kaldı
423 kayıtlı500 kapasite

Demo source

demo.tsxtsx

Usage

When to use

Reach for RegistrationCard for any signup / RSVP / ticket / fundraising widget — event registration sidebars, course / webinar enrollment, training signups, conference tickets, beta / waitlist, fundraising goal cards. Capacity progress + spots-left counter + status-aware primary CTA + optional share / actions slot.

Minimal example

import { RegistrationCard } from "@/components/registration-card";

<RegistrationCard
  heading="Registration"
  capacity={500}
  registered={142}
  onRegister={() => openRegisterDialog()}
  onShare={() => share(eventUrl)}
/>;

4-state state machine

Status auto-derives from (capacity, registered, closed):

  • open — capacity available, less than lastSpotsRatiofull → primary "Register" CTA
  • lastSpots — capacity available, ≥ lastSpotsRatiofull (default 80%) → primary "Register" CTA + spots-left counter flips to text-destructive when ≤ urgentSpotsCount (default 10)
  • fullregistered >= capacity→ disabled secondary "Sold out" CTA
  • closed — host-driven via closed: true→ disabled secondary "Closed" CTA. Wins over capacity (closed always closes regardless of remaining spots).

Public helper kernel — derive state without rendering

import {
  RegistrationCard,
  deriveRegistrationStatus,
  type RegistrationStatus,
} from "@/components/registration-card";

// Header counter: how many events are still open?
const stillOpen = events.filter(
  (e) =>
    deriveRegistrationStatus({
      capacity: e.capacity,
      registered: e.registered,
      closed: e.regClosed,
    }) !== "full" &&
    deriveRegistrationStatus({
      capacity: e.capacity,
      registered: e.registered,
      closed: e.regClosed,
    }) !== "closed",
).length;

Composing with event-card's state

registration-card is architecturally independent of event timing — the host derives event status (using getEventStatus from event-card) and passes closed: true when registration should close. Two common composition patterns:

import { getEventStatus } from "@/components/event-card";

// Pattern 1 — close registration only when event has expired:
<RegistrationCard
  capacity={event.capacity}
  registered={event.registered}
  closed={getEventStatus(event) === "expired"}
  onRegister={handleRegister}
/>

// Pattern 2 — close registration when event is expired OR ongoing
// (i.e., no walk-ins after the event starts):
<RegistrationCard
  capacity={event.capacity}
  registered={event.registered}
  closed={["expired", "ongoing"].includes(getEventStatus(event))}
  onRegister={handleRegister}
/>

No-quota mode

For unlimited / drop-in events, omit capacity and registered — the bar + counter rows hide; status stays open; CTA + share remain interactive.

<RegistrationCard
  heading="Sign up"
  onRegister={handleRegister}
  onShare={handleShare}
/>

Custom actions slot

The actions slot fully replaces the default share button. Drop in calendar export + save event + share clusters, or compose with share-bar:

<RegistrationCard
  capacity={200}
  registered={172}
  onRegister={handleRegister}
  actions={
    <div className="grid grid-cols-3 gap-2">
      <Button variant="outline" onClick={addToCalendar}>Cal</Button>
      <Button variant="outline" onClick={save}>Save</Button>
      <ShareBar url={eventUrl} variant="compact" />
    </div>
  }
/>

Relabeled for fundraising

<RegistrationCard
  heading="Fundraising Goal"
  capacity={50000}
  registered={32000}
  onRegister={openDonateDialog}
  labels={{
    capacityLabel: "Goal",
    spotsLeftSuffix: "remaining",
    spotsLeftFull: "Goal reached!",
    registeredSuffix: "raised",
    capacitySuffix: "goal",
    ctaRegister: "Donate now",
    ctaSoldOut: "Goal reached",
  }}
/>

Configurable thresholds

  • lastSpotsRatio (default 0.8) — percent of capacity (0–1) at which status flips to lastSpots. Drives the state machine.
  • urgentSpotsCount (default 10) — absolute spots-left count at which the counter color flips to text-destructive. Drives the visual color, not the state.

These are semantically distinct: lastSpotsRatio is percent (good for state), urgentSpotsCount is absolute (good for color urgency). For a 100-capacity event, 80% means 20 spots left — not yet urgent. For a 50-capacity event, 80% means 10 spots left — urgent.

Notes

  • The primary CTA is a REAL interactive button — fires onRegister() when clicked. (Differs from event-card's grid variant which has a decorative CTA.)
  • When onRegister is absent, the CTA renders disabled with labels.ctaUnavailable — useful for preview / admin contexts.
  • The bar uses Radix Progress primitive — emits role="progressbar" + aria-valuenow automatically.
  • Heading defaults to h3(this is a sidebar card, typically nested under the page's main h2).
  • statusOverride wins over derived status — useful for preview / what-if states in admin UIs.

Features

  • 4-state state machine — open / lastSpots / full / closed
  • Public helper kernel — deriveRegistrationStatus pure function
  • Capacity progress bar + spots-left counter with destructive-color threshold
  • Status-aware primary CTA (Register / Sold out / Closed / Unavailable)
  • Default share button when onShare provided
  • actions slot fully replaces the default share for richer clusters
  • No-quota mode (capacity-less events) hides bar + counter rows
  • Configurable thresholds — lastSpotsRatio (state machine) + urgentSpotsCount (counter color)
  • statusOverride for preview / what-if states
  • Optional heading with configurable level (h2/h3/h4, default h3)
  • Frame toggle — framed=true card chrome with shadow-lg / framed=false bare
  • i18n via 11-key labels object
  • WCAG — Radix Progress role=progressbar + aria-valuenow + section aria-labelledby
  • Architecturally INDEPENDENT of event-card's date-driven state machine (host composes)

Tags

registration-cardregistrationsignuprsvpevents

Dependencies

shadcn primitives: progress
npm peer deps: lucide-react@^1.11.0