{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waitlist",
  "title": "Waitlist",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 3 waitlist variants: https://avaraui.com/view/waitlist. This item holds 3 of them as looks of one component (launch, card, queue). Skip the question if the user already named one. A pre-launch sign-up in three layouts: a compact card, a brand mark with a live countdown over the form, or a pill on a soft ground with the faces already in and a bar of the spots claimed. The form becomes the reader's place in line — a number that counts up, and an invite link to copy — when your promise resolves.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/avatar-stack.json",
    "https://avaraui.com/r/button.json",
    "https://avaraui.com/r/copy-button.json",
    "https://avaraui.com/r/counter.json",
    "https://avaraui.com/r/input.json",
    "https://avaraui.com/r/progress.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/WaitlistBlock.tsx",
      "content": "\"use client\";\n\nimport React, {\n  createContext,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { AlertCircle, ArrowRight, Mail } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport Button from \"./Button\";\nimport { InputBlock } from \"./InputBlock\";\nimport { AvatarStack } from \"./AvatarStackBlock\";\nimport { CopyButton } from \"./CopyButtonBlock\";\nimport { CounterBlock } from \"./CounterBlock\";\nimport { Progress } from \"./ProgressBlock\";\nimport { useCountUp } from \"./progress-hooks\";\n\n// ==========================================\n// WAITLIST\n// A pre-launch sign-up in three layouts. `card` is a compact card with the\n// field over the button; `launch` puts a brand mark and a live countdown to\n// the opening date (the Counter block's slide countdown) over the form on a\n// wide panel; `queue` sets the form as a pill on a soft ground with the faces\n// of who is already in and a bar of the spots claimed. Whichever layout, the\n// form becomes the reader's place in line when your promise resolves — a\n// number that counts up, and a link to share to move up. One reveal when the\n// section first appears; after that only the countdown's seconds and the\n// reader's own actions move anything.\n// ==========================================\n\nexport type WaitlistVariant = \"card\" | \"launch\" | \"queue\";\n\ninterface WaitlistContextValue {\n  variant: WaitlistVariant;\n  reduced: boolean;\n}\n\nconst WaitlistContext = createContext<WaitlistContextValue>({ variant: \"card\", reduced: false });\n\n// Every Reveal rises in turn — 0.05s in, then 0.1s apart, `--waitlist-i` being\n// its place in line — once the section is shown. The form gives way to the\n// thank-you, whose tick then draws itself. With reduced motion nothing moves.\nconst STYLES = `\n@keyframes waitlist-rise { from { opacity: 0; transform: translateY(16px); } }\n@keyframes waitlist-form-out { to { opacity: 0; transform: translateY(-6px); } }\n@keyframes waitlist-done-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } }\n@keyframes waitlist-check { from { stroke-dasharray: 0 1; } to { stroke-dasharray: 1 1; } }\n@media (prefers-reduced-motion: no-preference) {\n  .waitlist-reveal:not([data-shown]) [data-reveal] { opacity: 0; transform: translateY(16px); }\n  .waitlist-reveal[data-shown] [data-reveal] {\n    animation: waitlist-rise 0.65s cubic-bezier(0.16, 1, 0.3, 1) calc(0.05s + var(--waitlist-i, 0) * 0.1s) backwards;\n  }\n}\n.waitlist-form-out { animation: waitlist-form-out 0.2s cubic-bezier(0, 0, 0.58, 1) forwards; }\n.waitlist-done-in { animation: waitlist-done-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) backwards; }\n.waitlist-check { animation: waitlist-check 0.45s cubic-bezier(0, 0, 0.58, 1) 0.2s backwards; }\n`;\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReducedMotion = (onChange: () => void) => {\n  const mq = window.matchMedia(REDUCED_MOTION);\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n};\n/** The visitor's reduced-motion setting; false while server-rendering. */\nfunction useReducedMotion() {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION).matches,\n    () => false,\n  );\n}\n\nconst subscribeNothing = () => () => {};\n/** False in the server render and while hydrating, true from then on. */\nfunction useHydrated() {\n  return useSyncExternalStore(\n    subscribeNothing,\n    () => true,\n    () => false,\n  );\n}\n\n/** Animates as one child of the section's staggered reveal. */\nfunction Reveal({ className, children }: { className?: string; children: React.ReactNode }) {\n  return (\n    <div className={className} data-reveal=\"\">\n      {children}\n    </div>\n  );\n}\n\n/** The ring colour between stacked faces, matched to each layout's surface. */\nconst RING: Record<WaitlistVariant, string> = {\n  card: \"ring-card\",\n  launch: \"ring-card\",\n  queue: \"ring-background\",\n};\n\n// ------------------------------------------\n// Mark — the brand, in a square of the accent\n// ------------------------------------------\nfunction Mark({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <Reveal className=\"mb-6 flex justify-center\">\n      <div\n        className={cn(\n          \"grid size-12 place-items-center overflow-hidden rounded-[14px] bg-primary text-[1.25rem] font-semibold text-primary-foreground shadow-[0_1px_2px_rgba(0,0,0,0.08),0_8px_20px_-10px_color-mix(in_oklab,var(--primary)_60%,transparent)]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </Reveal>\n  );\n}\n\n// ------------------------------------------\n// Countdown — days, hours, minutes and seconds to the opening date\n// ------------------------------------------\nexport interface WaitlistCountdownProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** When the doors open. */\n  date: Date | string;\n  /** Line above the figures. */\n  label?: string;\n  /** What to say once the date has passed. */\n  doneLabel?: string;\n}\n\n/** The counter's slide countdown, in the section's own text colour. */\nfunction Countdown({ date, label = \"Launching in\", doneLabel = \"Now live\", className, ...props }: WaitlistCountdownProps) {\n  return (\n    <Reveal className=\"mb-8\">\n      <div className={cn(\"flex flex-col items-center\", className)} {...props}>\n        <CounterBlock type=\"countdown\" variant=\"slide\" color=\"inherit\" targetDate={date} label={label} doneLabel={doneLabel} />\n      </div>\n    </Reveal>\n  );\n}\n\n// ------------------------------------------\n// Heading, Intro\n// ------------------------------------------\nfunction Heading({ className, children, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {\n  const { variant } = useContext(WaitlistContext);\n  return (\n    <Reveal>\n      <h2\n        className={cn(\n          \"text-center font-sans font-semibold tracking-[-0.03em]\",\n          variant === \"card\"\n            ? \"text-[1.5rem] leading-[1.15]\"\n            : \"text-[2rem] leading-[1.08] sm:text-[2.75rem]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </h2>\n    </Reveal>\n  );\n}\n\nfunction Intro({ className, children, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {\n  const { variant } = useContext(WaitlistContext);\n  return (\n    <Reveal>\n      <p\n        className={cn(\n          \"mx-auto text-center leading-[1.65] opacity-65\",\n          variant === \"card\" ? \"mt-2 max-w-xs text-[14px]\" : \"mt-4 max-w-md text-[15px]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </p>\n    </Reveal>\n  );\n}\n\n// ------------------------------------------\n// Form — becomes the reader's place in line\n// ------------------------------------------\nexport interface WaitlistFormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  placeholder?: string;\n  /** Button label. */\n  action?: string;\n  /**\n   * Called on submit. Return a promise to keep the button busy. Resolve with a\n   * number to show it as the reader's place in line; resolve with nothing for\n   * the thank-you alone; throw an Error to stay on the form with its message\n   * under the field.\n   */\n  onJoin?: (email: string) => void | number | Promise<void | number>;\n  successTitle?: string;\n  successText?: string;\n  /** Word after the number, e.g. \"in line\". */\n  positionLabel?: string;\n  /** A referral link. Given, the thank-you offers to copy it. */\n  shareUrl?: string;\n  shareLabel?: string;\n  shareHint?: string;\n  /** A small line under the form — what joining means. */\n  note?: string;\n}\n\nfunction Position({ value, label }: { value: number; label: string }) {\n  const { reduced } = useContext(WaitlistContext);\n  const shown = useCountUp(value, { duration: 1400, enabled: !reduced });\n  return (\n    <p className=\"mt-3 flex items-baseline gap-2\">\n      <span className=\"text-[3rem] font-semibold leading-none tabular-nums tracking-[-0.04em] sm:text-[3.5rem]\">\n        <span className=\"opacity-30\">#</span>\n        {Math.round(shown).toLocaleString()}\n      </span>\n      <span className=\"text-[14px] font-medium opacity-55\">{label}</span>\n    </p>\n  );\n}\n\nfunction Form({\n  placeholder = \"name@email.com\",\n  action = \"Get on the list\",\n  onJoin,\n  successTitle = \"Your spot is saved\",\n  successText = \"Expect an email from us as soon as your invite opens up.\",\n  positionLabel = \"in line\",\n  shareUrl,\n  shareLabel = \"Copy referral link\",\n  shareHint = \"Each friend who joins bumps you forward.\",\n  note,\n  className,\n  ...props\n}: WaitlistFormProps) {\n  const { variant, reduced } = useContext(WaitlistContext);\n  const [email, setEmail] = useState(\"\");\n  const [busy, setBusy] = useState(false);\n  const [error, setError] = useState<string | undefined>();\n  const [done, setDone] = useState<{ position?: number } | null>(null);\n  // The form plays its way out before the thank-you comes in.\n  const [formGone, setFormGone] = useState(false);\n  const leaving = done !== null && !formGone && !reduced;\n  useEffect(() => {\n    if (!leaving) return;\n    const t = window.setTimeout(() => setFormGone(true), 300);\n    return () => window.clearTimeout(t);\n  }, [leaving]);\n\n  const submit = async (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    if (busy) return;\n    if (!email) {\n      setError(\"Add your email address first.\");\n      return;\n    }\n    setBusy(true);\n    setError(undefined);\n    try {\n      const result = await onJoin?.(email);\n      setDone({ position: typeof result === \"number\" ? result : undefined });\n    } catch (err) {\n      setError(err instanceof Error && err.message ? err.message : \"Something went wrong. Please retry.\");\n    } finally {\n      setBusy(false);\n    }\n  };\n\n  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setEmail(e.target.value);\n    if (error) setError(undefined);\n  };\n\n  const pill = variant === \"queue\";\n  const stacked = variant === \"card\";\n\n  return (\n    <Reveal className={cn(\"mx-auto w-full\", stacked ? \"mt-6\" : \"mt-8 max-w-[28rem]\")}>\n      {done && !leaving ? (\n        <div\n          key=\"done\"\n          role=\"status\"\n          className={cn(\n            !reduced && \"waitlist-done-in\",\n            \"rounded-2xl bg-card p-5 text-left text-card-foreground\",\n            stacked\n              ? \"border border-border\"\n              : \"shadow-[0_1px_2px_rgba(0,0,0,0.05),0_12px_32px_-16px_rgba(0,0,0,0.25)] dark:shadow-none dark:ring-1 dark:ring-foreground/10\",\n          )}\n        >\n          <p className=\"flex items-center gap-2.5 text-[15px] font-semibold leading-tight\">\n            <span className=\"grid size-6 shrink-0 place-items-center rounded-full bg-emerald-500 text-white\">\n              <svg viewBox=\"0 0 24 24\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth={3} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                <path d=\"M5 12.5l4.5 4.5L19 7.5\" pathLength={1} className={reduced ? undefined : \"waitlist-check\"} />\n              </svg>\n            </span>\n            {successTitle}\n          </p>\n          {done.position !== undefined && <Position value={done.position} label={positionLabel} />}\n          <p className=\"mt-2 text-[13px] leading-relaxed opacity-60\">{successText}</p>\n          {shareUrl && (\n            <div className=\"mt-4 flex flex-wrap items-center gap-x-3 gap-y-2\">\n              <CopyButton value={shareUrl} label={shareLabel} copiedLabel=\"Link copied\" variant=\"soft\" size=\"sm\" />\n              <span className=\"text-[12px] opacity-55\">{shareHint}</span>\n            </div>\n          )}\n        </div>\n      ) : (\n        <div\n          key=\"form\"\n          className={leaving ? \"waitlist-form-out\" : undefined}\n          onAnimationEnd={(e) => {\n            if (leaving && e.target === e.currentTarget) setFormGone(true);\n          }}\n        >\n          <form onSubmit={submit} noValidate className={className} {...props}>\n            {pill ? (\n              <>\n                <div\n                  className={cn(\n                    \"flex items-center gap-1 rounded-full bg-card p-1.5 transition-shadow duration-300\",\n                    \"shadow-[0_1px_2px_rgba(0,0,0,0.05),0_8px_24px_-12px_rgba(0,0,0,0.2)] focus-within:shadow-[0_1px_2px_rgba(0,0,0,0.05),0_12px_32px_-12px_rgba(0,0,0,0.28)]\",\n                    \"dark:shadow-none dark:ring-1 dark:ring-foreground/15 dark:backdrop-blur dark:focus-within:shadow-none dark:focus-within:ring-foreground/30\",\n                    error && \"ring-1 ring-red-500/60 dark:ring-red-400/60\",\n                  )}\n                >\n                  <Mail size={16} className=\"ml-3 shrink-0 opacity-40\" aria-hidden />\n                  {/* The library input with its own box made invisible, so the pill is the field. */}\n                  <InputBlock\n                    type=\"email\"\n                    variant=\"filled\"\n                    inputSize=\"md\"\n                    value={email}\n                    onChange={onChange}\n                    placeholder={placeholder}\n                    aria-label=\"Email address\"\n                    aria-invalid={error ? true : undefined}\n                    autoComplete=\"email\"\n                    className=\"min-w-0 flex-1 gap-0 [&>div>div]:h-10 [&>div>div]:rounded-full [&>div>div]:border-0 [&>div>div]:bg-transparent [&>div>div]:px-2 [&>div>div]:shadow-none [&>div>div]:focus-within:bg-transparent [&>div>div]:focus-within:ring-0 dark:[&_input]:placeholder:text-foreground/45\"\n                  />\n                  <Button\n                    type=\"submit\"\n                    variant=\"default\"\n                    size=\"sm\"\n                    loading={busy}\n                    iconRight={<ArrowRight size={14} />}\n                    className=\"h-10 shrink-0 rounded-full px-4 text-[13px] shadow-none\"\n                  >\n                    {action}\n                  </Button>\n                </div>\n                {error && (\n                  <p role=\"alert\" className=\"mt-2 flex items-center justify-center gap-1.5 text-[12px] text-red-600 dark:text-red-400\">\n                    <AlertCircle size={13} /> {error}\n                  </p>\n                )}\n              </>\n            ) : (\n              <div className={cn(\"flex gap-3\", stacked ? \"flex-col\" : \"flex-col sm:flex-row sm:items-start\")}>\n                {/* The library input owns the error line, so a wrong address is reported beside the field. */}\n                <InputBlock\n                  type=\"email\"\n                  inputSize=\"md\"\n                  value={email}\n                  onChange={onChange}\n                  placeholder={placeholder}\n                  error={error}\n                  disabled={busy}\n                  aria-label=\"Email address\"\n                  autoComplete=\"email\"\n                  className={cn(\"min-w-0\", stacked ? \"w-full\" : \"flex-1\")}\n                />\n                <Button\n                  type=\"submit\"\n                  variant=\"default\"\n                  size=\"default\"\n                  loading={busy}\n                  iconRight={<ArrowRight size={15} />}\n                  className={cn(\"h-11 shrink-0\", stacked ? \"w-full\" : \"w-full sm:w-auto\")}\n                >\n                  {action}\n                </Button>\n              </div>\n            )}\n            {note && <p className=\"mt-3 text-center text-[12px] leading-relaxed opacity-50\">{note}</p>}\n          </form>\n        </div>\n      )}\n    </Reveal>\n  );\n}\n\n// ------------------------------------------\n// Proof — the faces of who is already in, and a line\n// ------------------------------------------\nexport interface WaitlistProofProps extends React.HTMLAttributes<HTMLDivElement> {\n  avatars: string[];\n}\n\nfunction Proof({ avatars, className, children, ...props }: WaitlistProofProps) {\n  const { variant } = useContext(WaitlistContext);\n  return (\n    <Reveal>\n      <div\n        className={cn(\n          \"flex items-center justify-center gap-3 text-[13px] font-medium text-foreground/65\",\n          variant === \"card\" ? \"mt-5\" : \"mt-7\",\n          className,\n        )}\n        {...props}\n      >\n        <AvatarStack\n          size=\"sm\"\n          max={4}\n          spreadOnHover={false}\n          ringClassName={RING[variant]}\n          className=\"m-0 p-0\"\n          avatars={avatars.map((src) => ({ src }))}\n        />\n        <span>{children}</span>\n      </div>\n    </Reveal>\n  );\n}\n\n// ------------------------------------------\n// Spots — how many early places are gone\n// ------------------------------------------\nexport interface WaitlistSpotsProps extends React.HTMLAttributes<HTMLDivElement> {\n  claimed: number;\n  total: number;\n  /** The line over the bar. Defaults to \"{claimed} of {total} early places taken\". */\n  children?: React.ReactNode;\n}\n\nfunction Spots({ claimed, total, className, children, ...props }: WaitlistSpotsProps) {\n  const { reduced } = useContext(WaitlistContext);\n  const shown = useCountUp(claimed, { duration: 1400, enabled: !reduced, range: total });\n  return (\n    <Reveal className={cn(\"mx-auto mt-9 w-full max-w-[22rem]\", className)}>\n      <div {...props}>\n        <p className=\"mb-2.5 flex items-baseline justify-between text-[13px] font-medium\">\n          <span className=\"tabular-nums\">\n            {children ?? (\n              <>\n                <span className=\"text-[15px] font-semibold\">{Math.round(shown).toLocaleString()}</span>\n                <span className=\"opacity-55\"> of {total.toLocaleString()} early places taken</span>\n              </>\n            )}\n          </span>\n        </p>\n        <Progress value={claimed} max={total} barSize=\"sm\" animate={!reduced} duration={1400} />\n      </div>\n    </Reveal>\n  );\n}\n\n// ------------------------------------------\n// Root\n// ------------------------------------------\nexport interface WaitlistProps extends React.HTMLAttributes<HTMLElement> {\n  variant?: WaitlistVariant;\n  /** Accent for the mark, button and bar; defaults to the theme's primary. */\n  accent?: string;\n  reveal?: \"in-view\" | \"immediate\";\n  children: React.ReactNode;\n}\n\nexport function Waitlist({\n  variant = \"card\",\n  accent,\n  reveal = \"in-view\",\n  className,\n  style,\n  children,\n  ...props\n}: WaitlistProps) {\n  const reduced = useReducedMotion();\n  const containerRef = useRef<HTMLDivElement>(null);\n  const hydrated = useHydrated();\n  const [shown, setShown] = useState(false);\n\n  // Each Reveal's place in line, in the order they are on the page — the\n  // parts can come in any order.\n  useLayoutEffect(() => {\n    containerRef.current?.querySelectorAll<HTMLElement>(\"[data-reveal]\").forEach((el, i) => {\n      el.style.setProperty(\"--waitlist-i\", String(i));\n    });\n  });\n\n  // Shown on mount (never in the server HTML, so the rise still plays), or\n  // the first time 30% of it is in view. Once shown it stays shown.\n  if (reveal === \"immediate\" && hydrated && !shown) setShown(true);\n  useEffect(() => {\n    const el = containerRef.current;\n    if (reveal === \"immediate\" || shown || !el) return;\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry?.isIntersecting) return;\n        setShown(true);\n        io.disconnect();\n      },\n      { threshold: 0.3 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [reveal, shown]);\n\n  return (\n    <WaitlistContext.Provider value={{ variant, reduced }}>\n      <section\n        className={cn(\n          \"relative isolate w-full text-foreground\",\n          // `queue` paints its own soft ground; the other two are a card or a\n          // panel on whatever the page provides.\n          variant === \"queue\" && \"overflow-hidden bg-background\",\n          className,\n        )}\n        style={accent ? ({ ...style, \"--primary\": accent } as React.CSSProperties) : style}\n        {...props}\n      >\n        <style>{STYLES}</style>\n        <div\n          ref={containerRef}\n          data-shown={shown || undefined}\n          className={cn(\n            \"waitlist-reveal mx-auto flex flex-col items-stretch\",\n            variant === \"card\" &&\n              \"max-w-sm rounded-2xl border border-border bg-card p-6 shadow-[0_1px_2px_rgba(0,0,0,0.04),0_10px_30px_-18px_rgba(0,0,0,0.25)] dark:shadow-none sm:p-7\",\n            variant === \"launch\" &&\n              \"max-w-4xl rounded-[28px] border border-border bg-card px-6 py-12 sm:px-12 sm:py-16\",\n            variant === \"queue\" && \"max-w-xl px-6 py-16 sm:py-24\",\n          )}\n        >\n          {variant === \"launch\" ? <div className=\"mx-auto flex w-full max-w-xl flex-col items-stretch\">{children}</div> : children}\n        </div>\n      </section>\n    </WaitlistContext.Provider>\n  );\n}\n\nWaitlist.Mark = Mark;\nWaitlist.Countdown = Countdown;\nWaitlist.Heading = Heading;\nWaitlist.Intro = Intro;\nWaitlist.Form = Form;\nWaitlist.Proof = Proof;\nWaitlist.Spots = Spots;\n\nexport default Waitlist;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/WaitlistBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/progress-hooks.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\n\n/** One easing for the fill and the readout, so they can never drift apart. */\nconst easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReducedMotion = (onChange: () => void) => {\n  if (typeof window === \"undefined\" || !window.matchMedia) return () => {};\n  const mq = window.matchMedia(REDUCED_MOTION);\n  mq.addEventListener?.(\"change\", onChange);\n  return () => mq.removeEventListener?.(\"change\", onChange);\n};\nconst prefersReducedMotion = () =>\n  typeof window !== \"undefined\" && window.matchMedia?.(REDUCED_MOTION).matches === true;\n/** The visitor's reduced-motion setting; false while server-rendering. */\nconst usePrefersReducedMotion = () =>\n  useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, () => false);\n\nexport interface CountUpOptions {\n  /** Length of the first run, from zero, in ms. Default 1200. */\n  duration?: number;\n  /** `false` returns `target` untouched. */\n  enabled?: boolean;\n  /**\n   * The scale later changes are measured against (the bar's `max`). A change\n   * across the whole range takes `duration`; a 1% tick takes a sliver of it.\n   */\n  range?: number;\n}\n\n/**\n * Tweens a number towards `target`. It starts at 0 on mount — so the bar\n * fills and the readout counts up when it first appears — then follows every\n * later change from wherever it currently is, so a stream of small updates\n * never jumps. The mount run always takes the full `duration`; later runs\n * take a slice proportional to how far they travel, floored so a tiny nudge\n * still visibly moves. Under `prefers-reduced-motion` it returns `target`.\n */\nexport function useCountUp(\n  target: number,\n  { duration = 1200, enabled = true, range }: CountUpOptions = {},\n): number {\n  const reduced = usePrefersReducedMotion();\n  const snap = !enabled || reduced;\n  const [current, setCurrent] = useState(enabled ? 0 : target);\n  // While snapping, the value sits on `target`, so a later run starts from it.\n  if (snap && current !== target) setCurrent(target);\n  const currentRef = useRef(current);\n  // Flipped by the first frame that actually moves, not by the effect — a\n  // run that is cancelled before it paints (strict mode) is not a mount run.\n  const movedRef = useRef(false);\n\n  useEffect(() => {\n    if (snap) {\n      currentRef.current = target;\n      return;\n    }\n    const from = currentRef.current;\n    const delta = target - from;\n    if (delta === 0) return;\n\n    const scale = Math.max(Math.abs(range ?? target), Math.abs(from), 1);\n    const ms = movedRef.current\n      ? Math.max(160, duration * Math.min(1, Math.abs(delta) / scale))\n      : duration;\n\n    let raf = 0;\n    const start = performance.now();\n    const tick = (now: number) => {\n      const t = Math.min(1, (now - start) / ms);\n      const next = t >= 1 ? target : from + delta * easeOutCubic(t);\n      currentRef.current = next;\n      movedRef.current = true;\n      setCurrent(next);\n      if (t < 1) raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [target, duration, snap, range]);\n\n  return snap ? target : current;\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/progress-hooks.ts"
    }
  ],
  "docs": "Named export: Waitlist, with parts Mark, Countdown, Heading, Intro, Form, Proof and Spots. Composed from the counter, input, button, avatar-stack, progress and copy-button items, which install alongside it. `variant` is card | launch | queue, each styled for light and dark mode; `Countdown` takes a `date` and is the counter's slide countdown; `Form` takes `onJoin(email)` — resolve a number to show it as the place in line, throw an Error to show its message — plus `action`, `shareUrl`, `note` and the success copy; `Spots` takes `claimed` and `total`; `accent` pins the colour; `reveal` is in-view or immediate.",
  "categories": [
    "marketing",
    "forms"
  ],
  "type": "registry:component"
}
