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-etacart-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-etaSource
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'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.