{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "event-1",
  "title": "Event 1: agenda and my schedule",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "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.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/cover-art.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/event-1/event-1.tsx",
      "content": "// Ballmac UI: Event 1. https://ui.ballmac.com/blocks/event-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarArrowDown, CalendarDays, Clock, MapPin, Star, TriangleAlert } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { CoverArt } from \"@/components/ballmac/cover-art\"\nimport { cn } from \"@/lib/utils\"\n\ntype Event1Session = {\n  id: string\n  /** ISO date of the day it happens. */\n  day: string\n  /** \"09:30\", in the event's time zone. */\n  start: string\n  end: string\n  title: string\n  track: string\n  room?: string\n  speakers?: { name: string; role?: string }[]\n  description?: string\n}\n\ntype Event1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  name?: string\n  tagline?: string\n  /** ISO dates of the first and last day. */\n  dates?: [string, string]\n  venue?: string\n  /** Hours from UTC for the venue, so the schedule file has the right times. */\n  utcOffset?: number\n  sessions?: Event1Session[]\n  /** An ISO date for \"now\", used for the countdown. The block never reads the clock. */\n  now?: string\n  locale?: string\n  /** Sessions saved at first. */\n  defaultSaved?: string[]\n  onSavedChange?: (ids: string[]) => void\n}\n\nconst sample: Event1Session[] = [\n  { 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\" }] },\n  { 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.\" },\n  { 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\" }] },\n  { 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\" }] },\n  { 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.\" },\n  { 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\" }] },\n  { 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\" }] },\n  { 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\" }] },\n  { 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\" }] },\n  { 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\" }] },\n]\n\nconst mins = (t: string) => Number(t.slice(0, 2)) * 60 + Number(t.slice(3, 5))\nconst trackTone = [\"bg-chart-1/15\", \"bg-chart-2/15\", \"bg-chart-3/15\", \"bg-chart-4/15\", \"bg-chart-5/15\"]\n\nfunction ics(sessions: Event1Session[], name: string, utcOffset: number) {\n  const stamp = (day: string, t: string) => {\n    const ms = Date.parse(`${day}T${t}:00Z`) - utcOffset * 3_600_000\n    return new Date(ms).toISOString().replace(/[-:]/g, \"\").replace(/\\.\\d{3}/, \"\")\n  }\n  const esc = (s: string) => s.replace(/([,;\\\\])/g, \"\\\\$1\").replace(/\\n/g, \"\\\\n\")\n  const lines = [\"BEGIN:VCALENDAR\", \"VERSION:2.0\", `PRODID:-//${esc(name)}//Schedule//EN`, \"CALSCALE:GREGORIAN\"]\n  for (const s of sessions) {\n    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\")\n  }\n  lines.push(\"END:VCALENDAR\")\n  return lines.join(\"\\r\\n\")\n}\n\n/**\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\n * to build your own schedule, a warning when two of your sessions overlap, and one press to download your schedule as a calendar file.\n */\nfunction Event1({\n  name = \"Acme Summit 2026\",\n  tagline = \"Two days of talks on building software people trust.\",\n  dates = [\"2026-11-12\", \"2026-11-13\"],\n  venue = \"Convento do Beato, Lisbon\",\n  utcOffset = 0,\n  sessions = sample,\n  now = \"2026-10-18T10:00:00Z\",\n  locale = \"en\",\n  defaultSaved = [],\n  onSavedChange,\n  className,\n  ...props\n}: Event1Props) {\n  const id = React.useId()\n  const days = [...new Set(sessions.map((s) => s.day))].sort()\n  const tracks = [...new Set(sessions.map((s) => s.track))]\n  const [day, setDay] = React.useState(days[0] ?? dates[0])\n  const [track, setTrack] = React.useState<string | null>(null)\n  const [saved, setSaved] = React.useState<string[]>(defaultSaved)\n  const [announce, setAnnounce] = React.useState(\"\")\n  const dayFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: \"long\", day: \"numeric\", month: \"long\", timeZone: \"UTC\" }), [locale])\n  const shortFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\", month: \"short\", timeZone: \"UTC\" }), [locale])\n  const rangeFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"long\", year: \"numeric\", timeZone: \"UTC\" }), [locale])\n\n  const until = Math.ceil((Date.parse(`${dates[0]}T00:00:00Z`) - Date.parse(now)) / 86_400_000)\n  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))\n  const mine = sessions.filter((s) => saved.includes(s.id)).sort((a, b) => a.day.localeCompare(b.day) || mins(a.start) - mins(b.start))\n  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))\n\n  const toggle = (s: Event1Session) => {\n    const on = !saved.includes(s.id)\n    const next = on ? [...saved, s.id] : saved.filter((x) => x !== s.id)\n    setSaved(next)\n    onSavedChange?.(next)\n    setAnnounce(`${s.title} ${on ? \"added to\" : \"removed from\"} your schedule`)\n  }\n  const download = () => {\n    const url = URL.createObjectURL(new Blob([ics(mine, name, utcOffset)], { type: \"text/calendar\" }))\n    const a = document.createElement(\"a\")\n    a.href = url\n    a.download = `${name.toLowerCase().replace(/[^a-z0-9]+/g, \"-\")}-schedule.ics`\n    a.click()\n    URL.revokeObjectURL(url)\n  }\n\n  return (\n    <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}>\n      <div className=\"relative isolate overflow-hidden rounded-3xl border\">\n        <CoverArt seed={name} variant=\"mesh\" className=\"absolute inset-0 -z-20 scale-110 saturate-150\" />\n        <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\" />\n        <div className=\"p-6 sm:p-10\">\n          <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\">\n            <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n            {rangeFmt.format(Date.parse(`${dates[0]}T00:00:00Z`))}\n            {dates[1] !== dates[0] ? ` to ${rangeFmt.format(Date.parse(`${dates[1]}T00:00:00Z`))}` : \"\"}\n          </p>\n          <h1 id={`${id}-title`} className=\"mt-5 max-w-2xl text-4xl font-semibold tracking-[-0.045em] text-balance sm:text-6xl\">\n            {name}\n          </h1>\n          <p className=\"text-muted-foreground mt-4 max-w-xl text-lg text-pretty\">{tagline}</p>\n          <div className=\"mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm\">\n            <p className=\"flex items-center gap-2\">\n              <MapPin aria-hidden=\"true\" className=\"size-4\" />\n              {venue}\n            </p>\n            {until > 0 ? (\n              <p className=\"flex items-center gap-2 font-medium tabular-nums\">\n                <Clock aria-hidden=\"true\" className=\"size-4\" />\n                {until} {until === 1 ? \"day\" : \"days\"} to go\n              </p>\n            ) : null}\n          </div>\n        </div>\n      </div>\n\n      <div className=\"mt-10 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_18rem]\">\n        <div className=\"min-w-0\">\n          <div role=\"tablist\" aria-label=\"Day\" className=\"flex gap-2\">\n            {days.map((d) => (\n              <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) => {\n                const i = days.indexOf(d)\n                const n = e.key === \"ArrowRight\" ? days[(i + 1) % days.length] : e.key === \"ArrowLeft\" ? days[(i - 1 + days.length) % days.length] : null\n                if (n) { e.preventDefault(); setDay(n); document.getElementById(`${id}-tab-${n}`)?.focus() }\n              }} 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\")}>\n                <span className=\"block text-xs opacity-70\">Day {days.indexOf(d) + 1}</span>\n                <span className=\"block text-sm font-semibold\">{shortFmt.format(Date.parse(`${d}T00:00:00Z`))}</span>\n              </button>\n            ))}\n          </div>\n\n          <div role=\"group\" aria-label=\"Filter by track\" className=\"mt-5 flex flex-wrap gap-2\">\n            {[null, ...tracks].map((t) => (\n              <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\")}>\n                {t ?? \"All tracks\"}\n              </button>\n            ))}\n          </div>\n\n          <div role=\"tabpanel\" id={`${id}-panel`} aria-labelledby={`${id}-tab-${day}`} className=\"mt-6\">\n            <h2 className=\"mb-4 text-lg font-semibold tracking-tight\">{dayFmt.format(Date.parse(`${day}T00:00:00Z`))}</h2>\n            {shown.length ? (\n              <ol className=\"grid gap-3\">\n                {shown.map((s) => {\n                  const on = saved.includes(s.id)\n                  const c = on ? clash(s) : undefined\n                  return (\n                    <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\")}>\n                      <p className=\"text-sm font-semibold tabular-nums\">\n                        {s.start}\n                        <span className=\"text-muted-foreground font-normal\"> to {s.end}</span>\n                      </p>\n                      <div className=\"min-w-0\">\n                        <h3 className=\"font-semibold tracking-tight text-pretty\">{s.title}</h3>\n                        {s.speakers?.length ? (\n                          <p className=\"text-muted-foreground mt-1 text-sm\">\n                            {s.speakers.map((p) => p.name).join(\", \")}\n                            {s.speakers.length === 1 && s.speakers[0]!.role ? ` · ${s.speakers[0]!.role}` : \"\"}\n                          </p>\n                        ) : null}\n                        {s.description ? <p className=\"text-muted-foreground mt-2 text-sm text-pretty\">{s.description}</p> : null}\n                        <p className=\"mt-3 flex flex-wrap items-center gap-2 text-xs\">\n                          <span className={cn(\"rounded-full border px-2.5 py-0.5 font-medium\", trackTone[tracks.indexOf(s.track) % trackTone.length])}>{s.track}</span>\n                          {s.room ? <span className=\"text-muted-foreground\">{s.room}</span> : null}\n                        </p>\n                        {c ? (\n                          <p role=\"status\" className=\"text-destructive mt-3 flex items-start gap-1.5 text-sm\">\n                            <TriangleAlert aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n                            Overlaps with {c.title} ({c.start} to {c.end})\n                          </p>\n                        ) : null}\n                      </div>\n                      <Button variant={on ? \"secondary\" : \"outline\"} size=\"sm\" aria-pressed={on} onClick={() => toggle(s)} className=\"self-start\">\n                        <Star aria-hidden=\"true\" className={cn(on && \"fill-current\")} />\n                        {on ? \"Saved\" : \"Save\"} <span className=\"sr-only\">{s.title}</span>\n                      </Button>\n                    </li>\n                  )\n                })}\n              </ol>\n            ) : (\n              <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>\n            )}\n          </div>\n        </div>\n\n        <aside aria-labelledby={`${id}-mine`} className=\"bg-card rounded-2xl border p-5 lg:sticky lg:top-6\">\n          <h2 id={`${id}-mine`} className=\"flex items-center gap-2 font-semibold tracking-tight\">\n            <Star aria-hidden=\"true\" className=\"size-4\" />\n            My schedule <span className=\"text-muted-foreground font-normal tabular-nums\">· {mine.length}</span>\n          </h2>\n          {mine.length ? (\n            <>\n              <ul className=\"mt-4 grid gap-3\">\n                {mine.map((s) => (\n                  <li key={s.id} className=\"text-sm\">\n                    <p className=\"text-muted-foreground text-xs tabular-nums\">\n                      {shortFmt.format(Date.parse(`${s.day}T00:00:00Z`))} · {s.start} to {s.end}\n                    </p>\n                    <p className=\"font-medium text-pretty\">{s.title}</p>\n                  </li>\n                ))}\n              </ul>\n              <Button className=\"mt-5 w-full\" onClick={download}>\n                <CalendarArrowDown aria-hidden=\"true\" />\n                Add to my calendar\n              </Button>\n              <p className=\"text-muted-foreground mt-2 text-xs\">Downloads a calendar file with the right times for {venue.split(\",\").pop()?.trim()}.</p>\n            </>\n          ) : (\n            <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>\n          )}\n        </aside>\n      </div>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Event1, type Event1Props, type Event1Session }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/event-1/event-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "event",
      "conference",
      "agenda",
      "schedule",
      "sessions",
      "speakers",
      "tracks",
      "calendar",
      "ics",
      "summit",
      "meetup"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "An event agenda. <Event1 name tagline dates={['2026-11-12', '2026-11-13']} venue utcOffset={1} sessions={[{ id, day: '2026-11-12', start: '09:00', end: '09:45', title, track, room, speakers: [{ name, role }], description }]} now='2026-10-18T10:00:00Z' defaultSaved={['s1']} onSavedChange />. Days and tracks come from the sessions; overlaps among saved sessions are found for you; 'Add to my calendar' builds an .ics file in the browser using utcOffset. Pass now yourself; the block never reads the clock.",
      "whenToUse": [
        "A conference, meetup or webinar series page with a schedule",
        "An internal offsite agenda"
      ],
      "whenNotToUse": [
        "Ticket purchase (use a pricing block)",
        "A month calendar of many events (use calendar-1)"
      ],
      "composesWith": [
        "hero-1",
        "pricing-1",
        "faq-1",
        "footer-1"
      ],
      "a11y": [
        {
          "keys": "Left / Right on the day tabs",
          "action": "Move between days (a tablist); the schedule below is the tab panel"
        },
        {
          "keys": "Tab, Enter / Space",
          "action": "Track buttons say whether they are on; each Save button says which session, and its pressed state"
        },
        {
          "keys": "Overlap",
          "action": "Written as a warning with the clashing session's name and times, not only a color; changes to your schedule are announced politely"
        }
      ],
      "customization": [
        "Sessions at the same time sit side by side in the list, ordered by title",
        "utcOffset sets the time zone of the calendar file",
        "Dates use the locale you pass"
      ]
    },
    "statesNone": "Static content: nothing to load, submit or fail.",
    "examples": [
      "event-1-demo",
      "event-1-saved"
    ],
    "url": "https://ui.ballmac.com/components/event-1"
  },
  "categories": [
    "blocks",
    "event"
  ],
  "type": "registry:block"
}