{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"store-hours","type":"registry:component","title":"Store hours","description":"Draws the week's hours on a 24-hour scale and can redraw them in your own time zone.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"components/store-hours.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useId, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\n// \"HH:MM\" in the store's own time. A close earlier than the open runs past\n// midnight into the next day, like a bar open 17:00 to 02:00.\nexport type Shift = [open: string, close: string];\n// Monday first, seven entries. An empty list is a closed day.\nexport type WeekHours = Shift[][];\n\nconst DAY = 1440;\nconst WEEK = DAY * 7;\nconst DAYS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst LONG_DAYS = [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"];\n// Closing within the hour turns the pill into a warning, and its ring\n// drains over those last 60 minutes.\nconst SOON = 60;\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n\nconst mod = (n: number, m: number) => ((n % m) + m) % m;\nconst toMinutes = (hhmm: string) => {\n  const [h, m] = hhmm.split(\":\").map(Number);\n  return h * 60 + m;\n};\n\n// Wall-clock parts of an instant in a zone.\nfunction wall(ms: number, timeZone: string) {\n  const parts = new Intl.DateTimeFormat(\"en-US\", {\n    timeZone,\n    weekday: \"short\",\n    year: \"numeric\",\n    month: \"numeric\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"numeric\",\n    hourCycle: \"h23\",\n  }).formatToParts(ms);\n  const get = (type: string) => parts.find((p) => p.type === type)?.value ?? \"\";\n  const n = (type: string) => Number(get(type));\n  return {\n    weekMinute: DAYS.indexOf(get(\"weekday\")) * DAY + n(\"hour\") * 60 + n(\"minute\"),\n    // Minutes since the epoch as if this wall clock were UTC, for offsets.\n    stamp: Date.UTC(n(\"year\"), n(\"month\") - 1, n(\"day\"), n(\"hour\"), n(\"minute\")) / 60000,\n  };\n}\n\n// Minutes the visitor's clock is ahead of the store's.\nfunction offsetBetween(ms: number, store: string, visitor: string) {\n  return wall(ms, visitor).stamp - wall(ms, store).stamp;\n}\n\ntype Interval = { start: number; end: number };\n\nfunction intervals(hours: WeekHours): Interval[] {\n  return hours.flatMap((shifts, day) =>\n    shifts.map(([open, close]) => {\n      const start = day * DAY + toMinutes(open);\n      let end = day * DAY + toMinutes(close);\n      if (end <= start) end += DAY;\n      return { start, end };\n    }),\n  );\n}\n\ntype Segment = { from: number; to: number; carried: boolean };\n\n// Splits the week's intervals, shifted into the zone being shown, into\n// per-day bars. A shift that runs past midnight leaves a carried-over bar\n// at the start of the next day's row.\nfunction rows(list: Interval[], shift: number) {\n  const out = DAYS.map(() => ({ segments: [] as Segment[], opens: [] as Interval[] }));\n  for (const { start, end } of list) {\n    const s = mod(start + shift, WEEK);\n    const e = s + (end - start);\n    out[Math.floor(s / DAY)].opens.push({ start: s % DAY, end: e - Math.floor(s / DAY) * DAY });\n    for (let t = Math.floor(s / DAY) * DAY; t < e; t += DAY) {\n      const from = Math.max(s, t);\n      const to = Math.min(e, t + DAY);\n      out[mod(t / DAY, 7)].segments.push({ from: from - t, to: to - t, carried: from > s });\n    }\n  }\n  return out;\n}\n\nfunction clock(minutes: number, compact = false) {\n  const m = mod(minutes, DAY);\n  const h = Math.floor(m / 60);\n  const min = m % 60;\n  const h12 = h % 12 || 12;\n  const mm = min ? `:${String(min).padStart(2, \"0\")}` : \"\";\n  if (compact) return `${h12}${mm}${h < 12 ? \"a\" : \"p\"}`;\n  return `${h12}${mm} ${h < 12 ? \"AM\" : \"PM\"}`;\n}\n\ntype Status =\n  | { kind: \"open\"; closesAt: number; left: number }\n  | { kind: \"closed\"; opensAt: number; until: number };\n\nfunction status(list: Interval[], t: number): Status | null {\n  for (const { start, end } of list) {\n    for (const base of [t, t + WEEK]) {\n      if (base >= start && base < end) {\n        return { kind: \"open\", closesAt: end, left: end - base };\n      }\n    }\n  }\n  let best: Status | null = null;\n  for (const { start } of list) {\n    const until = mod(start - t, WEEK);\n    if (!best || until < (best as { until: number }).until) {\n      best = { kind: \"closed\", opensAt: start, until };\n    }\n  }\n  return best;\n}\n\n// Ticks every 15 seconds; the snapshot only changes when the quarter\n// minute does, so React skips the renders in between. On the server it is\n// null, so nothing time-dependent renders until hydration is done.\nconst QUANTUM = 15_000;\nfunction subscribeClock(onChange: () => void) {\n  const id = window.setInterval(onChange, QUANTUM);\n  document.addEventListener(\"visibilitychange\", onChange);\n  return () => {\n    window.clearInterval(id);\n    document.removeEventListener(\"visibilitychange\", onChange);\n  };\n}\nexport function useNow() {\n  return useSyncExternalStore(\n    subscribeClock,\n    () => Math.floor(Date.now() / QUANTUM) * QUANTUM,\n    () => null,\n  );\n}\nconst noop = () => () => {};\nfunction useVisitorZone() {\n  return useSyncExternalStore(\n    noop,\n    () => Intl.DateTimeFormat().resolvedOptions().timeZone,\n    () => null,\n  );\n}\n\nexport function StoreHours({\n  name,\n  address,\n  hours,\n  timeZone,\n  city,\n  now: nowOverride,\n  visitorTimeZone,\n  className,\n}: {\n  name: string;\n  address: string;\n  hours: WeekHours;\n  // The store's IANA zone.\n  timeZone: string;\n  // Short place name for the store's clock, like \"Brooklyn\".\n  city: string;\n  // Pins the clock, for previews; live when left out.\n  now?: number;\n  // Defaults to the browser's zone.\n  visitorTimeZone?: string;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion();\n  const id = useId();\n  const live = useNow();\n  const detected = useVisitorZone();\n  const now = nowOverride ?? live;\n  const visitor = visitorTimeZone ?? detected;\n  const [mode, setMode] = useState<\"store\" | \"visitor\">(\"store\");\n\n  const list = useMemo(() => intervals(hours), [hours]);\n  const ready = now !== null && visitor !== null;\n  const offset = ready ? offsetBetween(now, timeZone, visitor) : 0;\n  const differs = ready && offset !== 0;\n  const shift = differs && mode === \"visitor\" ? offset : 0;\n  const week = useMemo(() => rows(list, shift), [list, shift]);\n\n  const storeMinute = ready ? wall(now, timeZone).weekMinute : 0;\n  const shownMinute = mod(storeMinute + shift, WEEK);\n  const today = ready ? Math.floor(shownMinute / DAY) : -1;\n  const nowPct = ((shownMinute % DAY) / DAY) * 100;\n  const state = ready ? status(list, storeMinute) : null;\n\n  // Times in the status line follow whichever clock the chart shows.\n  const pillText = !state\n    ? \"Checking hours\"\n    : state.kind === \"open\"\n      ? state.left <= SOON\n        ? `Open, closes in ${state.left} min`\n        : `Open until ${clock(state.closesAt + shift)}`\n      : state.until <= SOON\n        ? `Closed, opens in ${state.until} min`\n        : `Closed, opens ${openingDay(storeMinute + shift, state.opensAt + shift)}${clock(state.opensAt + shift)}`;\n  const tone = !state\n    ? \"idle\"\n    : state.kind === \"open\"\n      ? state.left <= SOON\n        ? \"soon\"\n        : \"open\"\n      : \"closed\";\n\n  const zoneName = (zone: string) =>\n    new Intl.DateTimeFormat(\"en-US\", { timeZone: zone, timeZoneName: \"short\" })\n      .formatToParts(now ?? 0)\n      .find((p) => p.type === \"timeZoneName\")?.value ?? zone;\n\n  return (\n    <section\n      aria-labelledby={`${id}-name`}\n      className={cn(\n        \"w-[min(540px,100%)] rounded-3xl bg-background p-5 shadow-raised\",\n        className,\n      )}\n    >\n      {/* The clock switch wraps under the name on a phone rather than\n          truncating the address. */}\n      <div className=\"flex flex-wrap items-start justify-between gap-x-3 gap-y-3\">\n        <div className=\"min-w-[180px] flex-1\">\n          <h3 id={`${id}-name`} className=\"text-[15px] font-medium text-foreground\">\n            {name}\n          </h3>\n          <p className=\"truncate text-[13px] text-muted-foreground\">{address}</p>\n        </div>\n        {differs && (\n          <div\n            role=\"radiogroup\"\n            aria-label=\"Show hours in\"\n            className=\"flex shrink-0 rounded-full bg-muted p-1\"\n          >\n            {([\"store\", \"visitor\"] as const).map((m) => (\n              <button\n                key={m}\n                type=\"button\"\n                role=\"radio\"\n                aria-checked={mode === m}\n                onClick={() => setMode(m)}\n                className=\"relative h-8 touch-manipulation rounded-full px-3 text-[13px] font-medium outline-hidden transition-[scale] duration-150 ease-out select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-foreground active:scale-[0.96]\"\n              >\n                {mode === m && (\n                  <motion.span\n                    layoutId={`${id}-mode`}\n                    transition={reduceMotion ? { duration: 0 } : { type: \"spring\", duration: 0.3, bounce: 0 }}\n                    className=\"absolute inset-0 rounded-full bg-background shadow-raised\"\n                  />\n                )}\n                <span\n                  className={cn(\n                    \"relative transition-[color] duration-150 ease-out\",\n                    mode === m ? \"text-foreground\" : \"text-muted-foreground\",\n                  )}\n                >\n                  {m === \"store\" ? \"Store time\" : \"My time\"}\n                </span>\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n\n      <div className=\"mt-4 flex h-8 items-center\">\n        <span\n          className={cn(\n            \"inline-flex h-8 items-center gap-2 rounded-full pr-3.5 pl-2.5 text-sm font-medium transition-[background-color,color] duration-200 ease-out\",\n            tone === \"open\" && \"bg-foreground text-background\",\n            tone === \"soon\" && \"bg-destructive/12 text-destructive\",\n            (tone === \"closed\" || tone === \"idle\") && \"bg-muted text-muted-foreground\",\n          )}\n        >\n          <Signal tone={tone} left={state?.kind === \"open\" ? state.left : 0} />\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={pillText.replace(/\\d+ min/, \"min\")}\n              initial={reduceMotion ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, transition: { duration: 0.1 } }}\n              transition={{ duration: 0.2, ease: EASE_OUT }}\n              className=\"tabular-nums\"\n            >\n              {pillText}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        {/* Announces state changes only, not every minute of a countdown. */}\n        <span className=\"sr-only\" aria-live=\"polite\">\n          {tone === \"open\" ? \"Open now\" : tone === \"soon\" ? \"Closing within the hour\" : tone === \"closed\" ? \"Closed now\" : \"\"}\n        </span>\n      </div>\n\n      {/* Two lines on phones, one line from sm up, always: the height\n          never depends on how long the times happen to be. */}\n      <p className=\"mt-2 flex h-10 flex-col text-[13px] leading-5 text-muted-foreground tabular-nums sm:h-5 sm:flex-row sm:gap-1.5\">\n        {ready && (\n          <>\n            <span>\n              {clock(storeMinute)} in {city}\n            </span>\n            {differs && (\n              <span>\n                <span aria-hidden className=\"hidden sm:inline\">\n                  ·{\" \"}\n                </span>\n                {clock(storeMinute + offset)} your time ({zoneName(visitor)})\n              </span>\n            )}\n          </>\n        )}\n      </p>\n\n      <div\n        // The times column is a fixed width on phones too: two split shifts\n        // wrap onto two lines instead of squeezing the chart to nothing.\n        className=\"mt-3 grid grid-cols-[36px_minmax(0,1fr)_84px] items-center gap-x-3 sm:grid-cols-[40px_minmax(0,1fr)_112px]\"\n        // Fixed row heights: switching clocks can wrap a row to two lines, and\n        // nothing below may move when it does.\n        style={{ gridTemplateRows: \"20px repeat(7, 44px)\" }}\n      >\n        {/* Axis, with the now marker riding along it. */}\n        <div aria-hidden className=\"relative col-start-2 row-start-1 h-5 text-[12px] text-muted-foreground tabular-nums\">\n          {[\"12a\", \"6a\", \"12p\", \"6p\", \"12a\"].map((label, i) => (\n            <span\n              key={i}\n              style={{ left: `${i * 25}%` }}\n              className={cn(\n                \"absolute top-0\",\n                i === 0 ? \"\" : i === 4 ? \"-translate-x-full\" : \"-translate-x-1/2\",\n              )}\n            >\n              {label}\n            </span>\n          ))}\n        </div>\n\n        {/* Today's band sits behind its row, full width. */}\n        {today >= 0 && (\n          <div\n            aria-hidden\n            style={{ gridRow: today + 2 }}\n            className=\"col-span-3 col-start-1 -mx-2 h-full rounded-xl bg-muted\"\n          />\n        )}\n\n        {/* Quarter-day guides and the live now line span every row. */}\n        <div\n          aria-hidden\n          className=\"pointer-events-none relative col-start-2 row-span-7 row-start-2 h-full\"\n        >\n          {[25, 50, 75].map((p) => (\n            <span key={p} style={{ left: `${p}%` }} className=\"absolute inset-y-0 w-px bg-border\" />\n          ))}\n          {ready && (\n            // Moves once every 15 seconds at most, so `left` is fine here.\n            // It glides when the clock jumps (a new preset, or switching to\n            // your zone), so you can follow where \"now\" went.\n            <span\n              style={{ left: `${nowPct}%` }}\n              className=\"absolute -top-1 -bottom-1 z-10 w-px -translate-x-1/2 bg-foreground transition-[left] duration-300 ease-[cubic-bezier(0.77,0,0.175,1)] motion-reduce:transition-none\"\n            >\n              <span\n                style={{ top: `calc(${((today + 0.5) / 7) * 100}% - 4px)` }}\n                className=\"absolute -left-[3.5px] size-2 rounded-full bg-foreground ring-2 ring-background transition-[top] duration-300 ease-[cubic-bezier(0.77,0,0.175,1)] motion-reduce:transition-none\"\n              />\n            </span>\n          )}\n        </div>\n\n        {week.map((row, day) => {\n          const isToday = day === today;\n          const text = row.opens.length\n            ? row.opens.map((o) => `${clock(o.start, true)}–${clock(o.end, true)}`).join(\", \")\n            : row.segments.length\n              ? `until ${clock(row.segments[0].to, true)}`\n              : \"Closed\";\n          const label = row.opens.length\n            ? row.opens.map((o) => `${clock(o.start)} to ${clock(o.end)}`).join(\" and \")\n            : row.segments.length\n              ? `open until ${clock(row.segments[0].to)} from the night before`\n              : \"closed\";\n          return (\n            <div key={day} className=\"contents\">\n              <span\n                style={{ gridRow: day + 2 }}\n                className={cn(\n                  \"col-start-1 text-sm transition-[color] duration-200\",\n                  isToday ? \"font-medium text-foreground\" : \"text-muted-foreground\",\n                )}\n              >\n                <span aria-hidden>{DAYS[day]}</span>\n                <span className=\"sr-only\">\n                  {LONG_DAYS[day]}\n                  {isToday ? \", today\" : \"\"}: {label}\n                </span>\n              </span>\n              <div aria-hidden style={{ gridRow: day + 2 }} className=\"relative col-start-2 h-2.5\">\n                <AnimatePresence initial={false} mode=\"popLayout\">\n                  <motion.div\n                    key={mode}\n                    initial={reduceMotion ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\" }}\n                    animate={{ opacity: 1, filter: \"blur(0px)\" }}\n                    exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                    transition={{ duration: 0.25, ease: EASE_OUT }}\n                    className=\"absolute inset-0\"\n                  >\n                    {row.segments.length === 0 && (\n                      <span className=\"absolute inset-x-0 top-1/2 h-px bg-border\" />\n                    )}\n                    {row.segments.map((s, i) => (\n                      <span\n                        key={i}\n                        style={{ left: `${(s.from / DAY) * 100}%`, width: `${((s.to - s.from) / DAY) * 100}%` }}\n                        className={cn(\n                          \"absolute inset-y-0 transition-[background-color] duration-200\",\n                          // A carried-over shift keeps a square left end,\n                          // so it reads as the tail of yesterday's bar.\n                          s.carried ? \"rounded-r-full\" : s.to === DAY ? \"rounded-l-full\" : \"rounded-full\",\n                          isToday ? \"bg-foreground\" : \"bg-foreground/25\",\n                        )}\n                      />\n                    ))}\n                  </motion.div>\n                </AnimatePresence>\n              </div>\n              <span\n                aria-hidden\n                style={{ gridRow: day + 2 }}\n                className={cn(\n                  \"col-start-3 text-right text-[12px] leading-4 text-balance tabular-nums transition-[color] duration-200 sm:text-[13px]\",\n                  isToday ? \"font-medium text-foreground\" : \"text-muted-foreground\",\n                )}\n              >\n                {text}\n              </span>\n            </div>\n          );\n        })}\n      </div>\n    </section>\n  );\n}\n\n// \"tomorrow \", \"Tue \" or \"\" for later today, measured in the shown zone.\nfunction openingDay(from: number, to: number) {\n  const a = Math.floor(mod(from, WEEK) / DAY);\n  const b = Math.floor(mod(to, WEEK) / DAY);\n  if (a === b && mod(to - from, WEEK) < DAY) return \"at \";\n  if (mod(b - a, 7) === 1) return \"tomorrow \";\n  return `${DAYS[b]} `;\n}\n\n// A dot while open, a ring that drains through the last hour, a hollow\n// dot while closed.\nfunction Signal({ tone, left }: { tone: string; left: number }) {\n  const r = 5;\n  const c = 2 * Math.PI * r;\n  return (\n    <svg aria-hidden viewBox=\"0 0 14 14\" className=\"size-3.5 -rotate-90\">\n      <circle\n        cx={7}\n        cy={7}\n        r={r}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={2}\n        className={cn(\"transition-opacity duration-200\", tone === \"soon\" ? \"opacity-25\" : \"opacity-0\")}\n      />\n      <circle\n        cx={7}\n        cy={7}\n        r={tone === \"soon\" ? r : 3.5}\n        fill={tone === \"open\" ? \"currentColor\" : \"none\"}\n        stroke=\"currentColor\"\n        strokeWidth={tone === \"soon\" ? 2 : 1.5}\n        strokeLinecap=\"round\"\n        strokeDasharray={tone === \"soon\" ? `${c} ${c}` : undefined}\n        strokeDashoffset={tone === \"soon\" ? c * (1 - Math.max(0, Math.min(1, left / SOON))) : undefined}\n        className=\"transition-[stroke-dashoffset] duration-500 ease-out\"\n      />\n    </svg>\n  );\n}\n\nconst HOURS: WeekHours = [\n  [],\n  [[\"07:00\", \"14:00\"], [\"17:00\", \"23:00\"]],\n  [[\"07:00\", \"14:00\"], [\"17:00\", \"23:00\"]],\n  [[\"07:00\", \"14:00\"], [\"17:00\", \"23:00\"]],\n  [[\"07:00\", \"14:00\"], [\"17:00\", \"02:00\"]],\n  [[\"08:00\", \"15:00\"], [\"17:00\", \"02:00\"]],\n  [[\"08:00\", \"15:00\"]],\n];\nconst ZONE = \"America/New_York\";\n\n// Preview presets in store time: Friday 1:15 PM, Friday 11:40 PM, Monday\n// morning (closed all day), and Sunday 3:30 PM.\nconst PRESETS = [\n  { label: \"Fri 1:15 PM\", minute: 4 * DAY + 13 * 60 + 15 },\n  { label: \"Fri 11:40 PM\", minute: 4 * DAY + 23 * 60 + 40 },\n  { label: \"Mon 10 AM\", minute: 10 * 60 },\n];\n\nexport default function StoreHoursDemo() {\n  const live = useNow();\n  const [preset, setPreset] = useState<number | null>(0);\n  // A visitor who is already on New York time would never see the clock\n  // switch, which is the point of the demo, so they get London instead.\n  const detected = useVisitorZone();\n  const visitor =\n    detected && live !== null && offsetBetween(live, ZONE, detected) === 0\n      ? \"Europe/London\"\n      : undefined;\n  // Moves the live instant to the preset's store time within this week.\n  const now =\n    live === null || preset === null\n      ? undefined\n      : live + mod(PRESETS[preset].minute - wall(live, ZONE).weekMinute, WEEK) * 60000;\n  return (\n    <div className=\"flex w-[min(540px,100%)] flex-col items-center gap-4\">\n      <StoreHours\n        name=\"Marlow & Pine\"\n        address=\"212 Wythe Ave, Brooklyn\"\n        hours={HOURS}\n        timeZone={ZONE}\n        city=\"Brooklyn\"\n        now={now}\n        visitorTimeZone={visitor}\n      />\n      <div role=\"radiogroup\" aria-label=\"Preview time\" className=\"flex flex-wrap justify-center gap-1.5\">\n        {[...PRESETS.map((p) => p.label), \"Live\"].map((label, i) => {\n          const value = i < PRESETS.length ? i : null;\n          const on = preset === value;\n          return (\n            <button\n              key={label}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={on}\n              onClick={() => setPreset(value)}\n              className={cn(\n                \"h-9 touch-manipulation rounded-full px-3.5 text-[13px] font-medium outline-hidden transition-[scale,background-color,color] duration-150 ease-out select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-foreground active:scale-[0.96]\",\n                on ? \"bg-foreground text-background\" : \"bg-muted text-foreground\",\n              )}\n            >\n              {label}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"},{"path":"hooks/use-reduced-motion.ts","type":"registry:hook","target":"hooks/use-reduced-motion.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// Motion's useReducedMotion reads the media query on the very first client\n// render, but the server can't know it, so every component that renders\n// differently under reduced motion broke hydration for those users. This\n// reports false during hydration, matching the server HTML, then the real\n// preference straight after, and follows it if it changes.\nconst query = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(onChange: () => void) {\n  const media = window.matchMedia(query);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(query).matches,\n    () => false,\n  );\n}\n"}],"cssVars":{"light":{"marker":"#d93d31","shadow-raised":"0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.06), 0 6px 16px -6px oklch(0 0 0 / 0.12)"},"dark":{"marker":"#ff6b5f","shadow-raised":"0 0 0 1px oklch(1 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px -6px oklch(0 0 0 / 0.6)"},"theme":{"color-marker":"var(--marker)","shadow-raised":"var(--shadow-raised)"}},"categories":["data"],"docs":"From ui lab: https://lab.xevrion.dev/lab/store-hours"}