{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wire",
  "type": "registry:ui",
  "title": "Wire",
  "description": "A self-looping activity feed for marketing heroes: entries land with one confident spring and a hairline flash that cools in 400ms, siblings shuffle down, and the oldest retires under an edge fade — all on seeded, jittered news-wire cadence. Decorative counterpart to dispatch; hover pauses the feed.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json",
    "https://crucibleui.com/r/use-visibility-pause.json"
  ],
  "files": [
    {
      "path": "registry/default/data/wire/wire.tsx",
      "type": "registry:ui",
      "target": "components/crucible/wire.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\nexport interface WireItem {\n  /** Small glyph rendered inside the accent chip (inline SVG works best). */\n  icon?: React.ReactNode;\n  /** Headline of the event — also used for the item's aria-label. */\n  title: string;\n  /** Secondary meta line under the title. */\n  description?: string;\n  /** Short timestamp string, e.g. \"just now\", \"2m ago\". */\n  time?: string;\n  /** Accent for this item's icon chip; falls back to the `colors` cycle. */\n  accent?: string;\n}\n\n/** Imperative handle for live-push mode — see `WireProps.ref`. */\nexport interface WireHandle {\n  /** Push a live item into the feed. Works alongside or instead of the loop. */\n  push: (item: WireItem) => void;\n}\n\nexport interface WireProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /**\n   * Events the feed cycles through. The loop order is a seeded shuffle of this\n   * array (deterministic per `seed`), so the cadence never reads scripted.\n   * @default a built-in set of generic product events\n   */\n  items?: WireItem[];\n  /** Mean seconds between inserts while looping. @default 2.4 */\n  interval?: number;\n  /**\n   * Jitter applied to each gap as a fraction of `interval` (0 = metronomic,\n   * 0.5 = gaps range ±50%). Seeded — the same rhythm every load. @default 0.35\n   */\n  jitter?: number;\n  /** Where new entries land: \"top\" (news wire) or \"bottom\" (chat). @default \"top\" */\n  direction?: \"top\" | \"bottom\";\n  /** How many entries stay in the feed before the oldest retires. @default 4 */\n  maxVisible?: number;\n  /** Depth in px of the fade mask under which the oldest entry retires. @default 48 */\n  fadeDepth?: number;\n  /**\n   * Spring for entry and the siblings' shuffle — one confident settle, no\n   * bounce chain. @default { stiffness: 500, damping: 42 }\n   */\n  spring?: { stiffness?: number; damping?: number };\n  /** Self-loop through `items`. Disable to drive the feed only via `ref.push()`. @default true */\n  loop?: boolean;\n  /** Pause the loop while the pointer is over the feed. @default true */\n  hoverPause?: boolean;\n  /**\n   * Accent cycle for icon chips (per-item `accent` wins). Accents stay confined\n   * to the chip — cards remain neutral. @default [\"#fbbf24\"]\n   */\n  colors?: string[];\n  /** Seed for the shuffle order and interval jitter. @default 7 */\n  seed?: number;\n  /** Freeze the loop entirely. @default false */\n  paused?: boolean;\n  /** Imperative handle exposing `push(item)` for live-push mode. */\n  ref?: React.Ref<WireHandle>;\n}\n\n/* ---------------------------------------------------------------- helpers */\n\n/** Deterministic PRNG (mulberry32) — no Math.random anywhere. */\nfunction mulberry32(seed: number): () => number {\n  let t = seed >>> 0;\n  return () => {\n    t = (t + 0x6d2b79f5) | 0;\n    let r = Math.imul(t ^ (t >>> 15), 1 | t);\n    r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;\n    return ((r ^ (r >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** Seeded Fisher–Yates over item indices — the loop's deterministic order. */\nfunction seededOrder(length: number, seed: number): number[] {\n  const rand = mulberry32(seed);\n  const order = Array.from({ length }, (_, i) => i);\n  for (let i = length - 1; i > 0; i--) {\n    const j = Math.floor(rand() * (i + 1));\n    [order[i], order[j]] = [order[j], order[i]];\n  }\n  return order;\n}\n\nfunction Glyph({ d }: { d: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.75}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"h-4 w-4\"\n      aria-hidden\n    >\n      <path d={d} />\n    </svg>\n  );\n}\n\nconst DEFAULT_ITEMS: WireItem[] = [\n  {\n    icon: <Glyph d=\"M15 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M8.5 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8M19 8v6M16 11h6\" />,\n    title: \"New signup\",\n    description: \"A new account just joined\",\n    time: \"just now\",\n  },\n  {\n    icon: <Glyph d=\"M2 9h20M5.5 4.5h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-13a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3ZM6 15.5h4\" />,\n    title: \"Payment received\",\n    description: \"Invoice #1042 was settled\",\n    time: \"1m ago\",\n  },\n  {\n    icon: <Glyph d=\"M12 3v12M7 10l5 5 5-5M4 21h16\" />,\n    title: \"Report exported\",\n    description: \"Monthly summary is ready\",\n    time: \"3m ago\",\n  },\n  {\n    icon: <Glyph d=\"M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0ZM8.5 12.5l2.5 2.5 5-5.5\" />,\n    title: \"Deploy complete\",\n    description: \"Version 2.4.1 is live\",\n    time: \"6m ago\",\n  },\n  {\n    icon: <Glyph d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\" />,\n    title: \"New comment\",\n    description: \"Someone replied to a thread\",\n    time: \"9m ago\",\n  },\n  {\n    icon: <Glyph d=\"M13 2 3 14h7l-1 8 10-12h-7l1-8Z\" />,\n    title: \"Milestone reached\",\n    description: \"10k events processed\",\n    time: \"12m ago\",\n  },\n];\n\nconst DEFAULT_COLORS = [\"#fbbf24\"];\n\n/** Minimum seconds between polite screen-reader announcements. */\nconst ANNOUNCE_GAP_MS = 4000;\n\ntype Entry = { key: number; item: WireItem };\n\n/**\n * Wire — a self-running \"live activity\" feed for marketing pages. New entries\n * land with one confident spring while siblings shuffle down via layout\n * animation and the oldest retires under an edge fade; each fresh entry's\n * leading edge flashes a hairline brightness that cools within 400ms — freshly\n * printed. Loops a provided items array on seeded, jittered intervals (never\n * metronomic, deterministic per seed), or accepts live items through an\n * imperative `push` ref. Hover pauses the feed; the interval also suspends\n * offscreen and on hidden tabs. Cards stay neutral with hairline borders — the\n * per-item accent lives only in the icon chip. Reduced motion renders a static\n * list of the most recent items, and live pushes appear without motion.\n *\n * **Boundary with `dispatch`:** wire is the *decorative* counterpart — a\n * declarative, self-looping showcase with seeded fake data, no dismissal and\n * no imperative toast() API, built to run unattended in a hero. For real app\n * notifications (imperative `toast()`, receding stack, swipe-dismiss, promise\n * morphs, `role=\"status\"`), use `dispatch` instead.\n */\nexport function Wire({\n  items = DEFAULT_ITEMS,\n  interval = 2.4,\n  jitter = 0.35,\n  direction = \"top\",\n  maxVisible = 4,\n  fadeDepth = 48,\n  spring,\n  loop = true,\n  hoverPause = true,\n  colors = DEFAULT_COLORS,\n  seed = 7,\n  paused = false,\n  ref,\n  className,\n  onPointerEnter,\n  onPointerLeave,\n  ...props\n}: WireProps) {\n  const reducedMotion = useReducedMotion();\n  const stiffness = spring?.stiffness ?? 500;\n  const damping = spring?.damping ?? 42;\n\n  const order = React.useMemo(() => seededOrder(items.length, seed), [items, seed]);\n\n  // Prefill so the feed reads alive from the first paint: the last `maxVisible`\n  // loop pushes, newest first. Deterministic (seeded) — SSR-safe.\n  const prefill = React.useMemo<Entry[]>(() => {\n    const count = Math.min(maxVisible, items.length);\n    return order\n      .slice(0, count)\n      .map((itemIndex, i) => ({ key: i, item: items[itemIndex] }))\n      .reverse();\n  }, [order, items, maxVisible]);\n\n  const [entries, setEntries] = React.useState<Entry[]>(prefill);\n  const [announcement, setAnnouncement] = React.useState(\"\");\n\n  const seqRef = React.useRef(prefill.length);\n  const cursorRef = React.useRef(prefill.length);\n  const hoveredRef = React.useRef(false);\n  const lastAnnounceRef = React.useRef(0);\n  const prevElapsedRef = React.useRef(0);\n  const countdownRef = React.useRef(interval);\n\n  // Persistent jitter PRNG, distinct stream from the shuffle. Lazy ref init\n  // keeps it deterministic and stable across renders.\n  const jitterRandRef = React.useRef<(() => number) | null>(null);\n  if (jitterRandRef.current === null) jitterRandRef.current = mulberry32(seed * 7919 + 1);\n\n  // If items/seed/maxVisible change, restart from the new deterministic prefill.\n  // (On mount `entries === prefill`, so React bails out — no extra render.)\n  React.useEffect(() => {\n    setEntries(prefill);\n    seqRef.current = prefill.length;\n    cursorRef.current = prefill.length;\n  }, [prefill]);\n\n  // A new seed also restarts the jitter stream, so the rhythm is deterministic\n  // per seed (not just the order).\n  const seedRef = React.useRef(seed);\n  React.useEffect(() => {\n    if (seedRef.current === seed) return;\n    seedRef.current = seed;\n    jitterRandRef.current = mulberry32(seed * 7919 + 1);\n  }, [seed]);\n\n  const pushItem = React.useCallback(\n    (item: WireItem) => {\n      const key = seqRef.current++;\n      setEntries((prev) => [{ key, item }, ...prev].slice(0, maxVisible));\n      // Polite announcements, throttled to sanity.\n      const now = performance.now();\n      if (now - lastAnnounceRef.current >= ANNOUNCE_GAP_MS) {\n        lastAnnounceRef.current = now;\n        setAnnouncement(`New activity: ${item.title}`);\n      }\n    },\n    [maxVisible]\n  );\n\n  React.useImperativeHandle(ref, () => ({ push: pushItem }), [pushItem]);\n\n  const advanceRef = React.useRef(() => {});\n  advanceRef.current = () => {\n    if (items.length === 0) return;\n    const itemIndex = order[cursorRef.current % order.length];\n    cursorRef.current++;\n    pushItem(items[itemIndex]);\n  };\n\n  const jitterProps = React.useRef({ interval, jitter, hoverPause });\n  jitterProps.current = { interval, jitter, hoverPause };\n\n  // rAF-driven countdown: suspends offscreen and on hidden tabs via the hook,\n  // and skips while hovered. dt-based so hook restarts (elapsed resets) are safe.\n  const loopSuspended = paused || !loop || (reducedMotion && loop);\n  const containerRef = useVisibilityPause<HTMLDivElement>(\n    (elapsed) => {\n      const prev = prevElapsedRef.current;\n      const dt = elapsed >= prev ? elapsed - prev : elapsed;\n      prevElapsedRef.current = elapsed;\n      if (jitterProps.current.hoverPause && hoveredRef.current) return;\n      countdownRef.current -= dt;\n      if (countdownRef.current <= 0) {\n        advanceRef.current();\n        const { interval: base, jitter: j } = jitterProps.current;\n        const rand = jitterRandRef.current!;\n        countdownRef.current = Math.max(0.35, base * (1 + j * (rand() * 2 - 1)));\n      }\n    },\n    { paused: loopSuspended }\n  );\n\n  const rendered = direction === \"top\" ? entries : [...entries].reverse();\n\n  // no-repeat + explicit size: content past the box is masked out, so the\n  // oldest entry retires under the fade and the entering card slips in from\n  // under a shallow leading-edge fade instead of overhanging the feed.\n  const mask =\n    direction === \"top\"\n      ? `linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - ${fadeDepth}px), transparent 100%)`\n      : `linear-gradient(to top, transparent 0, #000 12px, #000 calc(100% - ${fadeDepth}px), transparent 100%)`;\n\n  const enterY = direction === \"top\" ? -18 : 18;\n  const exitY = direction === \"top\" ? 14 : -14;\n\n  return (\n    <div\n      ref={containerRef}\n      data-crucible=\"wire\"\n      className={cn(\"relative w-full max-w-sm\", className)}\n      onPointerEnter={(event) => {\n        hoveredRef.current = true;\n        onPointerEnter?.(event);\n      }}\n      onPointerLeave={(event) => {\n        hoveredRef.current = false;\n        onPointerLeave?.(event);\n      }}\n      {...props}\n    >\n      <div\n        role=\"feed\"\n        aria-label=\"Activity feed\"\n        aria-busy={false}\n        className=\"flex w-full flex-col gap-2.5\"\n        style={{\n          maskImage: mask,\n          WebkitMaskImage: mask,\n          maskRepeat: \"no-repeat\",\n          WebkitMaskRepeat: \"no-repeat\",\n          maskSize: \"100% 100%\",\n          WebkitMaskSize: \"100% 100%\",\n        }}\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {rendered.map(({ key, item }, i) => {\n            const accent = item.accent ?? colors[key % Math.max(1, colors.length)] ?? DEFAULT_COLORS[0];\n            return (\n              <motion.article\n                key={key}\n                aria-label={item.title}\n                aria-posinset={i + 1}\n                aria-setsize={rendered.length}\n                layout={!reducedMotion}\n                initial={reducedMotion ? false : { opacity: 0, y: enterY, scale: 0.98 }}\n                animate={{ opacity: 1, y: 0, scale: 1 }}\n                exit={\n                  reducedMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { opacity: 0, y: exitY, transition: { duration: 0.3, ease: \"easeOut\" } }\n                }\n                transition={\n                  reducedMotion\n                    ? { duration: 0 }\n                    : {\n                        // One confident spring — entry and sibling shuffle alike.\n                        type: \"spring\",\n                        stiffness,\n                        damping,\n                        opacity: { duration: 0.28, ease: \"easeOut\" },\n                      }\n                }\n                className=\"relative flex w-full items-center gap-3 rounded-lg border border-white/[0.08] bg-white/[0.03] px-3.5 py-3\"\n              >\n                {/* Freshly printed: the leading edge flashes a hairline that\n                    cools within 400ms. Skipped for prefilled entries because\n                    AnimatePresence mounts them with initial={false}. */}\n                {!reducedMotion && (\n                  <motion.span\n                    aria-hidden\n                    className=\"pointer-events-none absolute inset-x-3 top-0 h-px\"\n                    style={{\n                      background:\n                        \"linear-gradient(90deg, transparent, rgba(255,255,255,0.85), transparent)\",\n                      boxShadow: \"0 0 10px rgba(255,255,255,0.35)\",\n                    }}\n                    initial={{ opacity: 1 }}\n                    animate={{ opacity: 0 }}\n                    transition={{ duration: 0.4, ease: \"easeOut\" }}\n                  />\n                )}\n                <span\n                  aria-hidden\n                  className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md border\"\n                  style={{\n                    color: accent,\n                    borderColor: `color-mix(in srgb, ${accent} 28%, transparent)`,\n                    background: `color-mix(in srgb, ${accent} 12%, transparent)`,\n                  }}\n                >\n                  {item.icon ?? <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />}\n                </span>\n                <span className=\"min-w-0 flex-1\">\n                  <span className=\"block truncate text-sm font-medium text-neutral-100\">{item.title}</span>\n                  {item.description && (\n                    <span className=\"mt-0.5 block truncate text-xs text-neutral-400\">{item.description}</span>\n                  )}\n                </span>\n                {item.time && (\n                  <span className=\"shrink-0 self-start text-[10px] font-medium tabular-nums text-neutral-500\">\n                    {item.time}\n                  </span>\n                )}\n              </motion.article>\n            );\n          })}\n        </AnimatePresence>\n      </div>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n"
    }
  ]
}