{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "counter",
  "title": "Counter",
  "author": "AlgorizeTech",
  "description": "A number component in four modes — stepper, count-up, circular progress and countdown. Use it for quantity inputs and animated statistics.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/CounterBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Clock, Minus, Plus } from \"lucide-react\";\nimport React, { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { getThemeStyles, type CounterTheme } from \"./counter-theme\";\n\n// ==========================================\n// DYNAMIC THEME ENGINE\n// ==========================================\nexport type CounterColor = \"indigo\" | \"rose\" | \"emerald\" | \"zinc\" | string;\nexport type CounterType = \"stepper\" | \"animated\" | \"circular\" | \"countdown\";\n/**\n * `outline`, `soft` and `vertical` are stepper layouts; `slide` is the\n * countdown's tile design, where a figure that changes slides out above as\n * the new one comes up from below.\n */\nexport type CounterVariant = \"default\" | \"outline\" | \"soft\" | \"vertical\" | \"slide\";\n\n// The slide countdown's keyframes ship inline so the block stays\n// self-contained — no plugin, no globals.css entry.\nconst KEYFRAMES = `\n@keyframes counter-slide-in {\n  from { transform: translateY(0.7em); opacity: 0; }\n  to { transform: none; opacity: 1; }\n}\n@keyframes counter-slide-out {\n  from { transform: none; opacity: 1; }\n  to { transform: translateY(-0.7em); opacity: 0; }\n}`;\n\n// ==========================================\n// MOTION\n// ==========================================\n\n/** GSAP's `back.out(2)` — a cubic, so this cubic-bezier is exact. */\nconst POP_EASE = \"cubic-bezier(0.3333, 1.6667, 0.6667, 1)\";\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. */\nconst useReducedMotion = () =>\n  useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia?.(REDUCED_MOTION).matches ?? false,\n    () => false,\n  );\n\n/** GSAP's `power3.out`. */\nconst power3Out = (p: number) => 1 - Math.pow(1 - p, 4);\n\n/**\n * Tweens 0 → `to` over `duration` ms on requestAnimationFrame, calling\n * `onUpdate` every frame with the eased value. Returns a cancel function.\n */\nfunction tweenNumber(\n  to: number,\n  duration: number,\n  onUpdate: (value: number) => void,\n) {\n  const start = performance.now();\n  let frame = requestAnimationFrame(function tick(now) {\n    const p = Math.min(Math.max((now - start) / duration, 0), 1);\n    onUpdate(power3Out(p) * to);\n    if (p < 1) frame = requestAnimationFrame(tick);\n  });\n  return () => cancelAnimationFrame(frame);\n}\n\n// ==========================================\n// INTERNAL SUB-COMPONENTS\n// ==========================================\n\n/** What every counter variant needs to paint itself. */\ninterface CounterBaseProps {\n  /** A row out of the block’s colour table. */\n  theme: CounterTheme;\n  style?: React.CSSProperties;\n  label?: string;\n}\n\ninterface StepperCounterProps extends CounterBaseProps {\n  initialValue: number;\n  min: number;\n  max: number;\n  variant?: CounterVariant;\n}\n\ninterface AnimatedCounterProps extends CounterBaseProps {\n  targetValue: number;\n  prefix?: string;\n  suffix?: string;\n  /** Just the figure, no card — it inherits the surrounding type. */\n  bare?: boolean;\n  /** Decimal places to show while counting, e.g. 1 for 5.5. */\n  decimals?: number;\n  /** Wait until the counter is on screen before counting. */\n  startOnView?: boolean;\n  className?: string;\n}\n\ninterface CircularCounterProps extends CounterBaseProps {\n  targetValue: number;\n  suffix?: string;\n}\n\ninterface CountdownCounterProps extends CounterBaseProps {\n  targetDate?: string | number | Date;\n  /** What the label becomes once the date has passed. */\n  doneLabel?: string;\n  variant?: CounterVariant;\n}\n\nconst StepperCounter = ({\n  theme,\n  style,\n  label,\n  initialValue,\n  min,\n  max,\n  variant,\n}: StepperCounterProps) => {\n  const [count, setCount] = useState(initialValue);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const popRef = useRef<Animation | null>(null);\n\n  // Each step restarts the pop from 0.95.\n  const animatePop = () => {\n    const el = containerRef.current;\n    if (!el?.animate) return;\n    popRef.current?.cancel();\n    popRef.current = el.animate(\n      [{ transform: \"scale(0.95)\" }, { transform: \"scale(1)\" }],\n      { duration: 200, easing: POP_EASE },\n    );\n  };\n\n  const handleDec = () => {\n    if (count > min) {\n      setCount((p: number) => p - 1);\n      animatePop();\n    }\n  };\n  const handleInc = () => {\n    if (count < max) {\n      setCount((p: number) => p + 1);\n      animatePop();\n    }\n  };\n\n  const renderVariant = () => {\n    switch (variant) {\n      case \"outline\":\n        return (\n          <div\n            ref={containerRef}\n            className=\"flex items-center rounded-full border-2 border-border p-1 bg-background\"\n          >\n            <button\n              onClick={handleDec}\n              disabled={count <= min}\n              className={cn(\n                \"w-10 h-10 flex items-center justify-center rounded-full transition-colors active:scale-95 disabled:opacity-50\",\n                theme.bg,\n              )}\n            >\n              <Minus size={18} strokeWidth={2.5} />\n            </button>\n            <div className=\"w-14 text-center text-xl font-semibold text-foreground select-none\">\n              {count}\n            </div>\n            <button\n              onClick={handleInc}\n              disabled={count >= max}\n              className={cn(\n                \"w-10 h-10 flex items-center justify-center rounded-full transition-colors active:scale-95 disabled:opacity-50\",\n                theme.bg,\n              )}\n            >\n              <Plus size={18} strokeWidth={2.5} />\n            </button>\n          </div>\n        );\n      case \"soft\":\n        return (\n          <div\n            ref={containerRef}\n            className={cn(\n              \"flex items-center rounded-2xl p-1.5 border border-border/50\",\n              theme.bgSoft,\n            )}\n          >\n            <button\n              onClick={handleDec}\n              disabled={count <= min}\n              className={cn(\n                \"w-10 h-10 flex items-center justify-center rounded-xl bg-card border border-border/40 hover:scale-105 active:scale-95 transition-all disabled:opacity-50\",\n                theme.text,\n              )}\n            >\n              <Minus size={18} strokeWidth={3} />\n            </button>\n            <div\n              className={cn(\n                \"w-16 text-center text-2xl font-bold select-none\",\n                theme.text,\n              )}\n            >\n              {count}\n            </div>\n            <button\n              onClick={handleInc}\n              disabled={count >= max}\n              className={cn(\n                \"w-10 h-10 flex items-center justify-center rounded-xl bg-card border border-border/40 hover:scale-105 active:scale-95 transition-all disabled:opacity-50\",\n                theme.text,\n              )}\n            >\n              <Plus size={18} strokeWidth={3} />\n            </button>\n          </div>\n        );\n      case \"vertical\":\n        return (\n          <div\n            ref={containerRef}\n            className=\"flex flex-col items-center bg-muted border border-border rounded-2xl overflow-hidden\"\n          >\n            <button\n              onClick={handleInc}\n              disabled={count >= max}\n              className={cn(\n                \"w-14 h-12 flex justify-center items-center hover:bg-muted transition-colors active:bg-muted disabled:opacity-50\",\n                theme.text,\n              )}\n            >\n              <Plus size={20} strokeWidth={2.5} />\n            </button>\n            <div className=\"w-14 py-3 text-center font-bold text-xl border-y border-border bg-background select-none\">\n              {count}\n            </div>\n            <button\n              onClick={handleDec}\n              disabled={count <= min}\n              className={cn(\n                \"w-14 h-12 flex justify-center items-center hover:bg-muted transition-colors active:bg-muted disabled:opacity-50\",\n                theme.text,\n              )}\n            >\n              <Minus size={20} strokeWidth={2.5} />\n            </button>\n          </div>\n        );\n      case \"default\":\n      default:\n        return (\n          <div\n            ref={containerRef}\n            className=\"flex items-center bg-card border border-border rounded-xl overflow-hidden p-1\"\n          >\n            <button\n              onClick={handleDec}\n              disabled={count <= min}\n              className={cn(\n                \"w-10 h-10 flex items-center justify-center rounded-lg transition-colors active:scale-95 disabled:opacity-50 border border-foreground/10\",\n                theme.bg,\n              )}\n            >\n              <Minus size={18} strokeWidth={2.5} />\n            </button>\n            <div className=\"w-16 text-center text-xl font-semibold text-foreground select-none\">\n              {count}\n            </div>\n            <button\n              onClick={handleInc}\n              disabled={count >= max}\n              className={cn(\n                \"w-10 h-10 flex items-center justify-center rounded-lg transition-colors active:scale-95 disabled:opacity-50 border border-foreground/10\",\n                theme.bg,\n              )}\n            >\n              <Plus size={18} strokeWidth={2.5} />\n            </button>\n          </div>\n        );\n    }\n  };\n\n  return (\n    <div className=\"flex flex-col items-center gap-3\" style={style}>\n      {label && (\n        <span className=\"text-sm font-semibold text-muted-foreground uppercase tracking-widest\">\n          {label}\n        </span>\n      )}\n      {renderVariant()}\n    </div>\n  );\n};\n\nconst AnimatedCounter = ({\n  theme,\n  style,\n  label,\n  targetValue,\n  prefix,\n  suffix,\n  bare = false,\n  decimals = 0,\n  startOnView = false,\n  className,\n}: AnimatedCounterProps) => {\n  const [displayVal, setDisplayVal] = useState(0);\n  const [armed, setArmed] = useState(!startOnView);\n  const rootRef = useRef<HTMLDivElement | HTMLSpanElement>(null);\n\n  // With `startOnView` the count waits for the element to be visible.\n  useEffect(() => {\n    if (armed) return;\n    const el = rootRef.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setArmed(true);\n      return;\n    }\n    const io = new IntersectionObserver(\n      (entries) => {\n        if (entries.some((e) => e.isIntersecting)) {\n          setArmed(true);\n          io.disconnect();\n        }\n      },\n      { threshold: 0.3 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [armed]);\n\n  // Reduced motion skips the tween: once armed, the final figure shows as is.\n  const reduced = useReducedMotion();\n  useEffect(() => {\n    if (!armed || reduced) return;\n    return tweenNumber(targetValue, 2000, setDisplayVal);\n  }, [armed, reduced, targetValue]);\n\n  const shownVal = armed && reduced ? targetValue : displayVal;\n  const formatted = shownVal.toLocaleString(undefined, {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  });\n\n  if (bare) {\n    return (\n      <span ref={rootRef as React.RefObject<HTMLSpanElement>} className={cn(\"tabular-nums\", className)} style={style}>\n        {prefix}\n        {formatted}\n        {suffix}\n      </span>\n    );\n  }\n\n  return (\n    <div\n      ref={rootRef as React.RefObject<HTMLDivElement>}\n      className={cn(\n        \"flex flex-col items-center justify-center p-8 bg-card border border-border rounded-2xl w-full max-w-sm\",\n        className,\n      )}\n      style={style}\n    >\n      <div\n        className={cn(\"text-6xl font-bold tracking-tighter mb-2\", theme.text)}\n      >\n        {prefix}\n        {formatted}\n        {suffix}\n      </div>\n      {label && (\n        <div className=\"text-sm font-semibold text-muted-foreground uppercase tracking-widest\">\n          {label}\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst CircularCounter = ({\n  theme,\n  style,\n  label,\n  targetValue,\n  suffix,\n}: CircularCounterProps) => {\n  const [displayVal, setDisplayVal] = useState(0);\n  const circleRef = useRef<SVGCircleElement>(null);\n  const size = 160;\n  const strokeWidth = 12;\n  const radius = (size - strokeWidth) / 2;\n  const circumference = radius * 2 * Math.PI;\n\n  useEffect(() => {\n    return tweenNumber(targetValue, 2500, (val) => {\n      setDisplayVal(Math.floor(val));\n      if (circleRef.current) {\n        const percent = Math.min(100, Math.max(0, val));\n        const offset = circumference - (percent / 100) * circumference;\n        circleRef.current.style.strokeDashoffset = offset.toString();\n      }\n    });\n  }, [targetValue, circumference]);\n\n  return (\n    <div className=\"flex flex-col items-center gap-6\" style={style}>\n      <div\n        className=\"relative flex items-center justify-center\"\n        style={{ width: size, height: size }}\n      >\n        <svg\n          className=\"absolute inset-0 rotate-[-90deg]\"\n          width={size}\n          height={size}\n        >\n          <circle\n            cx={size / 2}\n            cy={size / 2}\n            r={radius}\n            className=\"fill-none stroke-muted\"\n            strokeWidth={strokeWidth}\n          />\n          <circle\n            ref={circleRef}\n            cx={size / 2}\n            cy={size / 2}\n            r={radius}\n            className={cn(\"fill-none transition-all duration-75\", theme.fill)}\n            strokeWidth={strokeWidth}\n            strokeLinecap=\"round\"\n            strokeDasharray={circumference}\n            strokeDashoffset={circumference}\n          />\n        </svg>\n        <div className=\"absolute flex flex-col items-center justify-center text-center\">\n          <span className=\"text-3xl font-bold tracking-tighter text-foreground\">\n            {displayVal}\n            {suffix}\n          </span>\n        </div>\n      </div>\n      {label && (\n        <span className=\"text-sm font-semibold text-muted-foreground uppercase tracking-widest\">\n          {label}\n        </span>\n      )}\n    </div>\n  );\n};\n\ntype Remaining = { d: number; h: number; m: number; s: number };\n\nconst ZERO: Remaining = { d: 0, h: 0, m: 0, s: 0 };\n\nfunction remainingUntil(target: number): Remaining | null {\n  const diff = target - Date.now();\n  if (diff <= 0) return null;\n  const total = Math.floor(diff / 1000);\n  return {\n    d: Math.floor(total / 86400),\n    h: Math.floor((total % 86400) / 3600),\n    m: Math.floor((total % 3600) / 60),\n    s: total % 60,\n  };\n}\n\n/**\n * Ticks once a second. `undefined` until the first client tick, so the server\n * and the first client paint agree; `null` once the date has passed. With no\n * date it counts down three days from mount.\n */\nfunction useCountdown(targetDate?: string | number | Date) {\n  const target = targetDate === undefined ? undefined : new Date(targetDate).getTime();\n  const [left, setLeft] = useState<Remaining | null | undefined>(undefined);\n\n  useEffect(() => {\n    const at = target ?? Date.now() + 3 * 24 * 60 * 60 * 1000;\n    const tick = () => setLeft(remainingUntil(at));\n    tick();\n    const id = window.setInterval(tick, 1000);\n    return () => window.clearInterval(id);\n  }, [target]);\n\n  return left;\n}\n\nconst CountdownCounter = ({\n  theme,\n  style,\n  label,\n  targetDate,\n  doneLabel,\n  variant,\n}: CountdownCounterProps) => {\n  const left = useCountdown(targetDate);\n  const over = left === null;\n  const caption = over ? doneLabel : label;\n\n  if (variant === \"slide\") {\n    return (\n      <div className=\"flex flex-col items-center\" style={style}>\n        <style>{KEYFRAMES}</style>\n        {caption && <p className=\"mb-3 text-[13px] font-medium opacity-60\">{caption}</p>}\n        {!over && (\n          <div className=\"flex items-center gap-2\" role=\"timer\" aria-live=\"off\">\n            <SlideFigure themeText={theme.text} value={left?.d ?? null} unit=\"days\" />\n            <SlideFigure themeText={theme.text} value={left?.h ?? null} unit=\"hours\" />\n            <SlideFigure themeText={theme.text} value={left?.m ?? null} unit=\"minutes\" />\n            <SlideFigure themeText={theme.text} value={left?.s ?? null} unit=\"seconds\" />\n          </div>\n        )}\n      </div>\n    );\n  }\n\n  const timeLeft = left ?? ZERO;\n\n  return (\n    <div className=\"flex flex-col items-center gap-8\" style={style}>\n      {caption && (\n        <div className=\"flex items-center gap-2 px-4 py-1.5 rounded-full bg-muted border border-border text-sm font-semibold text-muted-foreground\">\n          <Clock size={16} className={theme.text} /> {caption}\n        </div>\n      )}\n      <div className=\"flex gap-3 sm:gap-4\">\n        <TimeUnit themeText={theme.text} value={timeLeft.d} unit=\"Days\" />\n        <TimeUnit themeText={theme.text} value={timeLeft.h} unit=\"Hours\" />\n        <TimeUnit themeText={theme.text} value={timeLeft.m} unit=\"Mins\" />\n        <TimeUnit themeText={theme.text} value={timeLeft.s} unit=\"Secs\" />\n      </div>\n    </div>\n  );\n};\n\n// ==========================================\n// MAIN EXPORT\n// ==========================================\n\nexport interface CounterProps {\n  type?: CounterType;\n  /** Stepper layouts, plus `slide` for the countdown. */\n  variant?: CounterVariant;\n  color?: CounterColor;\n  label?: string;\n  initialValue?: number;\n  min?: number;\n  max?: number;\n  targetValue?: number;\n  prefix?: string;\n  suffix?: string;\n  /** Countdown only: when it reaches zero. Defaults to three days from mount. */\n  targetDate?: Date | string | number;\n  /** Countdown only: what the label becomes once the date has passed. */\n  doneLabel?: string;\n  /** Animated only: just the figure, no card — it inherits the surrounding type. */\n  bare?: boolean;\n  /** Animated only: decimal places, e.g. 1 for 5.5. */\n  decimals?: number;\n  /** Animated only: wait until the counter is on screen before counting. */\n  startOnView?: boolean;\n  /** Animated only: classes for the figure (bare) or the card. */\n  className?: string;\n}\n\n/**\n * Defined at module scope on purpose: declared inside CounterBlock it would be\n * a new component type on every tick, remounting all four digits each second.\n */\nfunction TimeUnit({\n  value,\n  unit,\n  themeText,\n}: {\n  value: number;\n  unit: string;\n  themeText: string;\n}) {\n  return (\n    <div className=\"flex flex-col items-center\">\n      <div className=\"w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center bg-card border border-border rounded-2xl mb-3\">\n        <span\n          className={cn(\n            \"text-2xl sm:text-3xl font-bold tracking-tighter\",\n            themeText,\n          )}\n        >\n          {value.toString().padStart(2, \"0\")}\n        </span>\n      </div>\n      <span className=\"text-[10px] sm:text-xs font-semibold text-muted-foreground uppercase tracking-widest\">\n        {unit}\n      </span>\n    </div>\n  );\n}\n\n/** One tile of the slide countdown. `null` paints \"--\" before the first tick. */\nfunction SlideFigure({\n  value,\n  unit,\n  themeText,\n}: {\n  value: number | null;\n  unit: string;\n  themeText: string;\n}) {\n  const text = value === null ? \"--\" : String(value).padStart(2, \"0\");\n  // A figure that changes keeps its old text mounted for one pass so it can\n  // slide out above while the new one comes up from below; the others hold\n  // still. Nothing moves on the first paint.\n  const [shown, setShown] = useState({ current: text, previous: null as string | null });\n  if (shown.current !== text) setShown({ current: text, previous: shown.current });\n\n  return (\n    <div className=\"flex min-w-[3.75rem] flex-col items-center rounded-xl bg-foreground/[0.06] px-3 py-2.5\">\n      <span\n        className={cn(\n          \"relative block h-[1.75rem] overflow-hidden text-[1.5rem] font-semibold leading-[1.75rem] tabular-nums tracking-[-0.03em]\",\n          themeText,\n        )}\n      >\n        {shown.previous !== null && (\n          <span\n            key={`out-${shown.previous}`}\n            aria-hidden\n            className=\"absolute inset-0 block animate-[counter-slide-out_0.32s_cubic-bezier(0.16,1,0.3,1)_both] motion-reduce:hidden\"\n            onAnimationEnd={() => setShown((s) => ({ ...s, previous: null }))}\n          >\n            {shown.previous}\n          </span>\n        )}\n        <span\n          className={cn(\n            \"block\",\n            shown.previous !== null &&\n              \"animate-[counter-slide-in_0.32s_cubic-bezier(0.16,1,0.3,1)_both] motion-reduce:animate-none\",\n          )}\n        >\n          {text}\n        </span>\n      </span>\n      <span className=\"mt-1 text-[11px] font-medium opacity-55\">{unit}</span>\n    </div>\n  );\n}\n\nexport function CounterBlock({\n  type = \"stepper\",\n  variant = \"default\",\n  color = \"primary\",\n  label = \"Counter\",\n  initialValue = 1,\n  min = 0,\n  max = 100,\n  targetValue = 100,\n  prefix = \"\",\n  suffix = \"\",\n  targetDate,\n  doneLabel = \"Time's up\",\n  bare,\n  decimals,\n  startOnView,\n  className,\n}: CounterProps) {\n  const { theme, style } = getThemeStyles(color);\n\n  if (type === \"stepper\")\n    return (\n      <StepperCounter\n        theme={theme}\n        style={style}\n        label={label}\n        initialValue={initialValue}\n        min={min}\n        max={max}\n        variant={variant}\n      />\n    );\n  if (type === \"animated\")\n    return (\n      <AnimatedCounter\n        theme={theme}\n        style={style}\n        label={label}\n        targetValue={targetValue}\n        prefix={prefix}\n        suffix={suffix}\n        {...(bare !== undefined ? { bare } : {})}\n        {...(decimals !== undefined ? { decimals } : {})}\n        {...(startOnView !== undefined ? { startOnView } : {})}\n        {...(className !== undefined ? { className } : {})}\n      />\n    );\n  if (type === \"circular\")\n    return (\n      <CircularCounter\n        theme={theme}\n        style={style}\n        label={label}\n        targetValue={targetValue}\n        suffix={suffix}\n      />\n    );\n  if (type === \"countdown\")\n    return (\n      <CountdownCounter\n        theme={theme}\n        style={style}\n        label={label}\n        variant={variant}\n        doneLabel={doneLabel}\n        {...(targetDate !== undefined ? { targetDate } : {})}\n      />\n    );\n\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/CounterBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/counter-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\n/** The class set one colour gives the block. */\nexport interface CounterTheme {\n  bg: string;\n  text: string;\n  border: string;\n  ring: string;\n  fill: string;\n  bgSoft: string;\n}\n\nconst PRIMARY: CounterTheme = {\n  bg: \"bg-primary text-primary-foreground\",\n  text: \"text-primary\",\n  border: \"border-primary/30\",\n  ring: \"ring-primary/20\",\n  fill: \"stroke-primary\",\n  bgSoft: \"bg-primary/10 text-primary\",\n};\n\n/** Takes the surrounding text colour, for a counter inside a section that sets its own. */\nconst INHERIT: CounterTheme = {\n  bg: \"bg-current text-background\",\n  text: \"text-current\",\n  border: \"border-current\",\n  ring: \"ring-current/20\",\n  fill: \"stroke-current\",\n  bgSoft: \"bg-current/10 text-current\",\n};\n\nconst colorThemes: Record<string, CounterTheme> = {\n  primary: PRIMARY,\n  inherit: INHERIT,\n  indigo: {\n    bg: \"bg-indigo-600 text-white\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    border: \"border-indigo-600\",\n    ring: \"ring-indigo-600/20\",\n    fill: \"stroke-indigo-600\",\n    bgSoft:\n      \"bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400\",\n  },\n  rose: {\n    bg: \"bg-rose-600 text-white\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    border: \"border-rose-600\",\n    ring: \"ring-rose-600/20\",\n    fill: \"stroke-rose-600\",\n    bgSoft: \"bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400\",\n  },\n  emerald: {\n    bg: \"bg-emerald-600 text-white\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    border: \"border-emerald-600\",\n    ring: \"ring-emerald-600/20\",\n    fill: \"stroke-emerald-600\",\n    bgSoft:\n      \"bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\",\n  },\n  zinc: {\n    bg: \"bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    border: \"border-zinc-900 dark:border-zinc-100\",\n    ring: \"ring-zinc-900/20 dark:ring-white/10\",\n    fill: \"stroke-zinc-900 dark:stroke-zinc-100\",\n    bgSoft: \"bg-zinc-100 dark:bg-zinc-800/50 text-zinc-900 dark:text-zinc-100\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1] ?? \"63\", 16)}, ${parseInt(result[2] ?? \"66\", 16)}, ${parseInt(result[3] ?? \"f1\", 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string): { theme: CounterTheme; style: React.CSSProperties } => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        bg: \"bg-[var(--theme-color)] text-white\",\n        text: \"text-[var(--theme-color)]\",\n        border: \"border-[var(--theme-color)]\",\n        ring: \"ring-[var(--theme-color)]/20\",\n        fill: \"stroke-[var(--theme-color)]\",\n        bgSoft:\n          \"bg-[rgba(var(--theme-color-rgb),0.1)] text-[var(--theme-color)]\",\n      },\n      style: {\n        \"--theme-color\": color,\n        \"--theme-color-rgb\": rgb,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] ?? PRIMARY, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/counter-theme.ts"
    }
  ],
  "docs": "Named exports: CounterBlock, getThemeStyles (from counter-theme.ts, which installs alongside). `type` is stepper | animated | circular | countdown — stepper is an input with min/max, the others are display-only. Variants: default | outline | soft | vertical for the stepper; `slide` for the countdown (soft tiles, only the figure that changed slides — the same countdown the waitlist item uses). The countdown takes `targetDate` (Date, ISO string or timestamp) and `doneLabel`. The animated type also takes `bare` (just the figure, no card), `decimals`, `startOnView` (count once visible) and `className`. `color` accepts a theme name, a hex code or `inherit`.",
  "categories": [
    "data-display",
    "forms"
  ],
  "type": "registry:component"
}
