{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pixel-object",
  "title": "Pixel object",
  "author": "WLKR Labs",
  "description": "A projected pixel sphere, custom point cloud, or mask with considerate motion controls.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/wlkr/pixel-object.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Pause, Play } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\nexport type PixelPoint = { x: number; y: number; z?: number }\nexport type PixelPattern = \"steady\" | \"scatter\" | \"wave\" | \"scan\"\nexport type PixelObjectProps = {\n  points?: PixelPoint[]; mask?: string[]; resolution?: number; size?: number\n  label?: string; rotating?: boolean; className?: string\n  dotShape?: \"square\" | \"rounded\" | \"circle\"\n  /** Nominal SVG pixels. Setting either dotSize or gap derives density from their pitch, overriding resolution. */\n  dotSize?: number; gap?: number\n  /** Radians per second. Flicker timing is independent of rotation. */\n  rotationSpeed?: number\n  flickerAmount?: number; flickerSpeed?: number; pattern?: PixelPattern\n  /** Milliseconds before motion starts; stagger spreads the pattern phase across the object. */\n  delay?: number; stagger?: number\n  seed?: number | string; randomness?: number; opacity?: number\n  /** Change this value to restart the timeline without remounting the object. */\n  restartKey?: number | string\n}\nconst clamp = (value: number, min: number, max: number, fallback: number) => Math.max(min, Math.min(max, Number.isFinite(value) ? value : fallback))\nexport function spherePoints(count = 420): PixelPoint[] {\n  const total = Math.round(clamp(count, 12, 2000, 420))\n  return Array.from({ length: total }, (_, index) => {\n    const y = 1 - (index / (total - 1)) * 2\n    const radius = Math.sqrt(1 - y * y)\n    const angle = index * Math.PI * (3 - Math.sqrt(5))\n    return { x: Math.cos(angle) * radius, y, z: Math.sin(angle) * radius }\n  })\n}\nconst sphere = spherePoints()\nconst styles = `\n.wlkr-pixel-object{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:100%}\n.wlkr-pixel-object>svg{display:block;width:100%;height:auto;overflow:visible}\n.wlkr-pixel-toggle{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-width:44px;min-height:44px;padding:0 12px;cursor:pointer}\n.wlkr-pixel-toggle svg{width:14px;height:14px}\n.wlkr-pixel-toggle:focus-visible{outline:2px solid var(--ring,currentColor);outline-offset:4px}\n`\nfunction seedNumber(seed: string | number) {\n  let result = 2166136261\n  for (const character of String(seed)) result = Math.imul(result ^ character.charCodeAt(0), 16777619)\n  return result >>> 0\n}\nfunction noise(x: number, y: number, seed: number) {\n  let value = Math.imul(x + 1, 374761393) ^ Math.imul(y + 1, 668265263) ^ seed\n  value = Math.imul(value ^ value >>> 13, 1274126177)\n  return ((value ^ value >>> 16) >>> 0) / 4294967295\n}\n\n/** Native SVG projection; seeded smooth patterns; no work while paused, hidden, off-screen or reduced-motion. */\nexport function PixelObject({ points, mask, resolution = 36, size = 260, label = \"Rotating pixel sphere\", rotating = true, className, dotShape = \"rounded\", dotSize, gap, rotationSpeed = .15, flickerAmount = 0, flickerSpeed = .5, pattern = \"steady\", delay = 0, stagger = 0, seed = 1, randomness = 1, opacity = 1, restartKey = 0 }: PixelObjectProps) {\n  const root = React.useRef<HTMLDivElement>(null)\n  const timeline = React.useRef({ time:0, angle:0, key:restartKey })\n  const [frame, setFrame] = React.useState({time:0, angle:0})\n  const [paused, setPaused] = React.useState(false)\n  const [motion, setMotion] = React.useState(false)\n  const dimension = clamp(size,64,1000,260)\n  const legacyGrid = Math.round(clamp(resolution,8,80,36))\n  const explicitPitch = dotSize !== undefined || gap !== undefined\n  const dot = explicitPitch ? clamp(dotSize ?? dimension / legacyGrid * .72,.5,40,4) : .72\n  const spacing = explicitPitch ? clamp(gap ?? dimension / legacyGrid * .28,0,40,2) : .28\n  const pitch = explicitPitch ? dot + spacing : 1\n  const extent = explicitPitch ? dimension : legacyGrid\n  const grid = explicitPitch ? Math.max(1,Math.floor((extent + spacing) / pitch)) : legacyGrid\n  const inset = explicitPitch ? (extent - (grid - 1) * pitch - dot) / 2 : 0\n  const speed = clamp(rotationSpeed,0,3,.15)\n  const strength = clamp(flickerAmount,0,1,0)\n  const frequency = clamp(flickerSpeed,0,2,.5)\n  const startDelay = clamp(delay,0,30000,0)\n  const spread = clamp(stagger,0,10000,0)\n  const random = clamp(randomness,0,1,1)\n  const alpha = clamp(opacity,0,1,1)\n  const seedValue = seedNumber(seed)\n  const shape = [\"square\",\"rounded\",\"circle\"].includes(dotShape) ? dotShape : \"rounded\"\n  const effect = [\"steady\",\"scatter\",\"wave\",\"scan\"].includes(pattern) ? pattern : \"steady\"\n  const animated = rotating && speed > 0 || strength > 0 && frequency > 0 && effect !== \"steady\"\n  const cloud = React.useMemo(() => {\n    if (mask) {\n      const rows = mask.slice(0,80)\n      const width = Math.max(1,...rows.map(row => row.slice(0,80).length))\n      return rows.flatMap((row,y) => [...row.slice(0,80)].flatMap((pixel,x) => pixel === \"#\" ? [{x:width === 1 ? 0 : x/(width-1)*2-1,y:rows.length === 1 ? 0 : 1-y/(rows.length-1)*2,z:0}] : []))\n    }\n    return (points ?? sphere).filter(point => Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z ?? 0)).slice(0,2000)\n  },[points,mask])\n  React.useEffect(() => {\n    const preference = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    let intersecting = false\n    let request = 0\n    let last = 0\n    let drawn = 0\n    function resetTimeline() { timeline.current = {time:0,angle:0,key:restartKey}; setPaused(false) }\n    if (timeline.current.key !== restartKey) resetTimeline()\n    function stop() { if(request) cancelAnimationFrame(request); request=0; last=0 }\n    function tick(time:number) {\n      const elapsed = last ? Math.min(time-last,80) : 0\n      last=time\n      const before = Math.max(0,timeline.current.time-startDelay)\n      timeline.current.time += elapsed\n      const after = Math.max(0,timeline.current.time-startDelay)\n      if(rotating) timeline.current.angle += (after-before)/1000*speed\n      if(time-drawn>=80) { setFrame({time:timeline.current.time,angle:timeline.current.angle}); drawn=time }\n      request=requestAnimationFrame(tick)\n    }\n    function sync() {\n      const active = animated && !paused && !preference.matches && intersecting && !document.hidden\n      setMotion(active)\n      if(active && !request) request=requestAnimationFrame(tick)\n      if(!active) { stop(); setFrame({time:timeline.current.time,angle:timeline.current.angle}) }\n    }\n    const observer = new IntersectionObserver(entries => {intersecting=entries[0]?.isIntersecting??false;sync()},{threshold:.05})\n    if(root.current) observer.observe(root.current)\n    preference.addEventListener(\"change\",sync)\n    document.addEventListener(\"visibilitychange\",sync)\n    sync()\n    return () => {stop();observer.disconnect();preference.removeEventListener(\"change\",sync);document.removeEventListener(\"visibilitychange\",sync)}\n  },[animated,paused,rotating,speed,startDelay,restartKey])\n  const pixels = new Map<string,{x:number;y:number;depth:number}>()\n  const cosine=Math.cos(frame.angle), sine=Math.sin(frame.angle)\n  for(const point of cloud) {\n    const z=point.z??0\n    const x=point.x*cosine+z*sine\n    const depth=-point.x*sine+z*cosine\n    const column=Math.round((x*.43+.5)*(grid-1)), row=Math.round((-point.y*.43+.5)*(grid-1))\n    if(column<0||column>=grid||row<0||row>=grid) continue\n    const key=`${column},${row}`\n    if(!pixels.has(key)||pixels.get(key)!.depth<depth) pixels.set(key,{x:column,y:row,depth})\n  }\n  function intensity(x:number,y:number) {\n    if(!strength||effect===\"steady\") return 1\n    const elapsed=frame.time-startDelay-spread*(x+y)/Math.max(1,2*(grid-1))\n    if(elapsed<=0) return 1\n    const ramp=Math.min(1,elapsed/500)\n    const phase=elapsed/1000*frequency*2*Math.PI\n    const offset=noise(x,y,seedValue)*Math.PI*2*random\n    const wave=effect===\"scatter\" ? Math.sin(phase+offset) : effect===\"wave\" ? Math.sin(phase-(x+y)*.3-offset*.2) : Math.cos(phase-y/Math.max(1,grid-1)*Math.PI*2)\n    return 1-strength*ramp*(.65*(1-(wave+1)/2))\n  }\n  return <div ref={root} className={cn(\"wlkr-pixel-object\",className)} style={{width:dimension}} data-motion={motion?\"running\":\"paused\"} data-pattern={effect}>\n    <style>{styles}</style>\n    <svg viewBox={`0 0 ${extent} ${extent}`} role=\"img\" aria-label={label}>\n      {[...pixels.entries()].map(([key,pixel]) => <rect key={key} x={inset+pixel.x*pitch} y={inset+pixel.y*pitch} width={dot} height={dot} rx={shape===\"circle\"?dot/2:shape===\"rounded\"?dot/12:0} fill=\"currentColor\" opacity={alpha*(.22+Math.max(0,Math.min(1,(pixel.depth+1)/2))*.73)*intensity(pixel.x,pixel.y)} />)}\n    </svg>\n    {animated&&<Button type=\"button\" className=\"wlkr-pixel-toggle\" variant=\"ghost\" aria-label={paused?\"Resume object motion\":\"Pause object motion\"} aria-pressed={paused} onClick={()=>setPaused(value=>!value)}>{paused?<Play aria-hidden=\"true\"/>:<Pause aria-hidden=\"true\"/>}<span>{paused?\"Resume\":\"Pause\"}</span></Button>}\n    <span role=\"status\" className=\"sr-only\">{motion?\"Object motion active.\":\"Object is still.\"}</span>\n  </div>\n}\n",
      "type": "registry:component",
      "target": "components/wlkr/pixel-object.tsx"
    }
  ],
  "type": "registry:component"
}