{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"progress-tracker-clean","type":"registry:ui","title":"Progress Tracker (Clean)","description":"Onboarding checklist with an animated progress bar, ticks that draw themselves and auto play","dependencies":[],"files":[{"path":"registry/progress-tracker-clean/progress-tracker-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\n\n/*\n * Progress Tracker – a checklist with a progress bar, in a single self-contained file.\n * The styles are injected below, so no extra files are needed.\n * Clean style: one white card with hairline shadows, quiet gray type, a thin blue bar\n * and blue checks.\n */\n\nexport type ProgressStep = {\n  id: string;\n  label: string;\n  done?: boolean;\n  /** Small icon on the right, e.g. the app the step belongs to */\n  icon?: ReactNode;\n};\n\nexport type ProgressTrackerProps = {\n  title?: string;\n  steps: ProgressStep[];\n  /** Ticks the open steps one after another, then starts over – stops as soon as someone interacts */\n  autoPlay?: boolean;\n  /** Pause between two automatic steps, in ms */\n  interval?: number;\n  onChange?: (steps: ProgressStep[]) => void;\n  className?: string;\n};\n\nexport function ProgressTracker({\n  title = \"Getting started\",\n  steps: initial,\n  autoPlay = false,\n  interval = 2200,\n  onChange,\n  className,\n}: ProgressTrackerProps) {\n  const [done, setDone] = useState(() => new Set(initial.filter((s) => s.done).map((s) => s.id)));\n  const [playing, setPlaying] = useState(autoPlay);\n\n  // Steps added later bring their own done state; steps that go away no longer count\n  const seen = useRef(new Set(initial.map((s) => s.id)));\n  const ids = initial.map((s) => s.id).join(\"|\");\n  useEffect(() => {\n    const fresh = initial.filter((s) => !seen.current.has(s.id));\n    fresh.forEach((s) => seen.current.add(s.id));\n    if (fresh.some((s) => s.done)) setDone((prev) => new Set([...prev, ...fresh.filter((s) => s.done).map((s) => s.id)]));\n  }, [ids]); // eslint-disable-line react-hooks/exhaustive-deps\n\n  const total = initial.length;\n  const count = initial.filter((s) => done.has(s.id)).length;\n  const progress = total ? count / total : 0;\n\n  const toggle = (id: string) => {\n    const next = new Set(done);\n    if (next.has(id)) next.delete(id);\n    else next.add(id);\n    setDone(next);\n    onChange?.(initial.map((s) => ({ ...s, done: next.has(s.id) })));\n  };\n\n  // Auto play: tick the next open step; when all are done, start over\n  useEffect(() => {\n    if (!playing) return;\n    const open = initial.find((s) => !done.has(s.id));\n    const t = window.setTimeout(\n      () => {\n        if (open) toggle(open.id);\n        else setDone(new Set(initial.filter((s) => s.done).map((s) => s.id)));\n      },\n      open ? interval : interval * 2,\n    );\n    return () => clearTimeout(t);\n  }, [playing, done]); // eslint-disable-line react-hooks/exhaustive-deps\n\n  return (\n    <div\n      className={[\"ui-progressc\", className].filter(Boolean).join(\" \")}\n      data-complete={count === total || undefined}\n      onPointerDown={() => setPlaying(false)}\n      onKeyDown={() => setPlaying(false)}\n    >\n      {/* React 19 hoists this into <head> once, no matter how many trackers render */}\n      <style href=\"ui-progress-tracker-clean\" precedence=\"default\">\n        {css}\n      </style>\n\n      <div className=\"ui-progressc__head\">\n        <h3 className=\"ui-progressc__title\" dir=\"auto\" title={title}>\n          {title}\n        </h3>\n        <span className=\"ui-progressc__count\" aria-hidden=\"true\">\n          <span className=\"ui-progressc__digit\" key={count}>\n            {num.format(count)}\n          </span>\n          /{num.format(total)}\n        </span>\n      </div>\n\n      <div\n        className=\"ui-progressc__bar\"\n        role=\"progressbar\"\n        aria-label={title}\n        aria-valuemin={0}\n        aria-valuemax={total}\n        aria-valuenow={count}\n        aria-valuetext={`${num.format(count)} of ${num.format(total)} done`}\n      >\n        <span className=\"ui-progressc__fill\" style={{ transform: `scaleX(${progress})` }}>\n          {/* A new key replays the shine each time progress grows */}\n          <span className=\"ui-progressc__shine\" key={count} />\n        </span>\n      </div>\n\n      <ul className=\"ui-progressc__list\">\n        {initial.map((step, index) => {\n          const checked = done.has(step.id);\n          return (\n            <li key={step.id} style={{ animationDelay: `${180 + Math.min(index, 12) * 90}ms` }}>\n              <button\n                type=\"button\"\n                role=\"checkbox\"\n                aria-checked={checked}\n                className=\"ui-progressc__step\"\n                onClick={() => toggle(step.id)}\n              >\n                <span className=\"ui-progressc__check\" aria-hidden=\"true\">\n                  <svg viewBox=\"0 0 24 24\">\n                    <path d=\"M6.5 12.5l3.5 3.5 7.5-8\" pathLength={1} />\n                  </svg>\n                </span>\n                <span className=\"ui-progressc__label\" title={step.label || undefined}>\n                  {/* An empty label keeps the row's height */}\n                  <span dir=\"auto\">{step.label || \"\\u00a0\"}</span>\n                </span>\n                {step.icon && (\n                  <span className=\"ui-progressc__icon\" aria-hidden=\"true\">\n                    {step.icon}\n                  </span>\n                )}\n              </button>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n\n// 1284 → \"1,284\"\nconst num = new Intl.NumberFormat(\"en\");\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors and motion as variables – override them on .ui-progressc (see Theming in the docs) */\n.ui-progressc {\n  --ui-pgc-card: var(--aui-surface, #ffffff);\n  --ui-pgc-strong: var(--aui-ink, #000000);\n  --ui-pgc-ink: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-pgc-muted: var(--aui-muted, rgba(0, 0, 0, 0.45));\n  --ui-pgc-faint: var(--aui-placeholder, rgba(0, 0, 0, 0.27));\n  --ui-pgc-icon: var(--aui-muted, rgba(0, 0, 0, 0.447));\n  --ui-pgc-ring: var(--aui-border-strong, rgba(0, 0, 0, 0.2));\n  --ui-pgc-track: rgba(0, 0, 0, 0.06);\n  --ui-pgc-hover: var(--aui-subtle, rgba(0, 0, 0, 0.03));\n  --ui-pgc-accent: rgba(0, 96, 226, 0.765);\n  --ui-pgc-on-accent: #ffffff;\n  --ui-pgc-shine: rgba(255, 255, 255, 0.82);\n  --ui-pgc-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  /* Strokes are shadows: a 0.5px hairline plus a soft lift */\n  --ui-pgc-shadow: var(--aui-shadow-card, 0 0 0 0.5px rgba(0, 0, 0, 0.06), 0 1px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.05));\n  --ui-pgc-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n\n  container-type: inline-size;\n  box-sizing: border-box;\n  width: 100%;\n  max-width: 340px;\n  padding: 14px 6px 6px;\n  background: var(--ui-pgc-card);\n  border-radius: var(--aui-radius-lg, 10px);\n  box-shadow: var(--ui-pgc-shadow);\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  font-weight: 400;\n  line-height: 20px;\n  color: var(--ui-pgc-ink);\n  text-align: left;\n  -webkit-font-smoothing: antialiased;\n}\n.ui-progressc *,\n.ui-progressc *::before,\n.ui-progressc *::after {\n  box-sizing: border-box;\n}\n:where(.ui-progressc) button {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  background: none;\n  font: inherit;\n  letter-spacing: inherit;\n  color: inherit;\n  cursor: pointer;\n}\n\n/* Header: the title and a quiet counter */\n.ui-progressc__head {\n  display: flex;\n  align-items: baseline;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 0 10px;\n}\n.ui-progressc__title {\n  margin: 0;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  color: var(--ui-pgc-strong);\n  font-size: 15px;\n  font-weight: 500;\n  line-height: 24px;\n}\n.ui-progressc__count {\n  font-size: 12px;\n  line-height: 16px;\n  color: var(--ui-pgc-muted);\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n}\n/* The number rolls in from below whenever it changes */\n.ui-progressc__digit {\n  display: inline-block;\n  color: var(--ui-pgc-ink);\n  animation: ui-progressc-roll 560ms var(--ui-pgc-ease);\n}\n\n/* A thin blue bar on a gray track */\n.ui-progressc__bar {\n  position: relative;\n  height: 4px;\n  margin: 10px 10px 8px;\n  border-radius: 2px;\n  background: var(--ui-pgc-track);\n  overflow: hidden;\n}\n.ui-progressc__fill {\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  background: var(--ui-pgc-accent);\n  transform-origin: 0 50%;\n  overflow: hidden;\n  transition: transform 1100ms var(--ui-pgc-ease);\n}\n/* A light band – always lighter than the bar – sweeps across */\n.ui-progressc__shine {\n  position: absolute;\n  inset: 0;\n  background: linear-gradient(90deg, transparent 30%, var(--ui-pgc-shine) 50%, transparent 70%) 0 0 / 220% 100%\n    no-repeat;\n  animation: ui-progressc-shine 1400ms ease-out both;\n}\n\n.ui-progressc__list {\n  display: grid;\n  /* One column that never grows past the card, so long labels shorten instead of pushing rows out */\n  grid-template-columns: minmax(0, 1fr);\n  gap: 0;\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n/* On mount the card settles in, then the rows fade up one after another */\n.ui-progressc {\n  animation: ui-progressc-in 520ms var(--ui-pgc-ease) both;\n}\n.ui-progressc__list > li {\n  position: relative;\n  animation: ui-progressc-row-in 640ms var(--ui-pgc-ease) both;\n}\n\n.ui-progressc__step {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  width: 100%;\n  padding: 6px 10px;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  background: transparent;\n  font-size: 13px;\n  line-height: 20px;\n  text-align: left;\n  transition: background-color 90ms ease-out;\n}\n.ui-progressc__step:hover {\n  background: var(--ui-pgc-hover);\n}\n.ui-progressc__step:focus-visible {\n  outline: 2px solid var(--ui-pgc-focus);\n  outline-offset: -2px;\n}\n\n/* Checkbox: a hairline ring that fills blue and pops, then the tick draws itself */\n.ui-progressc__check {\n  position: relative;\n  flex: none;\n  display: grid;\n  place-items: center;\n  width: 16px;\n  height: 16px;\n  border-radius: 50%;\n  background: transparent;\n  box-shadow: inset 0 0 0 1px var(--ui-pgc-ring);\n  transition:\n    background-color 220ms var(--ui-pgc-ease),\n    box-shadow 220ms var(--ui-pgc-ease);\n}\n.ui-progressc__check svg {\n  width: 12px;\n  height: 12px;\n  fill: none;\n  stroke: var(--ui-pgc-on-accent);\n  stroke-width: 2.2;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  stroke-dasharray: 1;\n  stroke-dashoffset: 1;\n  transition: stroke-dashoffset 120ms ease-in;\n}\n.ui-progressc__step[aria-checked=\"true\"] .ui-progressc__check {\n  background: var(--ui-pgc-accent);\n  box-shadow: inset 0 0 0 1px transparent;\n  animation: ui-progressc-pop 560ms var(--ui-pgc-ease);\n}\n.ui-progressc__step[aria-checked=\"true\"] .ui-progressc__check svg {\n  stroke-dashoffset: 0;\n  transition: stroke-dashoffset 420ms var(--ui-pgc-ease) 160ms;\n}\n\n/* Label: grays out while a thin line strikes through it */\n.ui-progressc__label {\n  flex: 1;\n  min-width: 0;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  transition: color 300ms ease;\n}\n.ui-progressc__label > span {\n  background: linear-gradient(var(--ui-pgc-faint), var(--ui-pgc-faint)) 0 60% / 0% 1px no-repeat;\n  transition: background-size 380ms var(--ui-pgc-ease);\n}\n.ui-progressc__step[aria-checked=\"true\"] .ui-progressc__label {\n  color: var(--ui-pgc-muted);\n}\n.ui-progressc__step[aria-checked=\"true\"] .ui-progressc__label > span {\n  background-size: 100% 1px;\n  transition: background-size 620ms var(--ui-pgc-ease) 220ms;\n}\n\n.ui-progressc__icon {\n  flex: none;\n  display: grid;\n  place-items: center;\n  width: 16px;\n  height: 16px;\n  color: var(--ui-pgc-icon);\n  transition: opacity 300ms ease;\n}\n/* Any icon – an svg or an image of any shape – fits its 16px tile */\n.ui-progressc__icon {\n  overflow: hidden;\n}\n.ui-progressc__icon > :is(svg, img) {\n  width: 100%;\n  height: 100%;\n  object-fit: contain;\n}\n.ui-progressc__step[aria-checked=\"true\"] .ui-progressc__icon {\n  opacity: 0.45;\n}\n\n/* Touch screens: taller rows */\n@media (pointer: coarse) {\n  .ui-progressc__step {\n    padding-top: 10px;\n    padding-bottom: 10px;\n  }\n}\n/* Narrow cards: a little less inset */\n@container (max-width: 300px) {\n  .ui-progressc__head { padding: 0 8px; }\n  .ui-progressc__bar { margin-left: 8px; margin-right: 8px; }\n  .ui-progressc__step { padding-left: 8px; padding-right: 8px; }\n}\n\n@keyframes ui-progressc-pop {\n  0% { transform: scale(0.9); }\n  60% { transform: scale(1.08); }\n  100% { transform: scale(1); }\n}\n@keyframes ui-progressc-in {\n  from {\n    opacity: 0;\n    transform: translateY(8px);\n  }\n}\n@keyframes ui-progressc-row-in {\n  from {\n    opacity: 0;\n    filter: blur(3px);\n    transform: translateY(8px);\n  }\n}\n@keyframes ui-progressc-roll {\n  from { opacity: 0; transform: translateY(60%); }\n  to { opacity: 1; transform: none; }\n}\n@keyframes ui-progressc-shine {\n  from { background-position: 120% 0; opacity: 1; }\n  to { background-position: -20% 0; opacity: 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-progressc,\n  .ui-progressc *,\n  .ui-progressc *::before,\n  .ui-progressc *::after {\n    animation: none !important;\n    transition-duration: 1ms !important;\n    transition-delay: 0ms !important;\n  }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-progressc {\n  --ui-pgc-card: var(--aui-surface, #1c1c1e);\n  --ui-pgc-strong: var(--aui-ink, #ffffff);\n  --ui-pgc-ink: var(--aui-ink, rgba(255, 255, 255, 0.9));\n  --ui-pgc-muted: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-pgc-faint: var(--aui-placeholder, rgba(255, 255, 255, 0.3));\n  --ui-pgc-icon: var(--aui-muted, rgba(255, 255, 255, 0.5));\n  --ui-pgc-ring: var(--aui-border-strong, rgba(255, 255, 255, 0.24));\n  --ui-pgc-track: rgba(255, 255, 255, 0.08);\n  --ui-pgc-hover: var(--aui-subtle, rgba(255, 255, 255, 0.04));\n  --ui-pgc-accent: var(--aui-blue, #4fb0fe);\n  --ui-pgc-on-accent: #ffffff;\n  --ui-pgc-shine: #ffffff;\n  --ui-pgc-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n  --ui-pgc-shadow: var(--aui-shadow-card, 0 0 0 0.5px rgba(255, 255, 255, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.4));\n}\n`;\n"}]}