Cart delivery estimate

Arrival range in business days and a real warehouse cutoff countdown, computed after mount in the warehouse time zone.

componentetaanyshippingdelivery
$npx shadcn@latest add @storekit/cart-delivery-eta
cart-delivery-eta

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/cart-delivery-eta
$pnpm dlx shadcn@latest add @storekit/cart-delivery-eta
$yarn dlx shadcn@latest add @storekit/cart-delivery-eta
$bunx --bun shadcn@latest add @storekit/cart-delivery-eta

Source

components/storekit/cart/cart-delivery-eta.tsx
144 lines
"use client"

import * as React from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { Clock, Truck } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Delivery estimate for the cart. Works out the real dispatch day from a
   warehouse cutoff in the warehouse time zone, then adds business days for
   transit to give an arrival range. Everything is computed after mount, so
   the server renders a plain policy line and there is no hydration drift.
   After the cutoff it names the next dispatch day instead of resetting. */

const DAY = 86_400_000

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")) }
}

/** Calendar offset (days) after adding `n` business days to a start weekday. */
function addBusinessDays(startDay: number, n: number) {
  let offset = 0
  let added = 0
  while (added < n) {
    offset++
    const wd = (startDay + offset) % 7
    if (wd !== 0 && wd !== 6) added++
  }
  return offset
}

export function CartDeliveryEta({
  cutoffHour = 14,
  timeZone = "America/New_York",
  zoneLabel = "ET",
  dispatchDays = [1, 2, 3, 4, 5],
  minDays = 2,
  maxDays = 4,
  method = "Standard shipping",
  className,
}: {
  /** Warehouse cutoff hour, 24h, in `timeZone`. */
  cutoffHour?: number
  timeZone?: string
  zoneLabel?: string
  /** Weekdays the warehouse ships, 0 = Sunday. */
  dispatchDays?: number[]
  /** Transit time in business days. */
  minDays?: number
  maxDays?: number
  method?: string
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("cart", "cart-delivery-eta")
  const [now, setNow] = React.useState<Date | null>(null)

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

  const hourLabel = `${cutoffHour % 12 === 0 ? 12 : cutoffHour % 12}${cutoffHour < 12 ? "am" : "pm"} ${zoneLabel}`

  let info: null | { today: boolean; left: string; shipDay: string; from: string; to: string } = null
  if (now && dispatchDays.length) {
    const { day, secs } = clockIn(timeZone, now)
    const cutoff = cutoffHour * 3600
    let offset = 0
    const today = dispatchDays.includes(day) && secs < cutoff
    if (!today) {
      offset = 1
      while (!dispatchDays.includes((day + offset) % 7) && offset < 8) offset++
    }
    const shipWd = (day + offset) % 7
    const fmt = (o: number, opts: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat("en-US", { timeZone, ...opts }).format(new Date(now.getTime() + o * DAY))
    const short: Intl.DateTimeFormatOptions = { weekday: "short", month: "short", day: "numeric" }
    const s = Math.max(0, cutoff - secs)
    const h = Math.floor(s / 3600)
    const m = Math.floor((s % 3600) / 60)
    info = {
      today,
      left: h ? `${h}h ${m}m` : `${m}m`,
      shipDay: offset === 1 ? "tomorrow" : fmt(offset, { weekday: "long" }),
      from: fmt(offset + addBusinessDays(shipWd, minDays), short),
      to: fmt(offset + addBusinessDays(shipWd, maxDays), short),
    }
  }

  return (
    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn("w-full rounded-sk-card border border-sk-line bg-sk-card p-4", className)}>
      <div className="flex items-start gap-3">
        <span className="grid h-9 w-9 shrink-0 place-items-center rounded-sk-chip bg-sk-ink/[0.06] text-sk-ink">
          <Truck size={16} />
        </span>
        <div className="min-w-0 flex-1">
          <p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">{method}</p>
          <AnimatePresence mode="wait" initial={false}>
            <motion.p
              key={info ? "on" : "off"}
              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0 }}
              transition={{ duration: reduce ? 0 : 0.35, ease: [0.16, 1, 0.3, 1] }}
              className="mt-1 text-[15px] font-medium text-sk-ink"
            >
              {info ? (
                <>
                  Arrives <span className="whitespace-nowrap tabular-nums">{info.from}</span> to <span className="whitespace-nowrap tabular-nums">{info.to}</span>
                </>
              ) : (
                <>
                  Arrives in <span className="tabular-nums">{minDays}</span> to <span className="tabular-nums">{maxDays}</span> business days
                </>
              )}
            </motion.p>
          </AnimatePresence>
        </div>
      </div>
      <p className="mt-3 flex items-center gap-2 border-t border-sk-line pt-3 text-[13px] text-sk-muted" aria-live="off">
        <Clock size={14} className="shrink-0" />
        {info ? (
          info.today ? (
            <span>
              Order in the next <span className="font-medium tabular-nums text-sk-ink">{info.left}</span> and it ships today.
            </span>
          ) : (
            <span>
              Today&apos;s {hourLabel} cutoff has passed. Ships <span className="font-medium text-sk-ink">{info.shipDay}</span>.
            </span>
          )
        ) : (
          <span>Orders placed before {hourLabel} on business days ship the same day.</span>
        )}
      </p>
    </div>
  )
}

Usage

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

Cart delivery estimate, StoreKit