{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A SwitchboardCard component with light grid illustration for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useMemo, useState } from \"react\";\n\nexport type LightState = \"off\" | \"medium\" | \"high\";\n\nexport interface SwitchboardCardProps {\n  className?: string;\n  columns?: number;\n  gridPattern?: number[] | number[][]; // Direct grid pattern: flat array (columns*rows) or 2D array [rows][columns] where 0=off, 1=high\n  href?: string;\n  onButtonClick?: () => void;\n  randomLights?: boolean; // If true, randomly activate some lights\n  rows?: number;\n  showButton?: boolean;\n  subtitle: string;\n  title: string;\n  transitionDuration?: number; // in milliseconds (default: 200ms for smooth light transitions)\n  variant?: \"default\" | \"next\";\n}\n\nfunction generateRandomPattern(\n  totalLights: number,\n  activeRatio = 0.15\n): number[] {\n  const activeCount = Math.floor(totalLights * activeRatio);\n  const pattern: number[] = [];\n  const used = new Set<number>();\n\n  while (pattern.length < activeCount) {\n    const index = Math.floor(Math.random() * totalLights);\n    if (!used.has(index)) {\n      used.add(index);\n      pattern.push(index);\n    }\n  }\n\n  return pattern.sort((a, b) => a - b);\n}\n\nfunction parse2DGridPattern(\n  grid2D: number[][],\n  rows: number,\n  columns: number,\n  totalLights: number\n): number[] {\n  const pattern: number[] = [];\n  for (let row = 0; row < Math.min(grid2D.length, rows); row++) {\n    const rowData = grid2D[row];\n    if (Array.isArray(rowData)) {\n      for (let col = 0; col < Math.min(rowData.length, columns); col++) {\n        if (rowData[col] === 1) {\n          const index = col + row * columns;\n          if (index >= 0 && index < totalLights) {\n            pattern.push(index);\n          }\n        }\n      }\n    }\n  }\n  return pattern;\n}\n\nfunction parseFlatGridPattern(\n  flatPattern: number[],\n  totalLights: number\n): number[] {\n  const pattern: number[] = [];\n  for (let i = 0; i < Math.min(flatPattern.length, totalLights); i++) {\n    if (flatPattern[i] === 1) {\n      pattern.push(i);\n    }\n  }\n  return pattern;\n}\n\nexport default function SwitchboardCard({\n  title,\n  subtitle,\n  columns = 18,\n  rows = 5,\n  gridPattern,\n  randomLights = false,\n  transitionDuration = 200,\n  className,\n  variant = \"default\",\n  showButton: _showButton = true,\n  href,\n  onButtonClick,\n}: SwitchboardCardProps) {\n  const totalLights = columns * rows;\n  const isRandomLightsMode = randomLights && !gridPattern;\n\n  // Convert gridPattern to light indices\n  // Priority: gridPattern > randomLights\n  const basePattern = useMemo<number[]>(() => {\n    if (gridPattern) {\n      // Handle 2D array [rows][columns]\n      const is2DArray =\n        Array.isArray(gridPattern[0]) && typeof gridPattern[0] !== \"number\";\n      if (is2DArray) {\n        return parse2DGridPattern(\n          gridPattern as number[][],\n          rows,\n          columns,\n          totalLights\n        );\n      }\n      // Handle flat array [columns*rows]\n      return parseFlatGridPattern(gridPattern as number[], totalLights);\n    }\n    if (randomLights) {\n      return generateRandomPattern(totalLights);\n    }\n    return [];\n  }, [gridPattern, randomLights, columns, rows, totalLights]);\n\n  // Animated light states\n  // For random lights: all start off, will animate randomly through medium → high → medium → off\n  // For word/pattern: pattern lights start high (stay high), rest off\n  const [lightStates, setLightStates] = useState<LightState[]>(() => {\n    const states: LightState[] = new Array(totalLights).fill(\"off\");\n    if (!isRandomLightsMode) {\n      // For word/pattern, set pattern lights to high\n      for (const index of basePattern) {\n        if (index >= 0 && index < totalLights) {\n          states[index] = \"high\";\n        }\n      }\n    }\n    // For random lights, all start off (will animate)\n    return states;\n  });\n\n  // Animate lights - only for random lights mode (word/pattern stays high)\n  useEffect(() => {\n    // Word/pattern mode: no animation, lights stay high\n    if (!isRandomLightsMode) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      setLightStates((prev) => {\n        const next = [...prev];\n        // For random lights: animate all lights randomly through off → high (via medium transition)\n        // Randomly select a subset of lights to animate each tick\n        const lightsToAnimate = Math.floor(totalLights * 0.2); // Animate ~20% of all lights per tick\n        const allIndices = Array.from({ length: totalLights }, (_, i) => i);\n        const shuffled = allIndices.sort(() => Math.random() - 0.5);\n\n        for (let i = 0; i < Math.min(lightsToAnimate, shuffled.length); i++) {\n          const index = shuffled[i];\n          if (index >= 0 && index < totalLights) {\n            const current = prev[index];\n            // State transition logic for random lights: off → high (with medium as intermediate transition state)\n            // Medium state is only used as a transition, not a final state\n            if (current === \"off\") {\n              // Start transition to high by going to medium first\n              next[index] = \"medium\";\n            } else if (current === \"medium\") {\n              // Continue transition: medium → high\n              next[index] = \"high\";\n            } else if (current === \"high\") {\n              // Turn off directly (no medium transition when turning off)\n              next[index] = \"off\";\n            }\n          }\n        }\n        return next;\n      });\n    }, 200); // 0.2s interval to match transition duration\n\n    return () => clearInterval(interval);\n  }, [isRandomLightsMode, totalLights]);\n\n  const isNextVariant = variant === \"next\";\n\n  const CardContent = (\n    <div\n      className={cn(\n        \"relative flex h-[380px] w-full flex-col overflow-hidden rounded-xl border p-6 transition-[border-color,background-color] duration-150\",\n        isNextVariant\n          ? \"border-0 dark:border-0 dark:shadow-[inset_0_0_6px_rgba(255,255,255,0.1)]\"\n          : \"border-border bg-background hover:border-foreground/20\",\n        className\n      )}\n      data-variant={isNextVariant ? \"next\" : undefined}\n      style={\n        isNextVariant\n          ? ({\n              background:\n                \"linear-gradient(110deg, oklch(0.65 0 0) 0.06%, oklch(0.54 0 0) 100%)\",\n            } as React.CSSProperties)\n          : undefined\n      }\n    >\n      {/* Illustration/Switchboard */}\n      <div\n        className=\"flex h-full w-full items-center justify-center\"\n        data-illustration=\"true\"\n      >\n        <div\n          className=\"grid h-full w-full\"\n          style={{\n            gap: `min(1px, calc(100% / ${columns} / 10))`,\n            gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,\n            gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,\n            maxHeight: \"81px\",\n            transitionDuration: `${transitionDuration}ms`,\n          }}\n        >\n          {lightStates.map((state, index) => (\n            <LightBulb\n              // biome-ignore lint/suspicious/noArrayIndexKey: Light grid position is stable and never reorders\n              key={`light-${index}`}\n              state={state}\n              transitionDuration={transitionDuration}\n            />\n          ))}\n        </div>\n      </div>\n\n      {/* Title */}\n      <div className=\"mt-4 flex flex-row items-center justify-start gap-2 text-left\">\n        <h3\n          className={cn(\n            \"text-left font-semibold leading-8 tracking-[-0.04em]\",\n            isNextVariant ? \"mt-4 text-2xl\" : \"text-foreground text-xl\"\n          )}\n          data-title=\"true\"\n          style={\n            isNextVariant\n              ? ({ color: \"oklch(0.95 0 0)\" } as React.CSSProperties)\n              : undefined\n          }\n        >\n          {title}\n        </h3>\n      </div>\n\n      {/* Subtitle */}\n      <p\n        className={cn(\n          \"text-left text-sm leading-relaxed tracking-[-0.01em]\",\n          isNextVariant\n            ? \"mt-1 max-w-[80%] text-base\"\n            : \"mt-1 text-foreground/70\"\n        )}\n        data-subtitle=\"true\"\n        style={\n          isNextVariant\n            ? ({ color: \"oklch(0.9 0 0)\" } as React.CSSProperties)\n            : undefined\n        }\n      >\n        {subtitle}\n      </p>\n    </div>\n  );\n\n  if (href) {\n    return (\n      <a\n        aria-label={title}\n        className=\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        href={href}\n      >\n        {CardContent}\n      </a>\n    );\n  }\n\n  if (onButtonClick) {\n    return (\n      <button\n        aria-label={title}\n        className=\"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        onClick={onButtonClick}\n        type=\"button\"\n      >\n        {CardContent}\n      </button>\n    );\n  }\n\n  return CardContent;\n}\n\nfunction LightBulb({\n  state,\n  transitionDuration,\n}: {\n  state: LightState;\n  transitionDuration: number;\n}) {\n  const transitionStyle = {\n    transition: `opacity ${transitionDuration}ms ease-out, transform ${transitionDuration}ms ease-out`,\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative rounded-full\",\n        state === \"off\" && \"bg-foreground/50\",\n        state === \"high\" ? \"h-[2px] w-[2px]\" : \"h-px w-px\"\n      )}\n      data-state={state}\n      style={{\n        ...transitionStyle,\n        transform: state === \"off\" ? \"unset\" : \"scale(1)\",\n      }}\n    >\n      {/* Glow effect for medium state (::before equivalent) */}\n      {state === \"medium\" && (\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            ...transitionStyle,\n            backgroundColor: \"var(--color-brand)\",\n            boxShadow:\n              \"0 0 2px 1px color-mix(in oklch, var(--color-brand), transparent 60%), 0 0 4px 1.5px color-mix(in oklch, var(--color-brand), transparent 75%)\",\n            opacity: 1,\n          }}\n        />\n      )}\n\n      {/* Glow effect for high state (::after equivalent) */}\n      {state === \"high\" && (\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            ...transitionStyle,\n            backgroundColor: \"var(--color-brand)\",\n            boxShadow:\n              \"0 0 2px 1px color-mix(in oklch, var(--color-brand), transparent 40%), 0 0 4px 1.5px color-mix(in oklch, var(--color-brand), transparent 65%), 0 0 6px 2px color-mix(in oklch, var(--color-brand), transparent 80%)\",\n            opacity: 1,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/switchboard-card/index.tsx","type":"registry:ui"}],"name":"switchboard-card","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Switchboard Card","type":"registry:ui"}