{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cron-builder",
  "type": "registry:component",
  "title": "Cron Builder",
  "description": "Edit a cron expression field by field or with friendly controls, read it back as a plain sentence, see the next five runs in your timezone update as you type, and get told exactly why a schedule is invalid or can never run.",
  "categories": [
    "component",
    "Dev Tools"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/tabs.json"
  ],
  "files": [
    {
      "path": "registry/devl/cron-builder.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { AlertCircleIcon, CheckIcon, CopyIcon, MinusIcon, PlusIcon } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ClipboardEvent, type KeyboardEvent, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Tabs, TabsList, TabsTab } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CronBuilderProps {\n  defaultValue?: string;\n  onChange?: (expression: string, valid: boolean) => void;\n  presets?: { label: string; value: string }[];\n  className?: string;\n}\n\ntype FieldId = \"minute\" | \"hour\" | \"dom\" | \"month\" | \"dow\";\n\ninterface FieldSpec {\n  id: FieldId;\n  label: string;\n  noun: string;\n  plural: string;\n  min: number;\n  max: number;\n  names?: string[];\n}\n\nconst MONTHS = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\nconst DAYS = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\n\nconst FIELDS: FieldSpec[] = [\n  { id: \"minute\", label: \"Minute\", noun: \"minute\", plural: \"minutes\", min: 0, max: 59 },\n  { id: \"hour\", label: \"Hour\", noun: \"hour\", plural: \"hours\", min: 0, max: 23 },\n  { id: \"dom\", label: \"Day\", noun: \"day of the month\", plural: \"days\", min: 1, max: 31 },\n  { id: \"month\", label: \"Month\", noun: \"month\", plural: \"months\", min: 1, max: 12, names: MONTHS.map((m) => m.slice(0, 3).toUpperCase()) },\n  { id: \"dow\", label: \"Weekday\", noun: \"weekday\", plural: \"weekdays\", min: 0, max: 6, names: DAYS.map((d) => d.slice(0, 3).toUpperCase()) },\n];\n\nconst DEFAULT_PRESETS = [\n  { label: \"Every 15 min\", value: \"*/15 * * * *\" },\n  { label: \"Hourly\", value: \"0 * * * *\" },\n  { label: \"Daily 09:00\", value: \"0 9 * * *\" },\n  { label: \"Weekdays 09:30\", value: \"30 9 * * 1-5\" },\n  { label: \"Mondays 08:00\", value: \"0 8 * * 1\" },\n  { label: \"1st of month\", value: \"0 0 1 * *\" },\n];\n\nconst MACROS: Record<string, string> = {\n  \"@yearly\": \"0 0 1 1 *\",\n  \"@annually\": \"0 0 1 1 *\",\n  \"@monthly\": \"0 0 1 * *\",\n  \"@weekly\": \"0 0 * * 0\",\n  \"@daily\": \"0 0 * * *\",\n  \"@midnight\": \"0 0 * * *\",\n  \"@hourly\": \"0 * * * *\",\n};\n\ntype Parsed = { values: Set<number>; error?: undefined } | { values?: undefined; error: string };\n\nfunction parseField(raw: string, spec: FieldSpec): Parsed {\n  const text = raw.trim();\n  const What = spec.label;\n  if (!text) return { error: `${What} is empty. Use * for every ${spec.noun}.` };\n  const values = new Set<number>();\n  const max = spec.id === \"dow\" ? 7 : spec.max;\n  const num = (s: string): number | string => {\n    const named = spec.names?.indexOf(s.toUpperCase()) ?? -1;\n    if (named >= 0) return named + spec.min;\n    if (!/^\\d+$/.test(s)) return `“${s}” isn’t a valid ${spec.noun}.`;\n    const n = Number(s);\n    if (n < spec.min || n > max) return `${What} must be ${spec.min}–${max}; ${n} is out of range.`;\n    return n;\n  };\n  for (const part of text.split(\",\")) {\n    if (!part) return { error: `There’s an empty item in “${text}”.` };\n    const [range, stepText, extra] = part.split(\"/\");\n    if (extra !== undefined) return { error: `“${part}” has more than one step.` };\n    let step = 1;\n    if (stepText !== undefined) {\n      if (!/^\\d+$/.test(stepText)) return { error: `Step “${stepText}” must be a whole number.` };\n      step = Number(stepText);\n      if (step === 0) return { error: \"A step of 0 never moves forward. Use 1 or more.\" };\n    }\n    let lo: number;\n    let hi: number;\n    if (range === \"*\") {\n      lo = spec.min;\n      hi = spec.max;\n    } else if (range.includes(\"-\")) {\n      const [a, b] = range.split(\"-\");\n      const na = num(a);\n      const nb = num(b ?? \"\");\n      if (typeof na === \"string\") return { error: na };\n      if (typeof nb === \"string\") return { error: nb };\n      if (na > nb) return { error: `The range ${a}-${b} runs backwards.` };\n      lo = na;\n      hi = nb;\n    } else {\n      const n = num(range);\n      if (typeof n === \"string\") return { error: n };\n      lo = n;\n      hi = stepText !== undefined ? spec.max : n;\n    }\n    for (let v = lo; v <= hi; v += step) values.add(spec.id === \"dow\" && v === 7 ? 0 : v);\n  }\n  return { values };\n}\n\nfunction serialize(values: Set<number>, spec: FieldSpec) {\n  const sorted = [...values].sort((a, b) => a - b);\n  const full = spec.max - spec.min + 1;\n  if (sorted.length === full) return \"*\";\n  if (sorted.length > 2 && sorted[0] === spec.min) {\n    const step = sorted[1] - sorted[0];\n    const fits = sorted.every((v, i) => v === spec.min + i * step) && sorted[sorted.length - 1] + step > spec.max && step > 1;\n    if (fits) return `*/${step}`;\n  }\n  const parts: string[] = [];\n  let i = 0;\n  while (i < sorted.length) {\n    let j = i;\n    while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1) j++;\n    if (j - i >= 2) parts.push(`${sorted[i]}-${sorted[j]}`);\n    else for (let k = i; k <= j; k++) parts.push(String(sorted[k]));\n    i = j + 1;\n  }\n  return parts.join(\",\");\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\n\nfunction list(items: string[]) {\n  if (items.length <= 1) return items.join(\"\");\n  return `${items.slice(0, -1).join(\", \")} and ${items[items.length - 1]}`;\n}\n\nfunction runs(values: number[], name: (n: number) => string) {\n  const out: string[] = [];\n  let i = 0;\n  while (i < values.length) {\n    let j = i;\n    while (j + 1 < values.length && values[j + 1] === values[j] + 1) j++;\n    if (j - i >= 2) out.push(`${name(values[i])} through ${name(values[j])}`);\n    else for (let k = i; k <= j; k++) out.push(name(values[k]));\n    i = j + 1;\n  }\n  return list(out);\n}\n\nfunction stepOf(values: number[], min: number, max: number) {\n  if (values.length < 2 || values[0] !== min) return 0;\n  const s = values[1] - values[0];\n  return values.every((v, i) => v === min + i * s) && values[values.length - 1] + s > max ? s : 0;\n}\n\nfunction describe(sets: Record<FieldId, Set<number>>, raw: Record<FieldId, string>) {\n  const m = [...sets.minute].sort((a, b) => a - b);\n  const h = [...sets.hour].sort((a, b) => a - b);\n  const allM = m.length === 60;\n  const allH = h.length === 24;\n  const mStep = stepOf(m, 0, 59);\n  const hStep = stepOf(h, 0, 23);\n  let time: string;\n  if (allM && allH) time = \"Every minute\";\n  else if (mStep && allH) time = `Every ${mStep} minutes`;\n  else if (m.length === 1 && allH) time = m[0] === 0 ? \"Every hour, on the hour\" : `At ${m[0]} minutes past every hour`;\n  else if (m.length === 1 && hStep) time = `At minute ${m[0]}, every ${hStep} hours`;\n  else if (m.length * h.length <= 4 && !allH) time = `At ${list(h.flatMap((hh) => m.map((mm) => `${pad(hh)}:${pad(mm)}`)))}`;\n  else if (allH) time = `At minutes ${list(m.map(String))} past every hour`;\n  else if (allM) time = `Every minute during ${runs(h, (x) => `${pad(x)}:00`)} hours`;\n  else time = `At minutes ${runs(m, String)} past ${runs(h, (x) => `${pad(x)}:00`)}`;\n\n  const either = !raw.dom.trim().startsWith(\"*\") && !raw.dow.trim().startsWith(\"*\");\n  const domR = sets.dom.size < 31;\n  const dowR = sets.dow.size < 7;\n  const dom = [...sets.dom].sort((a, b) => a - b);\n  const dow = [...sets.dow].sort((a, b) => ((a + 6) % 7) - ((b + 6) % 7));\n  const dowSorted = dow.map((d) => (d + 6) % 7);\n  const dowWords = () => {\n    if (dow.length === 2 && sets.dow.has(0) && sets.dow.has(6)) return \"on weekends\";\n    return `on ${runs(dowSorted, (x) => DAYS[(x + 1) % 7])}`;\n  };\n  const domWords = () => {\n    const step = stepOf(dom, 1, 31);\n    if (step) return `every ${step} days`;\n    return `on day ${runs(dom, String)} of the month`;\n  };\n  let days = \"\";\n  if (domR && dowR) days = `${domWords()} ${either ? \"or\" : \"and\"} ${dowWords()}`;\n  else if (domR) days = domWords();\n  else if (dowR) days = dowWords();\n\n  const mo = [...sets.month].sort((a, b) => a - b);\n  let months = \"\";\n  if (mo.length < 12) {\n    const step = stepOf(mo, 1, 12);\n    months = step ? `every ${step} months` : `in ${runs(mo, (x) => MONTHS[x - 1])}`;\n  }\n  return [time, days, months].filter(Boolean).join(\", \");\n}\n\nfunction nextRuns(sets: Record<FieldId, Set<number>>, raw: Record<FieldId, string>, from: Date, count = 5) {\n  const out: Date[] = [];\n  const hours = [...sets.hour].sort((a, b) => a - b);\n  const minutes = [...sets.minute].sort((a, b) => a - b);\n  const either = !raw.dom.trim().startsWith(\"*\") && !raw.dow.trim().startsWith(\"*\");\n  const start = new Date(from);\n  start.setSeconds(0, 0);\n  start.setMinutes(start.getMinutes() + 1);\n  const day = new Date(start.getFullYear(), start.getMonth(), start.getDate());\n  for (let i = 0; i < 366 * 5 && out.length < count; i++) {\n    const d = new Date(day.getFullYear(), day.getMonth(), day.getDate() + i);\n    if (!sets.month.has(d.getMonth() + 1)) continue;\n    const domOk = sets.dom.has(d.getDate());\n    const dowOk = sets.dow.has(d.getDay());\n    const match = either ? domOk || dowOk : domOk && dowOk;\n    if (!match) continue;\n    for (const hh of hours) {\n      for (const mm of minutes) {\n        const t = new Date(d.getFullYear(), d.getMonth(), d.getDate(), hh, mm);\n        if (t >= start) out.push(t);\n        if (out.length >= count) return out;\n      }\n    }\n  }\n  return out;\n}\n\nfunction neverReason(sets: Record<FieldId, Set<number>>) {\n  const minDom = Math.min(...sets.dom);\n  const mo = [...sets.month];\n  if (mo.every((m) => m === 2) && minDom > 29) return `February never has a ${minDom}${minDom === 31 ? \"st\" : \"th\"}.`;\n  if (minDom === 31 && mo.every((m) => [2, 4, 6, 9, 11].includes(m))) return `${list(mo.map((m) => MONTHS[m - 1]))} ${mo.length > 1 ? \"don’t\" : \"doesn’t\"} have a 31st.`;\n  return \"No date matches every field at once.\";\n}\n\nconst SHORT_DAYS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst SHORT_MONTHS = MONTHS.map((m) => m.slice(0, 3));\nconst when = (d: Date) => `${SHORT_DAYS[d.getDay()]}, ${SHORT_MONTHS[d.getMonth()]} ${d.getDate()}`;\nfunction relative(d: Date, now: number) {\n  const mins = Math.round((d.getTime() - now) / 60000);\n  if (mins < 60) return `in ${Math.max(1, mins)} min`;\n  const hrs = Math.floor(mins / 60);\n  const today = new Date(now);\n  const days = Math.round(\n    (new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() - new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime()) / 86400000,\n  );\n  if (hrs < 12 || days === 0) return `in ${hrs}h ${pad(mins % 60)}m`;\n  return days === 1 ? \"tomorrow\" : `in ${days} days`;\n}\n\ntype Mode = \"every\" | \"step\" | \"specific\";\nconst modeOf = (raw: string): Mode => (raw.trim() === \"*\" ? \"every\" : /^\\*\\/\\d+$/.test(raw.trim()) ? \"step\" : \"specific\");\n\nfunction gridFor(spec: FieldSpec, current: Set<number>) {\n  if (spec.id === \"minute\") {\n    const base = Array.from({ length: 12 }, (_, i) => i * 5);\n    return [...new Set([...base, ...current])].sort((a, b) => a - b);\n  }\n  if (spec.id === \"dow\") return [1, 2, 3, 4, 5, 6, 0];\n  return Array.from({ length: spec.max - spec.min + 1 }, (_, i) => spec.min + i);\n}\n\nconst cellLabel = (spec: FieldSpec, v: number) =>\n  spec.id === \"month\" ? SHORT_MONTHS[v - 1] : spec.id === \"dow\" ? SHORT_DAYS[v] : spec.id === \"hour\" ? pad(v) : spec.id === \"minute\" ? `:${pad(v)}` : String(v);\n\nexport function CronBuilder({ defaultValue = \"30 9 * * 1-5\", onChange, presets = DEFAULT_PRESETS, className }: CronBuilderProps) {\n  const errorId = useId();\n  const reduce = useReducedMotion();\n  const panelRef = useRef<HTMLDivElement>(null);\n  const [panelHeight, setPanelHeight] = useState<number | null>(null);\n  const [raw, setRaw] = useState<Record<FieldId, string>>(() => {\n    const parts = (MACROS[defaultValue] ?? defaultValue).trim().split(/\\s+/);\n    return { minute: parts[0] ?? \"*\", hour: parts[1] ?? \"*\", dom: parts[2] ?? \"*\", month: parts[3] ?? \"*\", dow: parts[4] ?? \"*\" };\n  });\n  const [active, setActive] = useState<FieldId>(\"minute\");\n  const [now, setNow] = useState<number | null>(null);\n  const [tz, setTz] = useState(\"\");\n  const [copied, setCopied] = useState(false);\n  const inputs = useRef<Record<string, HTMLInputElement | null>>({});\n  const mounted = useRef(false);\n  const changeRef = useRef(onChange);\n  changeRef.current = onChange;\n\n  useEffect(() => {\n    mounted.current = true;\n    setNow(Date.now());\n    setTz(Intl.DateTimeFormat().resolvedOptions().timeZone.replace(/_/g, \" \"));\n    const t = setInterval(() => setNow(Date.now()), 30000);\n    return () => clearInterval(t);\n  }, []);\n\n  useLayoutEffect(() => {\n    const el = panelRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setPanelHeight(el.offsetHeight));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  useEffect(() => {\n    if (!copied) return;\n    const t = setTimeout(() => setCopied(false), 1400);\n    return () => clearTimeout(t);\n  }, [copied]);\n\n  const parsed = useMemo(() => Object.fromEntries(FIELDS.map((f) => [f.id, parseField(raw[f.id], f)])) as Record<FieldId, Parsed>, [raw]);\n  const firstError = FIELDS.find((f) => parsed[f.id].error);\n  const sets = firstError ? null : (Object.fromEntries(FIELDS.map((f) => [f.id, parsed[f.id].values])) as Record<FieldId, Set<number>>);\n  const expression = FIELDS.map((f) => raw[f.id].trim() || \"?\").join(\" \");\n  const sentence = sets ? describe(sets, raw) : null;\n  const runsList = useMemo(() => (sets && now ? nextRuns(sets, raw, new Date(now)) : []), [sets, raw, now]);\n  const never = sets && now !== null && runsList.length === 0;\n\n  useEffect(() => {\n    changeRef.current?.(expression, !firstError);\n  }, [expression, firstError]);\n\n  const spec = FIELDS.find((f) => f.id === active) ?? FIELDS[0];\n  const mode = modeOf(raw[active]);\n  const current = parsed[active].values ?? new Set<number>();\n  const stepValue = mode === \"step\" ? Number(raw[active].trim().slice(2)) : 0;\n\n  const setField = (id: FieldId, value: string) => setRaw((r) => ({ ...r, [id]: value }));\n\n  const applyExpression = (text: string) => {\n    const parts = (MACROS[text.trim().toLowerCase()] ?? text).trim().split(/\\s+/);\n    if (parts.length !== 5) return false;\n    setRaw({ minute: parts[0], hour: parts[1], dom: parts[2], month: parts[3], dow: parts[4] });\n    return true;\n  };\n\n  const onPaste = (e: ClipboardEvent<HTMLInputElement>) => {\n    const text = e.clipboardData.getData(\"text\");\n    if (/\\s/.test(text.trim()) || text.trim().startsWith(\"@\")) {\n      if (applyExpression(text)) e.preventDefault();\n    }\n  };\n\n  const onFieldKey = (e: KeyboardEvent<HTMLInputElement>, i: number) => {\n    const el = e.currentTarget;\n    const go = (j: number) => {\n      const f = FIELDS[j];\n      if (!f) return;\n      e.preventDefault();\n      inputs.current[f.id]?.focus();\n      requestAnimationFrame(() => inputs.current[f.id]?.select());\n    };\n    if (e.key === \" \") go(i + 1);\n    else if (e.key === \"ArrowRight\" && el.selectionStart === el.value.length) go(i + 1);\n    else if (e.key === \"ArrowLeft\" && el.selectionStart === 0) go(i - 1);\n    else if (e.key === \"Backspace\" && el.value === \"\") go(i - 1);\n  };\n\n  const setMode = (m: Mode) => {\n    if (m === \"every\") setField(active, \"*\");\n    else if (m === \"step\") setField(active, `*/${active === \"minute\" ? 15 : 2}`);\n    else {\n      const fallback = active === \"dow\" ? \"1-5\" : active === \"month\" || active === \"dom\" ? \"1\" : \"0\";\n      setField(active, current.size && mode === \"specific\" ? raw[active] : fallback);\n    }\n  };\n\n  const toggleValue = (v: number) => {\n    const next = new Set(mode === \"specific\" ? current : []);\n    if (next.has(v)) next.delete(v);\n    else next.add(v);\n    setField(active, next.size ? serialize(next, spec) : \"*\");\n  };\n\n  const grid = gridFor(spec, current);\n  const cols = spec.id === \"hour\" ? \"grid-cols-8\" : spec.id === \"dom\" ? \"grid-cols-8\" : spec.id === \"dow\" ? \"grid-cols-7\" : \"grid-cols-6\";\n  const canStep = active !== \"dow\";\n  const maxStep = Math.floor((spec.max - spec.min + 1) / 2);\n\n  return (\n    <div className={cn(\"flex w-full min-w-0 max-w-[500px] flex-col overflow-hidden rounded-2xl border bg-card text-sm shadow-xs/5\", className)}>\n      <div className=\"flex items-start gap-3 px-4 pt-3.5\">\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"font-medium\">Schedule</h3>\n          <p className=\"truncate text-muted-foreground text-xs\">\n            {tz ? `Times in ${tz}` : \"Times in your timezone\"}\n            <span className=\"max-sm:hidden\"> · paste any cron line</span>\n          </p>\n        </div>\n        <button\n          aria-label={copied ? \"Copied\" : \"Copy expression\"}\n          className=\"inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border bg-background px-2.5 font-mono text-xs shadow-xs/5 outline-none transition-[background-color,scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] dark:bg-input/32 [@media(hover:hover)]:hover:bg-accent\"\n          onClick={async () => {\n            try {\n              await navigator.clipboard.writeText(expression);\n            } catch {}\n            setCopied(true);\n          }}\n          type=\"button\"\n        >\n          {copied ? <CheckIcon className=\"size-3 text-success-foreground\" /> : <CopyIcon className=\"size-3 text-muted-foreground\" />}\n          {expression}\n        </button>\n      </div>\n\n      <div className=\"grid grid-cols-5 gap-1.5 px-4 pt-3.5 sm:gap-2\">\n        {FIELDS.map((f, i) => {\n          const err = parsed[f.id].error;\n          const isActive = f.id === active;\n          return (\n            <label className=\"flex min-w-0 flex-col items-center gap-1.5\" key={f.id}>\n              <input\n                aria-describedby={err ? errorId : undefined}\n                aria-invalid={Boolean(err) || undefined}\n                aria-label={f.label}\n                autoComplete=\"off\"\n                className={cn(\n                  \"h-11 w-full min-w-0 rounded-lg border bg-background text-center font-mono text-[15px] shadow-xs/5 outline-none ring-ring/24 transition-[border-color,box-shadow] duration-150 dark:bg-input/32\",\n                  isActive ? \"border-ring ring-[3px]\" : \"border-input\",\n                  err && \"border-destructive/60 text-destructive-foreground ring-destructive/16\",\n                )}\n                onChange={(e) => setField(f.id, e.target.value.replace(/\\s/g, \"\"))}\n                onFocus={(e) => {\n                  setActive(f.id);\n                  e.currentTarget.select();\n                }}\n                onKeyDown={(e) => onFieldKey(e, i)}\n                onPaste={onPaste}\n                ref={(el) => {\n                  inputs.current[f.id] = el;\n                }}\n                spellCheck={false}\n                value={raw[f.id]}\n              />\n              <span className={cn(\"text-[11px] transition-colors\", isActive ? \"font-medium text-foreground\" : \"text-muted-foreground\", err && \"text-destructive-foreground\")}>\n                {f.label}\n              </span>\n            </label>\n          );\n        })}\n      </div>\n\n      <div aria-live=\"polite\" className=\"min-h-[60px] px-4 pt-3 pb-3.5\">\n        {firstError ? (\n          <p className=\"flex items-start gap-2 text-destructive-foreground\" id={errorId}>\n            <AlertCircleIcon className=\"mt-0.5 size-4 shrink-0\" />\n            <span className=\"text-pretty\">{parsed[firstError.id].error}</span>\n          </p>\n        ) : (\n          <>\n            <p className=\"font-medium text-[15px] leading-6 text-pretty\">{sentence}</p>\n            {never && sets && (\n              <p className=\"mt-1 flex items-center gap-1.5 text-warning-foreground text-xs\">\n                <AlertCircleIcon className=\"size-3.5\" /> Never runs: {neverReason(sets)}\n              </p>\n            )}\n          </>\n        )}\n      </div>\n\n      <div className=\"flex flex-wrap gap-1.5 px-4 pb-3.5\">\n        {presets.map((p) => {\n          const on = p.value === expression;\n          return (\n            <button\n              aria-pressed={on}\n              className={cn(\n                \"h-8 rounded-full border px-3 text-xs outline-none transition-[background-color,border-color,color,scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\",\n                on ? \"border-primary bg-primary text-primary-foreground\" : \"bg-background text-muted-foreground dark:bg-input/32 [@media(hover:hover)]:hover:text-foreground\",\n              )}\n              key={p.value}\n              onClick={() => applyExpression(p.value)}\n              type=\"button\"\n            >\n              {p.label}\n            </button>\n          );\n        })}\n      </div>\n\n      <div className=\"border-t bg-muted/30 px-4 py-3.5\">\n        <div className=\"flex items-center justify-between gap-3\">\n          <span className=\"font-medium text-xs\">{spec.label}</span>\n          <Tabs onValueChange={(v) => setMode(v as Mode)} value={mode}>\n            <TabsList className=\"text-muted-foreground\" size=\"sm\">\n              <TabsTab className=\"px-2.5 text-xs sm:text-xs\" value=\"every\">\n                Every\n              </TabsTab>\n              {canStep && (\n                <TabsTab className=\"px-2.5 text-xs sm:text-xs\" value=\"step\">\n                  Every N\n                </TabsTab>\n              )}\n              <TabsTab className=\"px-2.5 text-xs sm:text-xs\" value=\"specific\">\n                Specific\n              </TabsTab>\n            </TabsList>\n          </Tabs>\n        </div>\n\n        <motion.div\n          animate={{ height: panelHeight ?? \"auto\" }}\n          className=\"overflow-hidden\"\n          initial={false}\n          transition={reduce ? { duration: 0 } : { duration: 0.2, ease: [0.23, 1, 0.32, 1] }}\n        >\n          <div ref={panelRef}>\n            <motion.div\n              animate={{ opacity: 1 }}\n              className=\"pt-3\"\n              initial={mounted.current ? { opacity: 0.4 } : false}\n              key={`${active}-${mode === \"specific\" ? \"s\" : mode}`}\n              transition={{ duration: 0.12 }}\n            >\n              {mode === \"every\" && (\n                <p className=\"flex h-9 items-center text-muted-foreground text-xs\">\n                  Runs every {spec.noun}\n                  {spec.id === \"dom\" || spec.id === \"dow\" ? \" (unless the other day field narrows it)\" : \"\"}.\n                </p>\n              )}\n              {mode === \"step\" && (\n                <div className=\"flex h-9 items-center gap-2 text-xs\">\n                  <span className=\"text-muted-foreground\">Every</span>\n                  <div className=\"flex items-center rounded-lg border bg-background shadow-xs/5 dark:bg-input/32\">\n                    <button\n                      aria-label=\"Fewer\"\n                      className=\"grid size-8 place-items-center rounded-s-lg text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40 [@media(hover:hover)]:hover:text-foreground\"\n                      disabled={stepValue <= 2}\n                      onClick={() => setField(active, `*/${stepValue - 1}`)}\n                      type=\"button\"\n                    >\n                      <MinusIcon className=\"size-3\" />\n                    </button>\n                    <span className=\"w-8 text-center font-medium font-mono tabular-nums\">{stepValue}</span>\n                    <button\n                      aria-label=\"More\"\n                      className=\"grid size-8 place-items-center rounded-e-lg text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40 [@media(hover:hover)]:hover:text-foreground\"\n                      disabled={stepValue >= maxStep}\n                      onClick={() => setField(active, `*/${stepValue + 1}`)}\n                      type=\"button\"\n                    >\n                      <PlusIcon className=\"size-3\" />\n                    </button>\n                  </div>\n                  <span className=\"text-muted-foreground\">{spec.plural}</span>\n                </div>\n              )}\n              {mode === \"specific\" && (\n                <div className={cn(\"grid gap-1\", cols)}>\n                  {grid.map((v) => {\n                    const on = current.has(v);\n                    return (\n                      <button\n                        aria-label={spec.id === \"month\" ? MONTHS[v - 1] : spec.id === \"dow\" ? DAYS[v] : `${spec.label} ${v}`}\n                        aria-pressed={on}\n                        className={cn(\n                          \"h-8 rounded-md font-mono text-[11px] tabular-nums outline-none transition-[background-color,color,scale] duration-100 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\",\n                          on ? \"border border-primary bg-primary text-primary-foreground\" : \"border bg-background text-muted-foreground dark:bg-input/32 [@media(hover:hover)]:hover:text-foreground\",\n                          (spec.id === \"month\" || spec.id === \"dow\") && \"font-sans text-xs\",\n                        )}\n                        key={v}\n                        onClick={() => toggleValue(v)}\n                        type=\"button\"\n                      >\n                        {cellLabel(spec, v)}\n                      </button>\n                    );\n                  })}\n                </div>\n              )}\n            </motion.div>\n          </div>\n        </motion.div>\n      </div>\n\n      <div className=\"border-t px-4 py-3\">\n        <p className=\"mb-1.5 font-medium text-muted-foreground text-xs\">Next runs</p>\n        {firstError || never ? (\n          <div className=\"grid h-[140px] place-items-center rounded-lg border border-dashed text-center text-muted-foreground text-xs\">\n            {firstError ? \"Fix the expression to see upcoming runs.\" : \"Nothing scheduled: this expression never matches a real date.\"}\n          </div>\n        ) : (\n          <ol className=\"flex flex-col\">\n            {(now === null ? Array.from({ length: 5 }, () => null) : runsList).map((d, i) => (\n              <li className=\"flex h-7 items-center gap-3 text-xs\" key={d ? d.getTime() : `e${i}`}>\n                <span className=\"w-4 text-muted-foreground tabular-nums\">{i + 1}</span>\n                {d && now !== null ? (\n                  <>\n                    <span className={cn(\"flex-1 tabular-nums\", i === 0 && \"font-medium\")}>\n                      {when(d)} <span className=\"text-muted-foreground\">·</span> {pad(d.getHours())}:{pad(d.getMinutes())}\n                    </span>\n                    <span className=\"text-muted-foreground tabular-nums\">{relative(d, now)}</span>\n                  </>\n                ) : (\n                  <span className=\"h-2 flex-1 rounded-full bg-muted\" style={{ maxWidth: `${60 - i * 6}%` }} />\n                )}\n              </li>\n            ))}\n          </ol>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}