Dispatch cutoff

A countdown to the real warehouse cutoff in its own time zone that names the next dispatch day after it passes.

componentdispatch-cutoffanyshipping
$npx shadcn@latest add @storekit/upsell-dispatch-cutoff
upsell-dispatch-cutoff

Live and interactive. Switch the brand in the frame header to see the same source under another token file.

Installation

$npx shadcn@latest add @storekit/upsell-dispatch-cutoff
$pnpm dlx shadcn@latest add @storekit/upsell-dispatch-cutoff
$yarn dlx shadcn@latest add @storekit/upsell-dispatch-cutoff
$bunx --bun shadcn@latest add @storekit/upsell-dispatch-cutoff

Source

components/storekit/upsell/upsell-dispatch-cutoff.tsx
156 lines
"use client"

import { useEffect, useState } from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { PackageCheck, Truck } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* PDP dispatch cutoff. A countdown to a real warehouse cutoff in the
   warehouse's own time zone, not the shopper's. After the cutoff, and on
   weekends, it switches to naming the next dispatch day instead of
   resetting a timer. Nothing is computed on the server: it renders a static
   policy line and starts ticking after mount, so there is no hydration
   drift and no fake clock. */

const EASE = [0.16, 1, 0.3, 1] as const
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

function clockIn(timeZone: string, now: Date) {
  const parts = new Intl.DateTimeFormat("en-US", { timeZone, weekday: "short", hour: "numeric", minute: "numeric", second: "numeric", hourCycle: "h23" }).formatToParts(now)
  const get = (t: string) => parts.find((p) => p.type === t)?.value ?? "0"
  const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(get("weekday"))
  return { day, secs: Number(get("hour")) * 3600 + Number(get("minute")) * 60 + Number(get("second")) }
}

function Digits({ value, reduce }: { value: string; reduce: boolean | null }) {
  return (
    <span className="inline-flex">
      {value.split("").map((ch, i) => (
        <span key={i} className="relative inline-block h-[1.3em] w-[1ch] overflow-hidden align-bottom">
          <AnimatePresence initial={false} mode="popLayout">
            <motion.span
              key={ch}
              initial={reduce ? { opacity: 0 } : { y: 8, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={reduce ? { opacity: 0 } : { y: -8, opacity: 0 }}
              transition={{ duration: 0.35, ease: EASE }}
              className="absolute inset-0 text-center"
            >
              {ch}
            </motion.span>
          </AnimatePresence>
        </span>
      ))}
    </span>
  )
}

export function UpsellDispatchCutoff({
  cutoffHour = 14,
  timeZone = "America/New_York",
  zoneLabel = "ET",
  dispatchDays = [1, 2, 3, 4, 5],
  showSeconds = true,
  className,
}: {
  /** Cutoff hour in the warehouse time zone, 24h. */
  cutoffHour?: number
  /** IANA zone of the warehouse. */
  timeZone?: string
  zoneLabel?: string
  /** Days the warehouse dispatches, 0 = Sunday. */
  dispatchDays?: number[]
  showSeconds?: boolean
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("upsell", "upsell-dispatch-cutoff")
  const [now, setNow] = useState<Date | null>(null)

  useEffect(() => {
    setNow(new Date())
    const id = window.setInterval(() => setNow(new Date()), 1000)
    return () => window.clearInterval(id)
  }, [])

  const hourLabel = `${cutoffHour % 12 === 0 ? 12 : cutoffHour % 12}${cutoffHour < 12 ? "am" : "pm"} ${zoneLabel}`
  const pad = (n: number) => String(n).padStart(2, "0")

  let state: { kind: "idle" } | { kind: "today"; h: number; m: number; s: number } | { kind: "next"; day: string } = { kind: "idle" }
  if (now) {
    const { day, secs } = clockIn(timeZone, now)
    const left = cutoffHour * 3600 - secs
    if (dispatchDays.includes(day) && left > 0) {
      state = { kind: "today", h: Math.floor(left / 3600), m: Math.floor((left % 3600) / 60), s: left % 60 }
    } else {
      let next = day
      for (let i = 1; i <= 7; i++) {
        next = (day + i) % 7
        if (dispatchDays.includes(next)) break
      }
      state = { kind: "next", day: next === (day + 1) % 7 ? "tomorrow" : DAYS[next] }
    }
  }

  return (
    <div
      {...sk.attrs}
      ref={sk.ref as React.RefObject<HTMLDivElement>}
      role="status"
      aria-live="off"
      className={cn("flex w-full items-start gap-3 rounded-sk-card border border-sk-line bg-sk-card px-4 py-3", className)}
    >
      <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")}>
        {state.kind === "today" ? <Truck size={15} aria-hidden /> : <PackageCheck size={15} aria-hidden />}
      </span>
      <div className="flex min-w-0 flex-col gap-0.5">
        <AnimatePresence mode="wait" initial={false}>
          <motion.p
            key={state.kind}
            initial={{ opacity: 0, y: reduce ? 0 : 6 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: reduce ? 0 : -6 }}
            transition={{ duration: reduce ? 0 : 0.3, ease: EASE }}
            className="text-sm leading-[1.5] text-sk-ink"
          >
            {state.kind === "today" ? (
              <>
                Order within{" "}
                <span className="font-semibold tabular-nums">
                  {state.h > 0 ? (
                    <>
                      <Digits value={String(state.h)} reduce={reduce} />h{" "}
                    </>
                  ) : null}
                  <Digits value={pad(state.m)} reduce={reduce} />m
                  {showSeconds ? (
                    <>
                      {" "}
                      <Digits value={pad(state.s)} reduce={reduce} />s
                    </>
                  ) : null}
                </span>{" "}
                to ship <span className="font-semibold text-sk-savings">today</span>
              </>
            ) : state.kind === "next" ? (
              <>
                Order now to ship <span className="font-semibold">{state.day}</span>
              </>
            ) : (
              <>Same-day dispatch on orders before {hourLabel}</>
            )}
          </motion.p>
        </AnimatePresence>
        <p className="text-[12px] text-sk-muted">
          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.
        </p>
      </div>
      {/* Screen readers get one stable sentence instead of a ticking clock. */}
      <span className="sr-only">
        {state.kind === "next" ? `Ships ${state.day}.` : `Orders before ${hourLabel} on dispatch days ship the same day.`}
      </span>
    </div>
  )
}

Usage

Import from "@/components/storekit/upsell/upsell-dispatch-cutoff" and pass your product data. The demo above exercises the full prop surface with sample values.

Dispatch cutoff, StoreKit