# Event 1: agenda and my schedule

> An event page with its agenda: dates, venue and a countdown, the schedule by day with a filter per track, a star on each session to build your own schedule, a warning for overlaps, and a calendar file download.

A shadcn/ui block from the [@ballmac](https://ui.ballmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ballmac/event-1).

- Page, with a live preview: https://shadcn.exchange/r/ballmac/event-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/event-1-light-975c67842ac05f33.png
- Registry item: https://ui.ballmac.com/r/event-1.json
- Categories: blocks, event
- Listed: 2026-10-18, source last changed 2026-10-18

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/ballmac/event-1.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: `utils`, `https://ui.ballmac.com/r/button.json`, `https://ui.ballmac.com/r/cover-art.json`

## Files

### `registry/ballmac/components/blocks/event-1/event-1.tsx`

```tsx
// Ballmac UI: Event 1. https://ui.ballmac.com/blocks/event-1
"use client"

import * as React from "react"
import { CalendarArrowDown, CalendarDays, Clock, MapPin, Star, TriangleAlert } from "lucide-react"

import { Button } from "@/components/ballmac/button"
import { CoverArt } from "@/components/ballmac/cover-art"
import { cn } from "@/lib/utils"

type Event1Session = {
  id: string
  /** ISO date of the day it happens. */
  day: string
  /** "09:30", in the event's time zone. */
  start: string
  end: string
  title: string
  track: string
  room?: string
  speakers?: { name: string; role?: string }[]
  description?: string
}

type Event1Props = Omit<React.ComponentProps<"section">, "title"> & {
  name?: string
  tagline?: string
  /** ISO dates of the first and last day. */
  dates?: [string, string]
  venue?: string
  /** Hours from UTC for the venue, so the schedule file has the right times. */
  utcOffset?: number
  sessions?: Event1Session[]
  /** An ISO date for "now", used for the countdown. The block never reads the clock. */
  now?: string
  locale?: string
  /** Sessions saved at first. */
  defaultSaved?: string[]
  onSavedChange?: (ids: string[]) => void
}

const sample: Event1Session[] = [
  { id: "s1", day: "2026-11-12", start: "09:00", end: "09:45", title: "Opening keynote: software that respects people", track: "Keynote", room: "Main hall", speakers: [{ name: "Mara Ellison", role: "CEO, Acme" }] },
  { id: "s2", day: "2026-11-12", start: "10:00", end: "10:45", title: "Design systems at 400 designers", track: "Design", room: "Room A", speakers: [{ name: "Noor Haddad", role: "Head of Design" }], description: "What broke, what held, and the three rules that survived growth." },
  { id: "s3", day: "2026-11-12", start: "10:15", end: "11:00", title: "Postgres in production: lessons from year five", track: "Engineering", room: "Room B", speakers: [{ name: "Priya Shah", role: "Platform lead" }] },
  { id: "s4", day: "2026-11-12", start: "11:15", end: "12:00", title: "Accessible by default", track: "Design", room: "Room A", speakers: [{ name: "Amara Okoye", role: "Product Designer" }, { name: "Jordan Lee", role: "Systems Lead" }] },
  { id: "s5", day: "2026-11-12", start: "13:30", end: "14:15", title: "Shipping with AI agents, safely", track: "Engineering", room: "Main hall", speakers: [{ name: "Theo Nakamura", role: "CTO" }], description: "Approvals, budgets and the logs you will wish you had." },
  { id: "s6", day: "2026-11-12", start: "14:30", end: "15:15", title: "Pricing pages that earn trust", track: "Business", room: "Room C", speakers: [{ name: "Sam Okafor", role: "Head of Support" }] },
  { id: "s7", day: "2026-11-13", start: "09:30", end: "10:15", title: "Remote teams, real friendships", track: "Business", room: "Main hall", speakers: [{ name: "Ana Souza", role: "Operations" }] },
  { id: "s8", day: "2026-11-13", start: "10:30", end: "11:15", title: "Fast front ends without a framework fight", track: "Engineering", room: "Room B", speakers: [{ name: "Yuki Tanaka", role: "Frontend Engineer" }] },
  { id: "s9", day: "2026-11-13", start: "10:30", end: "11:15", title: "Motion that helps, not distracts", track: "Design", room: "Room A", speakers: [{ name: "Jordan Lee", role: "Systems Lead" }] },
  { id: "s10", day: "2026-11-13", start: "12:00", end: "12:45", title: "Closing panel: what we got wrong", track: "Keynote", room: "Main hall", speakers: [{ name: "Mara Ellison" }, { name: "Theo Nakamura" }, { name: "Noor Haddad" }] },
]

const mins = (t: string) => Number(t.slice(0, 2)) * 60 + Number(t.slice(3, 5))
const trackTone = ["bg-chart-1/15", "bg-chart-2/15", "bg-chart-3/15", "bg-chart-4/15", "bg-chart-5/15"]

function ics(sessions: Event1Session[], name: string, utcOffset: number) {
  const stamp = (day: string, t: string) => {
    const ms = Date.parse(`${day}T${t}:00Z`) - utcOffset * 3_600_000
    return new Date(ms).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "")
  }
  const esc = (s: string) => s.replace(/([,;\\])/g, "\\$1").replace(/\n/g, "\\n")
  const lines = ["BEGIN:VCALENDAR", "VERSION:2.0", `PRODID:-//${esc(name)}//Schedule//EN`, "CALSCALE:GREGORIAN"]
  for (const s of sessions) {
    lines.push("BEGIN:VEVENT", `UID:${s.id}@${esc(name).replace(/\s+/g, "-").toLowerCase()}`, `DTSTAMP:${stamp(s.day, s.start)}`, `DTSTART:${stamp(s.day, s.start)}`, `DTEND:${stamp(s.day, s.end)}`, `SUMMARY:${esc(s.title)}`, ...(s.room ? [`LOCATION:${esc(s.room)}`] : []), ...(s.description ? [`DESCRIPTION:${esc(s.description)}`] : []), "END:VEVENT")
  }
  lines.push("END:VCALENDAR")
  return lines.join("\r\n")
}

/**
 * An event page with its agenda: the dates, venue and a countdown, then the schedule by day with a filter for each track, a star on every session
 * to build your own schedule, a warning when two of your sessions overlap, and one press to download your schedule as a calendar file.
 */
function Event1({
  name = "Acme Summit 2026",
  tagline = "Two days of talks on building software people trust.",
  dates = ["2026-11-12", "2026-11-13"],
  venue = "Convento do Beato, Lisbon",
  utcOffset = 0,
  sessions = sample,
  now = "2026-10-18T10:00:00Z",
  locale = "en",
  defaultSaved = [],
  onSavedChange,
  className,
  ...props
}: Event1Props) {
  const id = React.useId()
  const days = [...new Set(sessions.map((s) => s.day))].sort()
  const tracks = [...new Set(sessions.map((s) => s.track))]
  const [day, setDay] = React.useState(days[0] ?? dates[0])
  const [track, setTrack] = React.useState<string | null>(null)
  const [saved, setSaved] = React.useState<string[]>(defaultSaved)
  const [announce, setAnnounce] = React.useState("")
  const dayFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long", timeZone: "UTC" }), [locale])
  const shortFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: "short", day: "numeric", month: "short", timeZone: "UTC" }), [locale])
  const rangeFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" }), [locale])

  const until = Math.ceil((Date.parse(`${dates[0]}T00:00:00Z`) - Date.parse(now)) / 86_400_000)
  const shown = sessions.filter((s) => s.day === day && (!track || s.track === track)).sort((a, b) => mins(a.start) - mins(b.start) || a.title.localeCompare(b.title))
  const mine = sessions.filter((s) => saved.includes(s.id)).sort((a, b) => a.day.localeCompare(b.day) || mins(a.start) - mins(b.start))
  const clash = (s: Event1Session) => mine.find((m) => m.id !== s.id && m.day === s.day && mins(m.start) < mins(s.end) && mins(s.start) < mins(m.end))

  const toggle = (s: Event1Session) => {
    const on = !saved.includes(s.id)
    const next = on ? [...saved, s.id] : saved.filter((x) => x !== s.id)
    setSaved(next)
    onSavedChange?.(next)
    setAnnounce(`${s.title} ${on ? "added to" : "removed from"} your schedule`)
  }
  const download = () => {
    const url = URL.createObjectURL(new Blob([ics(mine, name, utcOffset)], { type: "text/calendar" }))
    const a = document.createElement("a")
    a.href = url
    a.download = `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-schedule.ics`
    a.click()
    URL.revokeObjectURL(url)
  }

  return (
    <section data-slot="event-1" aria-labelledby={`${id}-title`} className={cn("mx-auto w-full max-w-5xl px-4 py-10 sm:px-6", className)} {...props}>
      <div className="relative isolate overflow-hidden rounded-3xl border">
        <CoverArt seed={name} variant="mesh" className="absolute inset-0 -z-20 scale-110 saturate-150" />
        <div aria-hidden="true" className="from-background/90 via-background/70 to-background/30 absolute inset-0 -z-10 bg-gradient-to-r rtl:bg-gradient-to-l" />
        <div className="p-6 sm:p-10">
          <p className="bg-background/70 inline-flex items-center gap-2 rounded-full border px-3.5 py-1.5 text-sm font-medium backdrop-blur">
            <CalendarDays aria-hidden="true" className="size-4" />
            {rangeFmt.format(Date.parse(`${dates[0]}T00:00:00Z`))}
            {dates[1] !== dates[0] ? ` to ${rangeFmt.format(Date.parse(`${dates[1]}T00:00:00Z`))}` : ""}
          </p>
          <h1 id={`${id}-title`} className="mt-5 max-w-2xl text-4xl font-semibold tracking-[-0.045em] text-balance sm:text-6xl">
            {name}
          </h1>
          <p className="text-muted-foreground mt-4 max-w-xl text-lg text-pretty">{tagline}</p>
          <div className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm">
            <p className="flex items-center gap-2">
              <MapPin aria-hidden="true" className="size-4" />
              {venue}
            </p>
            {until > 0 ? (
              <p className="flex items-center gap-2 font-medium tabular-nums">
                <Clock aria-hidden="true" className="size-4" />
                {until} {until === 1 ? "day" : "days"} to go
              </p>
            ) : null}
          </div>
        </div>
      </div>

      <div className="mt-10 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_18rem]">
        <div className="min-w-0">
          <div role="tablist" aria-label="Day" className="flex gap-2">
            {days.map((d) => (
              <button key={d} type="button" role="tab" aria-selected={day === d} id={`${id}-tab-${d}`} aria-controls={`${id}-panel`} tabIndex={day === d ? 0 : -1} onClick={() => setDay(d)} onKeyDown={(e) => {
                const i = days.indexOf(d)
                const n = e.key === "ArrowRight" ? days[(i + 1) % days.length] : e.key === "ArrowLeft" ? days[(i - 1 + days.length) % days.length] : null
                if (n) { e.preventDefault(); setDay(n); document.getElementById(`${id}-tab-${n}`)?.focus() }
              }} className={cn("focus-visible:ring-ring/50 rounded-xl border px-4 py-2.5 text-start outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none", day === d ? "bg-foreground text-background border-foreground" : "hover:bg-accent")}>
                <span className="block text-xs opacity-70">Day {days.indexOf(d) + 1}</span>
                <span className="block text-sm font-semibold">{shortFmt.format(Date.parse(`${d}T00:00:00Z`))}</span>
              </button>
            ))}
          </div>

          <div role="group" aria-label="Filter by track" className="mt-5 flex flex-wrap gap-2">
            {[null, ...tracks].map((t) => (
              <button key={t ?? "all"} type="button" aria-pressed={track === t} onClick={() => setTrack(t)} className={cn("focus-visible:ring-ring/50 rounded-full border px-3.5 py-1.5 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none", track === t ? "bg-foreground text-background border-foreground" : "hover:bg-accent")}>
                {t ?? "All tracks"}
              </button>
            ))}
          </div>

          <div role="tabpanel" id={`${id}-panel`} aria-labelledby={`${id}-tab-${day}`} className="mt-6">
            <h2 className="mb-4 text-lg font-semibold tracking-tight">{dayFmt.format(Date.parse(`${day}T00:00:00Z`))}</h2>
            {shown.length ? (
              <ol className="grid gap-3">
                {shown.map((s) => {
                  const on = saved.includes(s.id)
                  const c = on ? clash(s) : undefined
                  return (
                    <li key={s.id} className={cn("bg-card grid gap-x-5 gap-y-2 rounded-2xl border p-4 transition-colors sm:grid-cols-[6.5rem_minmax(0,1fr)_auto] sm:p-5 motion-reduce:transition-none", on && "border-foreground/40")}>
                      <p className="text-sm font-semibold tabular-nums">
                        {s.start}
                        <span className="text-muted-foreground font-normal"> to {s.end}</span>
                      </p>
                      <div className="min-w-0">
                        <h3 className="font-semibold tracking-tight text-pretty">{s.title}</h3>
                        {s.speakers?.length ? (
                          <p className="text-muted-foreground mt-1 text-sm">
                            {s.speakers.map((p) => p.name).join(", ")}
                            {s.speakers.length === 1 && s.speakers[0]!.role ? ` · ${s.speakers[0]!.role}` : ""}
                          </p>
                        ) : null}
                        {s.description ? <p className="text-muted-foreground mt-2 text-sm text-pretty">{s.description}</p> : null}
                        <p className="mt-3 flex flex-wrap items-center gap-2 text-xs">
                          <span className={cn("rounded-full border px-2.5 py-0.5 font-medium", trackTone[tracks.indexOf(s.track) % trackTone.length])}>{s.track}</span>
                          {s.room ? <span className="text-muted-foreground">{s.room}</span> : null}
                        </p>
                        {c ? (
                          <p role="status" className="text-destructive mt-3 flex items-start gap-1.5 text-sm">
                            <TriangleAlert aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
                            Overlaps with {c.title} ({c.start} to {c.end})
                          </p>
                        ) : null}
                      </div>
                      <Button variant={on ? "secondary" : "outline"} size="sm" aria-pressed={on} onClick={() => toggle(s)} className="self-start">
                        <Star aria-hidden="true" className={cn(on && "fill-current")} />
                        {on ? "Saved" : "Save"} <span className="sr-only">{s.title}</span>
                      </Button>
                    </li>
                  )
                })}
              </ol>
            ) : (
              <p className="text-muted-foreground rounded-2xl border border-dashed px-6 py-12 text-center text-sm">No sessions on this day for that track. Try All tracks.</p>
            )}
          </div>
        </div>

        <aside aria-labelledby={`${id}-mine`} className="bg-card rounded-2xl border p-5 lg:sticky lg:top-6">
          <h2 id={`${id}-mine`} className="flex items-center gap-2 font-semibold tracking-tight">
            <Star aria-hidden="true" className="size-4" />
            My schedule <span className="text-muted-foreground font-normal tabular-nums">· {mine.length}</span>
          </h2>
          {mine.length ? (
            <>
              <ul className="mt-4 grid gap-3">
                {mine.map((s) => (
                  <li key={s.id} className="text-sm">
                    <p className="text-muted-foreground text-xs tabular-nums">
                      {shortFmt.format(Date.parse(`${s.day}T00:00:00Z`))} · {s.start} to {s.end}
                    </p>
                    <p className="font-medium text-pretty">{s.title}</p>
                  </li>
                ))}
              </ul>
              <Button className="mt-5 w-full" onClick={download}>
                <CalendarArrowDown aria-hidden="true" />
                Add to my calendar
              </Button>
              <p className="text-muted-foreground mt-2 text-xs">Downloads a calendar file with the right times for {venue.split(",").pop()?.trim()}.</p>
            </>
          ) : (
            <p className="text-muted-foreground mt-3 text-sm text-pretty">Press Save on a session to build your own schedule. Overlaps are flagged for you.</p>
          )}
        </aside>
      </div>
      <p aria-live="polite" className="sr-only">
        {announce}
      </p>
    </section>
  )
}

export { Event1, type Event1Props, type Event1Session }
```
