{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trip-1",
  "title": "Trip 1: day-by-day itinerary",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A trip itinerary: destination, dates and a countdown, then day by day what is booked and when with copyable booking codes and unbooked items marked in words, a cost breakdown, per-person total, type filters and a packing list with progress.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/cover-art.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/trip-1/trip-1.tsx",
      "content": "// Ballmac UI: Trip 1. https://ui.ballmac.com/blocks/trip-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { BedDouble, CalendarDays, Check, MapPin, Plane, Ticket, TrainFront, Users, Utensils } from \"lucide-react\"\n\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { CoverArt } from \"@/components/ballmac/cover-art\"\nimport { cn } from \"@/lib/utils\"\n\ntype Trip1Kind = \"flight\" | \"stay\" | \"food\" | \"activity\" | \"transport\"\ntype Trip1Item = {\n  id: string\n  kind: Trip1Kind\n  /** ISO date. */\n  date: string\n  /** \"09:30\" in the local time of the place. */\n  time?: string\n  title: string\n  detail?: string\n  /** \"TP 1368\", a booking code to copy. */\n  ref?: string\n  cost?: number\n  /** Not booked yet. */\n  pending?: boolean\n}\n\ntype Trip1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  title?: string\n  destination?: string\n  /** ISO dates of the first and last day. */\n  dates?: [string, string]\n  travelers?: string[]\n  items?: Trip1Item[]\n  packing?: { id: string; label: string; done?: boolean }[]\n  currency?: string\n  locale?: string\n  /** An ISO date for \"today\". The block never reads the clock. */\n  today?: string\n}\n\nconst items0: Trip1Item[] = [\n  { id: \"1\", kind: \"flight\", date: \"2026-11-06\", time: \"08:15\", title: \"London Heathrow to Lisbon\", detail: \"TAP Air Portugal, 2 h 50 min, arrives 11:05\", ref: \"TP1351\", cost: 286 },\n  { id: \"2\", kind: \"transport\", date: \"2026-11-06\", time: \"12:00\", title: \"Metro and tram 28 to Alfama\", detail: \"24-hour pass, 6.80 each\", cost: 13.6 },\n  { id: \"3\", kind: \"stay\", date: \"2026-11-06\", time: \"14:00\", title: \"Check in: Casa do Largo\", detail: \"3 nights, balcony room, breakfast included\", ref: \"BKG-48213\", cost: 612 },\n  { id: \"4\", kind: \"food\", date: \"2026-11-06\", time: \"20:00\", title: \"Dinner at Taberna da Rua das Flores\", detail: \"Table for 2, grilled octopus is the one to order\", cost: 78, pending: true },\n  { id: \"5\", kind: \"activity\", date: \"2026-11-07\", time: \"10:00\", title: \"Belém: tower, monastery and the custard tarts\", detail: \"Tram 15 from Praça da Figueira, 25 minutes\", cost: 36 },\n  { id: \"6\", kind: \"activity\", date: \"2026-11-07\", time: \"17:30\", title: \"Sunset at Miradouro da Senhora do Monte\", detail: \"Bring a layer, it gets windy\", cost: 0 },\n  { id: \"7\", kind: \"food\", date: \"2026-11-07\", time: \"21:00\", title: \"Fado dinner in Alfama\", detail: \"Show starts 21:30\", ref: \"FADO-2209\", cost: 120 },\n  { id: \"8\", kind: \"transport\", date: \"2026-11-08\", time: \"09:10\", title: \"Train to Sintra\", detail: \"From Rossio, 40 minutes, return ticket\", cost: 12.8 },\n  { id: \"9\", kind: \"activity\", date: \"2026-11-08\", time: \"10:30\", title: \"Pena Palace and the gardens\", detail: \"Timed entry, book ahead\", ref: \"PENA-7714\", cost: 64, pending: true },\n  { id: \"10\", kind: \"stay\", date: \"2026-11-09\", time: \"11:00\", title: \"Check out: Casa do Largo\", detail: \"Leave bags at the desk until the evening\" },\n  { id: \"11\", kind: \"flight\", date: \"2026-11-09\", time: \"20:40\", title: \"Lisbon to London Heathrow\", detail: \"TAP Air Portugal, arrives 23:30\", ref: \"TP1372\", cost: 214 },\n]\nconst packing0 = [{ id: \"p1\", label: \"Passports\", done: true }, { id: \"p2\", label: \"Travel insurance printout\", done: true }, { id: \"p3\", label: \"Phone charger and adapter\" }, { id: \"p4\", label: \"Comfortable walking shoes\" }, { id: \"p5\", label: \"Light rain jacket\" }, { id: \"p6\", label: \"Tram and train tickets saved offline\" }]\n\nconst kindMeta: Record<Trip1Kind, { label: string; icon: typeof Plane; tone: string; bar: string }> = {\n  flight: { label: \"Flights\", icon: Plane, tone: \"bg-chart-1/15 border-chart-1/40\", bar: \"bg-chart-1\" },\n  stay: { label: \"Stays\", icon: BedDouble, tone: \"bg-chart-3/15 border-chart-3/40\", bar: \"bg-chart-3\" },\n  food: { label: \"Food\", icon: Utensils, tone: \"bg-chart-4/15 border-chart-4/40\", bar: \"bg-chart-4\" },\n  activity: { label: \"Things to do\", icon: Ticket, tone: \"bg-chart-2/15 border-chart-2/40\", bar: \"bg-chart-2\" },\n  transport: { label: \"Getting around\", icon: TrainFront, tone: \"bg-chart-5/15 border-chart-5/40\", bar: \"bg-chart-5\" },\n}\nconst DAY = 86_400_000\nconst toT = (iso: string) => Date.parse(`${iso}T00:00:00Z`)\n\n/**\n * A trip itinerary: the destination and dates with a countdown, then day by day what is booked and when, each item with its type, time, details and a booking code\n * you can copy, anything not yet booked marked in words. Beside it, what the trip costs by type with a bar, and a packing list with progress. Filters show only flights,\n * stays or food. Nights, days to go, totals and per-person cost are all worked out from the items.\n */\nfunction Trip1({ title = \"Long weekend in Lisbon\", destination = \"Lisbon, Portugal\", dates = [\"2026-11-06\", \"2026-11-09\"], travelers = [\"Mara\", \"Theo\"], items = items0, packing: packingProp = packing0, currency = \"EUR\", locale = \"en-GB\", today = \"2026-10-18\", className, ...props }: Trip1Props) {\n  const id = React.useId()\n  const [kinds, setKinds] = React.useState<Set<Trip1Kind>>(new Set())\n  const [packed, setPacked] = React.useState<Set<string>>(new Set(packingProp.filter((p) => p.done).map((p) => p.id)))\n  const money = (n: number) => new Intl.NumberFormat(locale, { style: \"currency\", currency, maximumFractionDigits: n % 1 ? 2 : 0 }).format(n)\n  const dayFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: \"long\", day: \"numeric\", month: \"long\", timeZone: \"UTC\" }), [locale])\n  const short = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\", timeZone: \"UTC\" }), [locale])\n\n  const nights = Math.round((toT(dates[1]) - toT(dates[0])) / DAY)\n  const until = Math.round((toT(dates[0]) - toT(today)) / DAY)\n  const total = items.reduce((n, i) => n + (i.cost ?? 0), 0)\n  const unbooked = items.filter((i) => i.pending)\n  const byKind = (Object.keys(kindMeta) as Trip1Kind[]).map((k) => ({ k, sum: items.filter((i) => i.kind === k).reduce((n, i) => n + (i.cost ?? 0), 0) })).filter((x) => x.sum > 0)\n  const days = [...new Set(items.map((i) => i.date))].sort()\n  const shown = (d: string) => items.filter((i) => i.date === d && (!kinds.size || kinds.has(i.kind))).sort((a, b) => (a.time ?? \"\").localeCompare(b.time ?? \"\"))\n  const toggleKind = (k: Trip1Kind) => setKinds((s) => { const n = new Set(s); if (n.has(k)) n.delete(k); else n.add(k); return n })\n  const tone = (k: Trip1Kind) => kindMeta[k].bar\n\n  return (\n    <section data-slot=\"trip-1\" aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-10 sm:px-6\", className)} {...props}>\n      <div className=\"relative isolate overflow-hidden rounded-3xl border\">\n        <CoverArt seed={destination} variant=\"waves\" className=\"absolute inset-0 -z-20 scale-110 saturate-150\" />\n        <div aria-hidden=\"true\" className=\"from-background via-background/85 to-background/20 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=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            <MapPin aria-hidden=\"true\" className=\"size-4\" />\n            {destination}\n          </p>\n          <h1 id={`${id}-title`} className=\"mt-3 max-w-xl text-4xl font-semibold tracking-[-0.045em] text-balance sm:text-6xl\">\n            {title}\n          </h1>\n          <ul className=\"mt-6 flex flex-wrap gap-x-7 gap-y-3 text-sm\">\n            <li className=\"flex items-center gap-2\">\n              <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n              {short.format(toT(dates[0]))} to {short.format(toT(dates[1]))} · {nights} {nights === 1 ? \"night\" : \"nights\"}\n            </li>\n            <li className=\"flex items-center gap-2\">\n              <Users aria-hidden=\"true\" className=\"size-4\" />\n              {travelers.join(\" and \")}\n            </li>\n            <li className=\"font-semibold tabular-nums\">{until > 0 ? `${until} ${until === 1 ? \"day\" : \"days\"} to go` : until === 0 ? \"Leaving today\" : \"Under way\"}</li>\n          </ul>\n        </div>\n      </div>\n\n      <div className=\"mt-8 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n        <div className=\"min-w-0\">\n          <div role=\"group\" aria-label=\"Show only\" className=\"flex flex-wrap gap-2\">\n            <button type=\"button\" aria-pressed={!kinds.size} onClick={() => setKinds(new Set())} className={cn(\"focus-visible:ring-ring/50 rounded-full border px-3.5 py-1.5 text-sm font-medium outline-none focus-visible:ring-[3px]\", !kinds.size ? \"bg-foreground text-background border-foreground\" : \"hover:bg-accent\")}>\n              Everything\n            </button>\n            {(Object.keys(kindMeta) as Trip1Kind[]).map((k) => (\n              <button key={k} type=\"button\" aria-pressed={kinds.has(k)} onClick={() => toggleKind(k)} className={cn(\"focus-visible:ring-ring/50 flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-sm font-medium outline-none focus-visible:ring-[3px]\", kinds.has(k) ? \"bg-foreground text-background border-foreground\" : \"hover:bg-accent\")}>\n                {React.createElement(kindMeta[k].icon, { \"aria-hidden\": true, className: \"size-3.5\" })}\n                {kindMeta[k].label}\n              </button>\n            ))}\n          </div>\n          {unbooked.length ? (\n            <p role=\"status\" className=\"mt-4 rounded-xl border border-amber-500/40 bg-amber-500/10 px-4 py-2.5 text-sm\">\n              {unbooked.length} {unbooked.length === 1 ? \"thing is\" : \"things are\"} not booked yet: {unbooked.map((u) => u.title.split(\":\")[0]).join(\"; \")}.\n            </p>\n          ) : null}\n\n          <ol className=\"mt-6 grid gap-8\">\n            {days.map((d, di) => {\n              const list = shown(d)\n              if (!list.length) return null\n              return (\n                <li key={d}>\n                  <h2 className=\"bg-background/90 sticky top-0 z-10 -mx-1 flex items-baseline gap-3 px-1 py-2 text-lg font-semibold tracking-tight backdrop-blur\">\n                    <span className=\"bg-foreground text-background rounded-md px-2 py-0.5 text-xs font-medium tabular-nums\">Day {di + 1}</span>\n                    {dayFmt.format(toT(d))}\n                  </h2>\n                  <ul className=\"mt-3 grid gap-3\">\n                    {list.map((i) => {\n                      const K = kindMeta[i.kind]\n                      return (\n                        <li key={i.id} className=\"bg-card flex gap-4 rounded-2xl border p-4 sm:p-5\">\n                          <span aria-hidden=\"true\" className={cn(\"flex size-11 shrink-0 items-center justify-center rounded-xl border\", K.tone)}>\n                            <K.icon className=\"size-5\" />\n                          </span>\n                          <div className=\"min-w-0 flex-1\">\n                            <p className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1\">\n                              {i.time ? <span className=\"text-sm font-semibold tabular-nums\">{i.time}</span> : null}\n                              <span className=\"font-semibold tracking-tight text-pretty\">{i.title}</span>\n                              {i.pending ? <span className=\"rounded-full border border-amber-500/40 bg-amber-500/15 px-2 py-0.5 text-xs font-medium\">Not booked yet</span> : null}\n                            </p>\n                            {i.detail ? <p className=\"text-muted-foreground mt-1 text-sm text-pretty\">{i.detail}</p> : null}\n                            <div className=\"mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm\">\n                              <span className=\"text-muted-foreground text-xs\">{K.label.replace(\"Things to do\", \"Activity\").replace(\"Flights\", \"Flight\").replace(\"Stays\", \"Stay\").replace(\"Getting around\", \"Transport\")}</span>\n                              {i.cost !== undefined ? <span className=\"tabular-nums\">{i.cost === 0 ? \"Free\" : money(i.cost)}</span> : null}\n                              {i.ref ? (\n                                <span className=\"flex items-center gap-1.5\">\n                                  <span className=\"bg-muted rounded px-1.5 py-0.5 font-mono text-xs\" dir=\"ltr\">\n                                    {i.ref}\n                                  </span>\n                                  <CopyButton value={i.ref} ariaLabel={`Copy booking code ${i.ref}`} copiedLabel=\"Copied\" variant=\"ghost\" />\n                                </span>\n                              ) : null}\n                            </div>\n                          </div>\n                        </li>\n                      )\n                    })}\n                  </ul>\n                </li>\n              )\n            })}\n          </ol>\n        </div>\n\n        <aside className=\"grid gap-5 lg:sticky lg:top-6\">\n          <div className=\"bg-card rounded-3xl border p-5 sm:p-6\">\n            <h2 className=\"font-semibold tracking-tight\">Cost</h2>\n            <p className=\"mt-2 text-4xl font-semibold tracking-[-0.045em] tabular-nums\">{money(total)}</p>\n            <p className=\"text-muted-foreground text-sm tabular-nums\">\n              {money(total / Math.max(1, travelers.length))} each for {travelers.length}\n            </p>\n            <div role=\"img\" aria-label={`Cost by type: ${byKind.map((x) => `${kindMeta[x.k].label} ${money(x.sum)}`).join(\", \")}`} className=\"bg-muted mt-4 flex h-2.5 overflow-hidden rounded-full\">\n              {byKind.map((x) => (\n                <span key={x.k} className={cn(\"h-full\", tone(x.k))} style={{ width: `${(x.sum / total) * 100}%` }} />\n              ))}\n            </div>\n            <ul aria-hidden=\"true\" className=\"mt-4 grid gap-2 text-sm\">\n              {byKind.map((x) => (\n                <li key={x.k} className=\"flex items-center justify-between gap-3\">\n                  <span className=\"flex items-center gap-2\">\n                    <span className={cn(\"size-2.5 rounded-full\", tone(x.k))} />\n                    {kindMeta[x.k].label}\n                  </span>\n                  <span className=\"tabular-nums\">{money(x.sum)}</span>\n                </li>\n              ))}\n            </ul>\n          </div>\n\n          <div className=\"bg-card rounded-3xl border p-5 sm:p-6\">\n            <div className=\"flex items-baseline justify-between gap-3\">\n              <h2 className=\"font-semibold tracking-tight\">Packing</h2>\n              <p className=\"text-muted-foreground text-sm tabular-nums\" aria-live=\"polite\">\n                {packed.size} of {packingProp.length}\n              </p>\n            </div>\n            <div role=\"progressbar\" aria-label=\"Packing progress\" aria-valuemin={0} aria-valuemax={packingProp.length} aria-valuenow={packed.size} className=\"bg-muted mt-3 h-2 overflow-hidden rounded-full\">\n              <span className=\"bg-foreground block h-full rounded-full transition-[width] duration-500 motion-reduce:transition-none\" style={{ width: `${packingProp.length ? (packed.size / packingProp.length) * 100 : 0}%` }} />\n            </div>\n            <ul className=\"mt-4 grid gap-1\">\n              {packingProp.map((p) => (\n                <li key={p.id}>\n                  <label className=\"hover:bg-accent/50 -mx-2 flex cursor-pointer items-center gap-3 rounded-lg px-2 py-2 text-sm\">\n                    <input type=\"checkbox\" checked={packed.has(p.id)} onChange={() => setPacked((s) => { const n = new Set(s); if (n.has(p.id)) n.delete(p.id); else n.add(p.id); return n })} className=\"accent-foreground size-4\" />\n                    <span className={cn(packed.has(p.id) && \"text-muted-foreground line-through\")}>{p.label}</span>\n                    {packed.has(p.id) ? <Check aria-hidden=\"true\" className=\"text-chart-2 ms-auto size-4\" /> : null}\n                  </label>\n                </li>\n              ))}\n            </ul>\n          </div>\n        </aside>\n      </div>\n    </section>\n  )\n}\n\nexport { Trip1, type Trip1Props, type Trip1Item, type Trip1Kind }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/trip-1/trip-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "trip",
      "itinerary",
      "travel",
      "vacation",
      "holiday",
      "plan",
      "flights",
      "hotel",
      "packing",
      "booking"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A trip itinerary. <Trip1 title destination dates={['2026-11-06', '2026-11-09']} travelers={['Mara', 'Theo']} items={[{ id, kind: 'flight' | 'stay' | 'food' | 'activity' | 'transport', date, time: '08:15', title, detail, ref: 'TP1351', cost, pending }]} packing={[{ id, label, done }]} currency locale today='2026-10-18' />. Nights, days to go, the total, the per-person cost and the cost by type are worked out from the items; a pending item says Not booked yet and is listed in a notice. Pass today yourself; the block never reads the clock.",
      "whenToUse": [
        "A shared trip plan or a confirmation page for a booked trip",
        "Travel apps and agency itineraries"
      ],
      "whenNotToUse": [
        "Booking flights or hotels (use a booking flow)",
        "A single event agenda (use event-1)"
      ],
      "composesWith": [
        "event-1",
        "locations-1",
        "invoice-1",
        "header-1"
      ],
      "a11y": [
        {
          "keys": "Tab, Space",
          "action": "Filters say whether they are on; each packing item is a checkbox and the count (2 of 6) and a progress bar update politely; each booking code has a Copy button naming it"
        },
        {
          "keys": "Days",
          "action": "Each day is a heading ('Day 2 Saturday 7 November') in an ordered list, with items in time order"
        },
        {
          "keys": "Booked or not",
          "action": "'Not booked yet' is written on the item and summarised in a status notice, not only colored"
        }
      ],
      "customization": [
        "kind picks the icon and tint",
        "Booking codes and times are shown left to right in right-to-left layouts, like codes",
        "Dates, weekdays and money follow the locale and currency"
      ]
    },
    "statesNone": "Static content with its own filters and checklist: nothing to load, submit or fail.",
    "examples": [
      "trip-1-demo",
      "trip-1-underway"
    ],
    "url": "https://ui.ballmac.com/components/trip-1"
  },
  "categories": [
    "blocks",
    "trip"
  ],
  "type": "registry:block"
}