{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "canvas",
  "type": "registry:component",
  "title": "Canvas",
  "description": "A surface of dots, lines or crosses for other components to sit on, with a light that can follow the pointer.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/canvas.tsx",
      "type": "registry:component",
      "target": "@components/panelui/canvas.tsx",
      "content": "'use client';\n\n/**\n * Canvas — a surface for other components to sit on: a field of dots, a\n * grid or crosses, with a light that can follow the pointer across it.\n *\n * ```tsx\n * <Canvas className=\"h-96\">\n *   <Canvas.Pattern variant=\"dots\" />\n *   <Canvas.Spotlight />\n *   <Canvas.Content>…</Canvas.Content>\n * </Canvas>\n * ```\n *\n * ## Layers on one surface\n *\n * A canvas is a stack of layers. `Canvas.Pattern` draws marks across the\n * whole surface, several patterns stack, `Canvas.Spotlight` lights them near\n * the pointer, and `Canvas.Content` is what sits on top. Every layer is\n * drawn from the same `viewport`, so a layer added later, such as ink drawn\n * with the pointer, lines up with the pattern without measuring it.\n *\n * ## It moves with what is on it\n *\n * `viewport` takes the `{ x, y, zoom }` a Flow reports from\n * `onViewportChange`, so the pattern pans and scales with the graph on top\n * of it instead of staying behind as the graph slides over it.\n *\n * ## The light follows the hand\n *\n * The spotlight is a second copy of the pattern in the foreground colour,\n * shown through a round window centred on the pointer that fades from the\n * middle to its edge. It follows the pointer without easing, from events on\n * the canvas itself, so it works over whatever is in the content. It is\n * written straight to the element, once a frame, without rendering, and it\n * fades out when the pointer leaves. On a touch screen it shows while a\n * finger is down.\n */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nconst canvasVariants = tv({\n  slots: {\n    root: 'relative isolate overflow-hidden bg-background text-foreground',\n    layer: 'pointer-events-none absolute inset-0 size-full',\n    // The resting marks: the foreground at a fraction of its strength, so the\n    // field reads as a texture and not as content.\n    pattern: 'text-foreground',\n    spotlight:\n      'text-foreground opacity-0 transition-opacity duration-200 data-on:opacity-100 motion-reduce:transition-none',\n    content: 'relative z-10 size-full',\n  },\n});\n\n/** Where the canvas is looking: the same shape a Flow reports. */\nexport interface CanvasViewport {\n  x: number;\n  y: number;\n  zoom: number;\n}\n\n/** The marks a pattern draws. */\nexport type CanvasPatternVariant = 'dots' | 'grid' | 'cross';\n\n/** How a pattern fades out: not at all, towards every edge, or towards the bottom. */\nexport type CanvasFade = 'none' | 'edges' | 'bottom';\n\ninterface PatternSpec {\n  variant: CanvasPatternVariant;\n  gap: number;\n  size: number;\n  major?: number;\n  fade: CanvasFade;\n}\n\ninterface CanvasContextValue {\n  viewport: CanvasViewport;\n  root: HTMLDivElement | null;\n  patterns: Array<{ key: string } & PatternSpec>;\n  register(key: string, spec: PatternSpec): () => void;\n}\n\nconst CanvasContext = createContext<CanvasContextValue | null>(null);\n\nfunction useCanvas(part: string): CanvasContextValue {\n  const canvas = useContext(CanvasContext);\n  if (!canvas) throw new Error(`${part} must be used inside a <Canvas>.`);\n  return canvas;\n}\n\nconst ORIGIN: CanvasViewport = { x: 0, y: 0, zoom: 1 };\n\nconst FADES: Record<CanvasFade, string | undefined> = {\n  none: undefined,\n  edges: 'radial-gradient(ellipse 75% 70% at 50% 50%, #000 35%, transparent 100%)',\n  bottom: 'linear-gradient(to bottom, #000 40%, transparent 95%)',\n};\n\nexport interface CanvasProps extends ComponentProps<'div'> {\n  /**\n   * Where the canvas is looking, as `{ x, y, zoom }`. Pass what a Flow reports\n   * from `onViewportChange`, and the pattern pans and scales with the graph.\n   * Default `{ x: 0, y: 0, zoom: 1 }`.\n   */\n  viewport?: CanvasViewport;\n}\n\nfunction CanvasRoot({ viewport = ORIGIN, className, children, ...props }: CanvasProps) {\n  const [root, setRoot] = useState<HTMLDivElement | null>(null);\n  const [patterns, setPatterns] = useState<CanvasContextValue['patterns']>([]);\n\n  const register = useCallback((key: string, spec: PatternSpec) => {\n    setPatterns((current) => [...current.filter((pattern) => pattern.key !== key), { key, ...spec }]);\n    return () => setPatterns((current) => current.filter((pattern) => pattern.key !== key));\n  }, []);\n\n  const context = useMemo<CanvasContextValue>(\n    () => ({ viewport, root, patterns, register }),\n    [viewport, root, patterns, register]\n  );\n\n  return (\n    <CanvasContext.Provider value={context}>\n      <div ref={setRoot} data-slot=\"canvas\" className={canvasVariants().root({ className })} {...props}>\n        {children}\n      </div>\n    </CanvasContext.Provider>\n  );\n}\nCanvasRoot.displayName = 'Canvas';\n\n/** Draws one pattern's marks into an SVG, from the viewport. */\nfunction Marks({ id, spec, viewport }: { id: string; spec: PatternSpec; viewport: CanvasViewport }) {\n  const { variant, gap, size, major } = spec;\n  const transform = `translate(${viewport.x.toFixed(2)} ${viewport.y.toFixed(2)}) scale(${viewport.zoom.toFixed(4)})`;\n  const half = gap / 2;\n\n  const mark = (scale: number, at: number) => {\n    if (variant === 'dots') return <circle cx={at} cy={at} r={size * scale} fill=\"currentColor\" />;\n    if (variant === 'cross') {\n      const arm = size * scale;\n      return (\n        <path\n          d={`M${at - arm} ${at}h${arm * 2}M${at} ${at - arm}v${arm * 2}`}\n          stroke=\"currentColor\"\n          strokeWidth={scale > 1 ? 1.25 : 1}\n          strokeLinecap=\"round\"\n          fill=\"none\"\n        />\n      );\n    }\n    return null;\n  };\n\n  // Every variant marks the same lattice points, half a gap in from each\n  // tile's corner, so stacked patterns line up: a cross lands on a grid's\n  // crossing, and a major line on a minor one.\n  const tile = major ? gap * major : gap;\n  return (\n    <>\n      <defs>\n        <pattern id={`${id}-minor`} width={gap} height={gap} patternUnits=\"userSpaceOnUse\" patternTransform={transform}>\n          {variant === 'grid' ? (\n            <path\n              d={`M${half} 0V${gap}M0 ${half}H${gap}`}\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={size}\n              opacity={major ? 0.6 : 1}\n            />\n          ) : (\n            mark(1, half)\n          )}\n        </pattern>\n        {major ? (\n          <pattern\n            id={`${id}-major`}\n            width={tile}\n            height={tile}\n            patternUnits=\"userSpaceOnUse\"\n            patternTransform={transform}\n          >\n            {variant === 'grid' ? (\n              <path\n                d={`M${half} 0V${tile}M0 ${half}H${tile}`}\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth={size * 1.5}\n              />\n            ) : (\n              mark(variant === 'dots' ? 1.8 : 1.6, half)\n            )}\n          </pattern>\n        ) : null}\n      </defs>\n      <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-minor)`} />\n      {major ? <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-major)`} /> : null}\n    </>\n  );\n}\n\nexport interface CanvasPatternProps extends Omit<ComponentProps<'svg'>, 'children'> {\n  /** Dots, a grid of lines, or small crosses. Default `dots`. */\n  variant?: CanvasPatternVariant;\n  /** The distance from one mark to the next, in pixels at a zoom of 1. Default `12`, or `24` for a grid. */\n  gap?: number;\n  /** How big a mark is: a dot's radius, a line's width, half a cross's arm. Default `0.75`, `1` or `3`. */\n  size?: number;\n  /** Draws every this-many-th line or mark stronger, as on graph paper. */\n  major?: number;\n  /** Fades the pattern out towards every edge or towards the bottom. Default `none`. */\n  fade?: CanvasFade;\n  /**\n   * How strongly the marks show at rest, as a share of the foreground\n   * colour. Default `28%` for dots and crosses, `10%` for a grid.\n   */\n  strength?: string;\n}\n\n/** A pattern across the whole canvas, which pans and scales with its viewport. */\nfunction CanvasPattern({\n  variant = 'dots',\n  gap,\n  size,\n  major,\n  fade = 'none',\n  strength,\n  className,\n  style,\n  render,\n  ...props\n}: CanvasPatternProps & RenderProp<'svg'>) {\n  const canvas = useCanvas('Canvas.Pattern');\n  const id = `canvas${useId().replace(/[^a-zA-Z0-9]/g, '')}`;\n  const spec = useMemo<PatternSpec>(\n    () => ({\n      variant,\n      gap: gap ?? (variant === 'grid' ? 24 : 12),\n      size: size ?? (variant === 'dots' ? 0.75 : variant === 'grid' ? 1 : 3),\n      major,\n      fade,\n    }),\n    [variant, gap, size, major, fade]\n  );\n  const { register } = canvas;\n  // Known to the spotlight, which draws a lit copy of every pattern.\n  useLayoutEffect(() => register(id, spec), [register, id, spec]);\n\n  const mask = FADES[fade];\n  return useRender({\n    defaultTagName: 'svg',\n    render,\n    props: mergeProps(\n      {\n        'aria-hidden': 'true',\n        'data-slot': 'canvas-pattern',\n        className: canvasVariants().layer({ className: [canvasVariants().pattern(), className] }),\n        style: {\n          opacity: strength ?? (variant === 'grid' ? '10%' : '28%'),\n          maskImage: mask,\n          WebkitMaskImage: mask,\n          ...style,\n        } as CSSProperties,\n        children: (\n          <>\n            <Marks id={id} spec={spec} viewport={canvas.viewport} />\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nCanvasPattern.displayName = 'Canvas.Pattern';\n\nexport interface CanvasSpotlightProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** How far the light reaches from its centre, in pixels. Default `100`. */\n  radius?: number;\n  /**\n   * Holds the light at a point, in pixels from the canvas's top left corner,\n   * instead of following the pointer.\n   */\n  position?: { x: number; y: number };\n}\n\n/**\n * Lights the patterns near the pointer: the marks inside the circle brighten\n * towards the foreground colour, most at the centre.\n */\nfunction CanvasSpotlight({\n  radius = 100,\n  position,\n  className,\n  style,\n  render,\n  ...props\n}: CanvasSpotlightProps & RenderProp<'div'>) {\n  const canvas = useCanvas('Canvas.Spotlight');\n  const light = useRef<HTMLDivElement>(null);\n  const prefix = `spot${useId().replace(/[^a-zA-Z0-9]/g, '')}`;\n\n  // Follows the pointer from events on the whole canvas, so content on top\n  // does not stop it. Written to the element once a frame, without rendering.\n  useEffect(() => {\n    const root = canvas.root;\n    const node = light.current;\n    if (!root || !node || position) return;\n    let frame = 0;\n    let x = 0;\n    let y = 0;\n    let down = false;\n    const write = () => {\n      frame = 0;\n      node.style.setProperty('--canvas-x', `${x}px`);\n      node.style.setProperty('--canvas-y', `${y}px`);\n    };\n    const place = (event: PointerEvent) => {\n      const box = root.getBoundingClientRect();\n      x = event.clientX - box.left;\n      y = event.clientY - box.top;\n      if (!frame) frame = requestAnimationFrame(write);\n    };\n    const show = (on: boolean) => node.toggleAttribute('data-on', on);\n    const move = (event: PointerEvent) => {\n      place(event);\n      if (event.pointerType !== 'touch' || down) show(true);\n    };\n    const press = (event: PointerEvent) => {\n      down = true;\n      place(event);\n      show(true);\n    };\n    const release = (event: PointerEvent) => {\n      down = false;\n      if (event.pointerType === 'touch') show(false);\n    };\n    const leave = () => show(false);\n    root.addEventListener('pointermove', move);\n    root.addEventListener('pointerdown', press);\n    root.addEventListener('pointerup', release);\n    root.addEventListener('pointercancel', release);\n    root.addEventListener('pointerleave', leave);\n    return () => {\n      cancelAnimationFrame(frame);\n      root.removeEventListener('pointermove', move);\n      root.removeEventListener('pointerdown', press);\n      root.removeEventListener('pointerup', release);\n      root.removeEventListener('pointercancel', release);\n      root.removeEventListener('pointerleave', leave);\n    };\n  }, [canvas.root, position]);\n\n  // A round window on the lit copy: opaque at the pointer, clear at the radius.\n  const circle = `radial-gradient(circle var(--canvas-r) at var(--canvas-x, -999px) var(--canvas-y, -999px), #000, transparent)`;\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        ref: light,\n        'aria-hidden': 'true',\n        'data-slot': 'canvas-spotlight',\n        'data-on': position ? '' : undefined,\n        className: canvasVariants().layer({ className: [canvasVariants().spotlight(), className] }),\n        style: {\n          '--canvas-r': `${radius}px`,\n          ...(position ? { '--canvas-x': `${position.x}px`, '--canvas-y': `${position.y}px` } : null),\n          ...style,\n        } as CSSProperties,\n        children: (\n          <>\n            {canvas.patterns.map((pattern) => {\n              const fade = FADES[pattern.fade];\n              // The pattern's own fade still applies inside the light.\n              const mask = fade ? `${circle}, ${fade}` : circle;\n              return (\n                <svg\n                  key={pattern.key}\n                  className=\"absolute inset-0 size-full\"\n                  style={{\n                    maskImage: mask,\n                    WebkitMaskImage: mask,\n                    maskComposite: fade ? 'intersect' : undefined,\n                    WebkitMaskComposite: fade ? 'source-in' : undefined,\n                  }}\n                >\n                  <Marks id={`${prefix}${pattern.key}`} spec={pattern} viewport={canvas.viewport} />\n                </svg>\n              );\n            })}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nCanvasSpotlight.displayName = 'Canvas.Spotlight';\n\nexport interface CanvasContentProps extends ComponentProps<'div'> {}\n\n/** What sits on the canvas, above its patterns and its light. */\nfunction CanvasContent({ className, render, ...props }: CanvasContentProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'canvas-content', className: canvasVariants().content({ className }) }, props),\n  });\n}\nCanvasContent.displayName = 'Canvas.Content';\n\nexport const Canvas = Object.assign(CanvasRoot, {\n  Pattern: CanvasPattern,\n  Spotlight: CanvasSpotlight,\n  Content: CanvasContent,\n});\n\nexport { CanvasPattern, CanvasSpotlight, CanvasContent };\n"
    }
  ]
}
