{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-dispatch-cutoff",
  "type": "registry:component",
  "title": "Dispatch cutoff",
  "description": "A countdown to the real warehouse cutoff in its own time zone that names the next dispatch day after it passes.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "upsell/upsell-dispatch-cutoff.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { PackageCheck, Truck } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* PDP dispatch cutoff. A countdown to a real warehouse cutoff in the\n   warehouse's own time zone, not the shopper's. After the cutoff, and on\n   weekends, it switches to naming the next dispatch day instead of\n   resetting a timer. Nothing is computed on the server: it renders a static\n   policy line and starts ticking after mount, so there is no hydration\n   drift and no fake clock. */\n\nconst EASE = [0.16, 1, 0.3, 1] as const\nconst DAYS = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"]\n\nfunction clockIn(timeZone: string, now: Date) {\n  const parts = new Intl.DateTimeFormat(\"en-US\", { timeZone, weekday: \"short\", hour: \"numeric\", minute: \"numeric\", second: \"numeric\", hourCycle: \"h23\" }).formatToParts(now)\n  const get = (t: string) => parts.find((p) => p.type === t)?.value ?? \"0\"\n  const day = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"].indexOf(get(\"weekday\"))\n  return { day, secs: Number(get(\"hour\")) * 3600 + Number(get(\"minute\")) * 60 + Number(get(\"second\")) }\n}\n\nfunction Digits({ value, reduce }: { value: string; reduce: boolean | null }) {\n  return (\n    <span className=\"inline-flex\">\n      {value.split(\"\").map((ch, i) => (\n        <span key={i} className=\"relative inline-block h-[1.3em] w-[1ch] overflow-hidden align-bottom\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={ch}\n              initial={reduce ? { opacity: 0 } : { y: 8, opacity: 0 }}\n              animate={{ y: 0, opacity: 1 }}\n              exit={reduce ? { opacity: 0 } : { y: -8, opacity: 0 }}\n              transition={{ duration: 0.35, ease: EASE }}\n              className=\"absolute inset-0 text-center\"\n            >\n              {ch}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ))}\n    </span>\n  )\n}\n\nexport function UpsellDispatchCutoff({\n  cutoffHour = 14,\n  timeZone = \"America/New_York\",\n  zoneLabel = \"ET\",\n  dispatchDays = [1, 2, 3, 4, 5],\n  showSeconds = true,\n  className,\n}: {\n  /** Cutoff hour in the warehouse time zone, 24h. */\n  cutoffHour?: number\n  /** IANA zone of the warehouse. */\n  timeZone?: string\n  zoneLabel?: string\n  /** Days the warehouse dispatches, 0 = Sunday. */\n  dispatchDays?: number[]\n  showSeconds?: boolean\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"upsell\", \"upsell-dispatch-cutoff\")\n  const [now, setNow] = useState<Date | null>(null)\n\n  useEffect(() => {\n    setNow(new Date())\n    const id = window.setInterval(() => setNow(new Date()), 1000)\n    return () => window.clearInterval(id)\n  }, [])\n\n  const hourLabel = `${cutoffHour % 12 === 0 ? 12 : cutoffHour % 12}${cutoffHour < 12 ? \"am\" : \"pm\"} ${zoneLabel}`\n  const pad = (n: number) => String(n).padStart(2, \"0\")\n\n  let state: { kind: \"idle\" } | { kind: \"today\"; h: number; m: number; s: number } | { kind: \"next\"; day: string } = { kind: \"idle\" }\n  if (now) {\n    const { day, secs } = clockIn(timeZone, now)\n    const left = cutoffHour * 3600 - secs\n    if (dispatchDays.includes(day) && left > 0) {\n      state = { kind: \"today\", h: Math.floor(left / 3600), m: Math.floor((left % 3600) / 60), s: left % 60 }\n    } else {\n      let next = day\n      for (let i = 1; i <= 7; i++) {\n        next = (day + i) % 7\n        if (dispatchDays.includes(next)) break\n      }\n      state = { kind: \"next\", day: next === (day + 1) % 7 ? \"tomorrow\" : DAYS[next] }\n    }\n  }\n\n  return (\n    <div\n      {...sk.attrs}\n      ref={sk.ref as React.RefObject<HTMLDivElement>}\n      role=\"status\"\n      aria-live=\"off\"\n      className={cn(\"flex w-full items-start gap-3 rounded-sk-card border border-sk-line bg-sk-card px-4 py-3\", className)}\n    >\n      <span className={cn(\"mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full transition-colors duration-300\", state.kind === \"today\" ? \"bg-sk-savings/[0.12] text-sk-savings\" : \"bg-sk-surface text-sk-muted\")}>\n        {state.kind === \"today\" ? <Truck size={15} aria-hidden /> : <PackageCheck size={15} aria-hidden />}\n      </span>\n      <div className=\"flex min-w-0 flex-col gap-0.5\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.p\n            key={state.kind}\n            initial={{ opacity: 0, y: reduce ? 0 : 6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: reduce ? 0 : -6 }}\n            transition={{ duration: reduce ? 0 : 0.3, ease: EASE }}\n            className=\"text-sm leading-[1.5] text-sk-ink\"\n          >\n            {state.kind === \"today\" ? (\n              <>\n                Order within{\" \"}\n                <span className=\"font-semibold tabular-nums\">\n                  {state.h > 0 ? (\n                    <>\n                      <Digits value={String(state.h)} reduce={reduce} />h{\" \"}\n                    </>\n                  ) : null}\n                  <Digits value={pad(state.m)} reduce={reduce} />m\n                  {showSeconds ? (\n                    <>\n                      {\" \"}\n                      <Digits value={pad(state.s)} reduce={reduce} />s\n                    </>\n                  ) : null}\n                </span>{\" \"}\n                to ship <span className=\"font-semibold text-sk-savings\">today</span>\n              </>\n            ) : state.kind === \"next\" ? (\n              <>\n                Order now to ship <span className=\"font-semibold\">{state.day}</span>\n              </>\n            ) : (\n              <>Same-day dispatch on orders before {hourLabel}</>\n            )}\n          </motion.p>\n        </AnimatePresence>\n        <p className=\"text-[12px] text-sk-muted\">\n          Cutoff {hourLabel}, {dispatchDays.length === 5 && [1, 2, 3, 4, 5].every((d) => dispatchDays.includes(d)) ? \"Monday to Friday\" : dispatchDays.map((d) => DAYS[d].slice(0, 3)).join(\", \")}. Tracking sent on dispatch.\n        </p>\n      </div>\n      {/* Screen readers get one stable sentence instead of a ticking clock. */}\n      <span className=\"sr-only\">\n        {state.kind === \"next\" ? `Ships ${state.day}.` : `Orders before ${hourLabel} on dispatch days ship the same day.`}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/upsell/upsell-dispatch-cutoff.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "dispatch-cutoff",
    "niche": [
      "any"
    ],
    "commerce": [
      "shipping"
    ],
    "previewHeight": 240
  }
}