{
  "name": "boarding-pass",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Boarding Pass\n * @version: 1.0.0\n * @date: 2026-10-22\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { Plane } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/** Radius of the notches on either side of the tear line, in px. */\nconst NOTCH = 10;\n\nexport type FlightStatus = \"on-time\" | \"delayed\" | \"gate-change\" | \"boarding\";\n\nexport interface Flight {\n  airline: string;\n  number: string;\n  from: { code: string; city: string; time: string };\n  to: { code: string; city: string; time: string };\n  date: string;\n  gate: string;\n  boards: string;\n  seat: string;\n  group: string;\n  passenger: string;\n  status: FlightStatus;\n  /** One line under the status, like \"Delayed 25 min\". */\n  note?: string;\n}\n\nexport interface BoardingPassProps {\n  /** Leave out to watch a demo flight get a gate change and a delay. */\n  flight?: Flight;\n  className?: string;\n}\n\nconst BASE: Flight = {\n  airline: \"Northwind\",\n  number: \"NW 214\",\n  from: { code: \"LIS\", city: \"Lisbon\", time: \"10:40\" },\n  to: { code: \"CPH\", city: \"Copenhagen\", time: \"15:05\" },\n  date: \"Thu, 12 Nov\",\n  gate: \"B12\",\n  boards: \"10:05\",\n  seat: \"14A\",\n  group: \"2\",\n  passenger: \"Maya Chen\",\n  status: \"on-time\",\n};\n\nconst DEMO: Flight[] = [\n  BASE,\n  {\n    ...BASE,\n    gate: \"B31\",\n    status: \"gate-change\",\n    note: \"Gate moved to B31, 6 min walk\",\n  },\n  {\n    ...BASE,\n    gate: \"B31\",\n    boards: \"10:30\",\n    from: { ...BASE.from, time: \"11:05\" },\n    to: { ...BASE.to, time: \"15:30\" },\n    status: \"delayed\",\n    note: \"Delayed 25 min, late inbound aircraft\",\n  },\n  {\n    ...BASE,\n    gate: \"B31\",\n    boards: \"10:30\",\n    from: { ...BASE.from, time: \"11:05\" },\n    to: { ...BASE.to, time: \"15:30\" },\n    status: \"boarding\",\n    note: \"Group 2 is boarding now\",\n  },\n];\n\nconst STATUS: Record<FlightStatus, { label: string; tone: string }> = {\n  \"on-time\": {\n    label: \"On time\",\n    tone: \"bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400\",\n  },\n  \"gate-change\": {\n    label: \"Gate change\",\n    tone: \"bg-orange-50 text-orange-700 dark:bg-orange-500/10 dark:text-orange-400\",\n  },\n  delayed: {\n    label: \"Delayed\",\n    tone: \"bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400\",\n  },\n  boarding: {\n    label: \"Boarding\",\n    tone: \"bg-sky-50 text-sky-700 dark:bg-sky-500/10 dark:text-sky-400\",\n  },\n};\n\n/** Remembers the value before the last change, so it can sit struck under the new one. */\nfunction usePrevious(value: string) {\n  const [state, setState] = useState({\n    now: value,\n    was: null as string | null,\n  });\n  if (state.now !== value) {\n    setState({ now: value, was: state.now });\n  }\n  return state.was;\n}\n\n/**\n * A value where only the characters that changed roll over, one after\n * another, like a departures board updating one slot.\n */\nfunction Flap({\n  value,\n  className,\n  reduceMotion,\n}: {\n  value: string;\n  className?: string;\n  reduceMotion: boolean;\n}) {\n  const was = usePrevious(value);\n  let changed = 0;\n  return (\n    <span className={cn(\"inline-flex\", className)}>\n      <span className=\"sr-only\">{value}</span>\n      {[...value].map((char, i) => {\n        const moved = was !== null && was[i] !== char;\n        const order = moved ? changed++ : 0;\n        return (\n          <span\n            aria-hidden=\"true\"\n            className=\"relative inline-block overflow-hidden\"\n            key={`${i.toString()}-${value.length}`}\n          >\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                animate={{ y: \"0%\", opacity: 1 }}\n                className=\"inline-block whitespace-pre\"\n                exit={{ y: \"100%\", opacity: 0 }}\n                initial={{ y: \"-100%\", opacity: 0 }}\n                key={char}\n                transition={\n                  reduceMotion\n                    ? { duration: 0 }\n                    : { duration: 0.32, delay: order * 0.06, ease: EASE_OUT }\n                }\n              >\n                {char}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        );\n      })}\n    </span>\n  );\n}\n\nfunction Field({\n  label,\n  value,\n  previous,\n  reduceMotion,\n  large,\n}: {\n  label: string;\n  value: string;\n  previous: string;\n  reduceMotion: boolean;\n  large?: boolean;\n}) {\n  const changed = previous !== value;\n  return (\n    <div className=\"min-w-0\">\n      <p className=\"text-[11px] text-zinc-500 dark:text-zinc-400\">{label}</p>\n      <Flap\n        className={cn(\n          \"font-semibold text-zinc-900 tabular-nums tracking-tight transition-colors duration-300 ease-out dark:text-zinc-50\",\n          large ? \"text-xl\" : \"text-base\",\n          changed && \"text-orange-600 dark:text-orange-400\"\n        )}\n        reduceMotion={reduceMotion}\n        value={value}\n      />\n      <div className=\"mt-0.5 h-4\">\n        <AnimatePresence initial={false}>\n          {changed && (\n            <motion.p\n              animate={{ opacity: 1, y: 0 }}\n              className=\"text-[11px] text-zinc-400 tabular-nums line-through decoration-zinc-400/70 dark:text-zinc-500\"\n              exit={{ opacity: 0 }}\n              initial={{ opacity: 0, y: -4 }}\n              transition={{\n                duration: reduceMotion ? 0 : 0.24,\n                delay: reduceMotion ? 0 : 0.25,\n                ease: EASE_OUT,\n              }}\n            >\n              {previous}\n            </motion.p>\n          )}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n\n/** A barcode drawn from the passenger and flight, so each pass gets its own. */\nfunction Barcode({ seed }: { seed: string }) {\n  let n = 7;\n  for (const c of seed) {\n    n = (n * 31 + c.charCodeAt(0)) % 2_147_483_647;\n  }\n  const bars: { x: number; w: number }[] = [];\n  let x = 0;\n  while (x < 200) {\n    n = (n * 16_807) % 2_147_483_647;\n    const w = 1 + (n % 3);\n    n = (n * 16_807) % 2_147_483_647;\n    const gap = 1 + (n % 3);\n    bars.push({ x, w });\n    x += w + gap;\n  }\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"h-10 w-full text-zinc-900 dark:text-zinc-100\"\n      preserveAspectRatio=\"none\"\n      viewBox=\"0 0 200 40\"\n    >\n      {bars.map((b) => (\n        <rect fill=\"currentColor\" height=\"40\" key={b.x} width={b.w} x={b.x} />\n      ))}\n    </svg>\n  );\n}\n\n/**\n * A boarding pass that keeps itself current. When the gate or times change,\n * only the characters that moved roll over, the new value turns orange, and\n * the old one stays underneath, struck through, so you can see what changed.\n */\nexport default function BoardingPass({ flight, className }: BoardingPassProps) {\n  const [step, setStep] = useState(0);\n  const reduceMotion = Boolean(useReducedMotion());\n  const current = flight ?? DEMO[step];\n  // What the pass said when it was issued: the baseline for \"changed\".\n  const [issued] = useState(current);\n  const cardRef = useRef<HTMLDivElement>(null);\n  const tearRef = useRef<HTMLDivElement>(null);\n  const [tearY, setTearY] = useState(0);\n\n  useEffect(() => {\n    if (flight) {\n      return;\n    }\n    const id = window.setInterval(\n      () => setStep((s) => (s + 1) % DEMO.length),\n      3200\n    );\n    return () => window.clearInterval(id);\n  }, [flight]);\n\n  useLayoutEffect(() => {\n    const measure = () => {\n      if (tearRef.current) {\n        setTearY(tearRef.current.offsetTop);\n      }\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    if (cardRef.current) {\n      observer.observe(cardRef.current);\n    }\n    return () => observer.disconnect();\n  }, []);\n\n  const status = STATUS[current.status];\n  const fade = { duration: reduceMotion ? 0 : 0.22, ease: EASE_OUT };\n  const notches = `radial-gradient(circle ${NOTCH}px at 0 ${tearY}px, #0000 97%, #000 100%), radial-gradient(circle ${NOTCH}px at 100% ${tearY}px, #0000 97%, #000 100%)`;\n\n  return (\n    <div className={cn(\"relative w-full max-w-sm\", className)}>\n      <div\n        className=\"rounded-3xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950\"\n        ref={cardRef}\n        style={{\n          maskImage: notches,\n          WebkitMaskImage: notches,\n          maskComposite: \"intersect\",\n          WebkitMaskComposite: \"source-in\",\n        }}\n      >\n        <div className=\"p-5\">\n          <div className=\"flex items-center justify-between\">\n            <p className=\"flex items-center gap-2 font-medium text-sm text-zinc-900 dark:text-zinc-100\">\n              <span className=\"flex size-6 items-center justify-center rounded-full bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900\">\n                <Plane aria-hidden className=\"size-3.5 -rotate-45\" />\n              </span>\n              {current.airline}\n              <span className=\"font-normal text-zinc-400 dark:text-zinc-500\">\n                {current.number}\n              </span>\n            </p>\n            <span\n              className={cn(\n                \"relative inline-flex h-6 items-center overflow-hidden rounded-full px-2.5 font-medium text-[11px] transition-colors duration-300\",\n                status.tone\n              )}\n            >\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: -8 }}\n                  initial={{ opacity: 0, y: 8 }}\n                  key={status.label}\n                  transition={fade}\n                >\n                  {status.label}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          </div>\n\n          <div className=\"mt-5 flex items-end justify-between gap-3\">\n            <div className=\"min-w-0\">\n              <p className=\"font-semibold text-4xl text-zinc-900 tracking-tight dark:text-zinc-50\">\n                {current.from.code}\n              </p>\n              <p className=\"truncate text-xs text-zinc-500 dark:text-zinc-400\">\n                {current.from.city} ·{\" \"}\n                <Flap\n                  className={cn(\n                    \"tabular-nums transition-colors duration-300 ease-out\",\n                    current.from.time === issued.from.time\n                      ? \"text-zinc-700 dark:text-zinc-300\"\n                      : \"text-orange-600 dark:text-orange-400\"\n                  )}\n                  reduceMotion={reduceMotion}\n                  value={current.from.time}\n                />\n              </p>\n            </div>\n            <div className=\"mb-5 flex flex-1 items-center gap-1.5 text-zinc-300 dark:text-zinc-700\">\n              <span className=\"h-px flex-1 border-current border-t border-dashed\" />\n              <Plane\n                aria-hidden\n                className=\"size-3.5 rotate-45 text-zinc-400 dark:text-zinc-500\"\n              />\n              <span className=\"h-px flex-1 border-current border-t border-dashed\" />\n            </div>\n            <div className=\"min-w-0 text-right\">\n              <p className=\"font-semibold text-4xl text-zinc-900 tracking-tight dark:text-zinc-50\">\n                {current.to.code}\n              </p>\n              <p className=\"truncate text-xs text-zinc-500 dark:text-zinc-400\">\n                <Flap\n                  className={cn(\n                    \"tabular-nums transition-colors duration-300 ease-out\",\n                    current.to.time === issued.to.time\n                      ? \"text-zinc-700 dark:text-zinc-300\"\n                      : \"text-orange-600 dark:text-orange-400\"\n                  )}\n                  reduceMotion={reduceMotion}\n                  value={current.to.time}\n                />{\" \"}\n                · {current.to.city}\n              </p>\n            </div>\n          </div>\n\n          <div className=\"mt-5 grid grid-cols-4 gap-3\">\n            <Field\n              label=\"Gate\"\n              large\n              previous={issued.gate}\n              reduceMotion={reduceMotion}\n              value={current.gate}\n            />\n            <Field\n              label=\"Boards\"\n              large\n              previous={issued.boards}\n              reduceMotion={reduceMotion}\n              value={current.boards}\n            />\n            <Field\n              label=\"Seat\"\n              large\n              previous={issued.seat}\n              reduceMotion={reduceMotion}\n              value={current.seat}\n            />\n            <Field\n              label=\"Group\"\n              large\n              previous={issued.group}\n              reduceMotion={reduceMotion}\n              value={current.group}\n            />\n          </div>\n\n          <div className=\"relative mt-2 h-4 text-xs\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.p\n                animate={{ opacity: 1, y: 0 }}\n                className=\"truncate text-zinc-500 dark:text-zinc-400\"\n                exit={{ opacity: 0, y: -4 }}\n                initial={{ opacity: 0, y: 4 }}\n                key={current.note ?? current.date}\n                transition={fade}\n              >\n                {current.note ??\n                  `${current.date} · Doors close 15 min before departure`}\n              </motion.p>\n            </AnimatePresence>\n          </div>\n        </div>\n\n        {/* The tear line, between the notches. */}\n        <div className=\"relative mx-4\" ref={tearRef}>\n          <div className=\"border-zinc-200 border-t-2 border-dashed dark:border-zinc-800\" />\n        </div>\n\n        <div className=\"flex items-center gap-4 p-5\">\n          <div className=\"min-w-0 flex-1\">\n            <Barcode seed={`${current.passenger}${current.number}`} />\n          </div>\n          <div className=\"shrink-0 text-right leading-tight\">\n            <p className=\"text-[11px] text-zinc-500 dark:text-zinc-400\">\n              Passenger\n            </p>\n            <p className=\"font-medium text-sm text-zinc-900 dark:text-zinc-100\">\n              {current.passenger}\n            </p>\n            <p className=\"mt-1 text-[11px] text-zinc-400 tabular-nums dark:text-zinc-500\">\n              {current.date}\n            </p>\n          </div>\n        </div>\n      </div>\n\n      {/* Outlines for the notches, which the mask cut out of the border. */}\n      {tearY > 0 &&\n        [0, 1].map((side) => (\n          <svg\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute text-zinc-200 dark:text-zinc-800\"\n            height={NOTCH * 2 + 2}\n            key={side}\n            style={{\n              top: tearY - NOTCH - 1,\n              [side ? \"right\" : \"left\"]: -1,\n            }}\n            viewBox={`0 0 ${NOTCH + 1} ${NOTCH * 2 + 2}`}\n            width={NOTCH + 1}\n          >\n            <path\n              d={\n                side\n                  ? `M ${NOTCH + 1} 1 A ${NOTCH} ${NOTCH} 0 0 0 ${NOTCH + 1} ${NOTCH * 2 + 1}`\n                  : `M 0 1 A ${NOTCH} ${NOTCH} 0 0 1 0 ${NOTCH * 2 + 1}`\n              }\n              fill=\"none\"\n              stroke=\"currentColor\"\n            />\n          </svg>\n        ))}\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/boarding-pass.tsx",
      "target": "components/kokonutui/boarding-pass.tsx"
    }
  ]
}