{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "striktly",
  "homepage": "https://striktly.com/docs/registry/",
  "items": [
    {
      "name": "shader-surface",
      "type": "registry:component",
      "title": "Striktly Shader Surface",
      "description": "Accessible React shader integration shell with video/poster fallback, pause controls, reduced motion, offscreen pausing and cleanup. Bring your own member-downloaded Striktly runtime.",
      "author": "Striktly <https://striktly.com>",
      "categories": [
        "backgrounds",
        "webgl",
        "animation"
      ],
      "files": [
        {
          "path": "registry/striktly/shader-surface.tsx",
          "type": "registry:component",
          "target": "@components/striktly/shader-surface.tsx",
          "content": "\"use client\";\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\n\nexport type ShaderConfig = Record<string, unknown> & { effect?: string; interaction?: number };\ntype Renderer = {\n  update(config: ShaderConfig): void;\n  destroy(): void;\n  setPointer(x: number, y: number, id?: number, time?: number): void;\n  resetPointer(): void;\n  pulse(x: number, y: number): void;\n};\nexport type ShaderModule = {\n  createBackground(host: HTMLElement, config: ShaderConfig): Renderer;\n  PRISM_STAGE_CSS?: string;\n};\nexport type ShaderSurfaceProps = {\n  /** Import your own member-downloaded shader kit. Keep this function stable. */\n  loadShader?: () => Promise<ShaderModule>;\n  config: ShaderConfig;\n  poster: string;\n  video?: string;\n  prismStage?: boolean;\n  children?: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n};\n\n/** Public integration shell. Contains no shader runtime, account or billing code. */\nexport function ShaderSurface({ loadShader, config, poster, video, prismStage = false, children, className, style }: ShaderSurfaceProps) {\n  const section = useRef<HTMLElement>(null);\n  const host = useRef<HTMLDivElement>(null);\n  const media = useRef<HTMLVideoElement>(null);\n  const renderer = useRef<Renderer | null>(null);\n  const settings = useRef(config);\n  const manualPause = useRef(false);\n  const visibility = useRef(true);\n  const sync = useRef<() => void>(() => {});\n  const [paused, setPaused] = useState(true);\n  const [live, setLive] = useState(false);\n  const [stageCss, setStageCss] = useState(\"\");\n\n  useEffect(() => { settings.current = config; renderer.current?.update(config); sync.current(); }, [config]);\n  useEffect(() => {\n    let disposed = false;\n    setLive(false);\n    setStageCss(\"\");\n    const preference = matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const apply = () => {\n      if (disposed) return;\n      const stop = manualPause.current || preference.matches || document.hidden || !visibility.current;\n      setPaused(stop);\n      renderer.current?.update({ paused: stop, interaction: stop ? 0 : settings.current.interaction ?? 1 });\n      if (media.current) {\n        if (stop || renderer.current) media.current.pause();\n        else void media.current.play().catch(() => {});\n      }\n    };\n    sync.current = apply;\n    const observer = new IntersectionObserver(([entry]) => { visibility.current = entry.isIntersecting; apply(); });\n    if (section.current) observer.observe(section.current);\n    document.addEventListener(\"visibilitychange\", apply);\n    preference.addEventListener(\"change\", apply);\n    apply();\n    if (loadShader) void loadShader().then(module => {\n      if (disposed || !host.current) return;\n      try {\n        const stop = manualPause.current || preference.matches || document.hidden || !visibility.current;\n        setStageCss(prismStage ? module.PRISM_STAGE_CSS ?? \"\" : \"\");\n        renderer.current = module.createBackground(host.current, { ...settings.current, maxPixels: settings.current.maxPixels ?? 700000, maxFPS: settings.current.maxFPS ?? 30, paused: stop, interaction: stop ? 0 : settings.current.interaction ?? 1 });\n        setLive(true);\n        apply();\n      } catch { host.current.replaceChildren(); setLive(false); apply(); }\n    }).catch(() => { if (!disposed) { setLive(false); apply(); } });\n    return () => {\n      disposed = true;\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", apply);\n      preference.removeEventListener(\"change\", apply);\n      renderer.current?.destroy();\n      renderer.current = null;\n      media.current?.pause();\n      sync.current = () => {};\n    };\n  }, [loadShader, prismStage]);\n\n  return <section ref={section} className={`${prismStage && live ? \"ambient-prism-stage \" : \"\"}${className ?? \"\"}`}\n    data-shader-mode={live ? \"live\" : \"preview\"} data-motion={paused ? \"paused\" : \"playing\"}\n    style={{ position: \"relative\", isolation: \"isolate\", overflow: \"hidden\", background: \"#111\", ...style }}\n    onPointerMove={event => { if (!paused) renderer.current?.setPointer(event.clientX, event.clientY, event.pointerId, event.timeStamp); }}\n    onPointerLeave={() => renderer.current?.resetPointer()}\n    onPointerCancel={() => renderer.current?.resetPointer()}\n    onPointerDown={event => { if (!paused && event.button === 0 && !(event.target as Element).closest(\"a,button,input,select,textarea\")) renderer.current?.pulse(event.clientX, event.clientY); }}>\n    {stageCss && <style>{stageCss}</style>}\n    <div aria-hidden=\"true\" style={{ position: \"absolute\", inset: 0, zIndex: -2, background: `center / cover url(${JSON.stringify(poster)})` }}>\n      {video && <video ref={media} src={video} poster={poster} muted loop playsInline preload=\"none\" style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\", visibility: live ? \"hidden\" : \"visible\" }} />}\n    </div>\n    <div ref={host} aria-hidden=\"true\" style={{ position: \"absolute\", inset: 0, zIndex: -1, pointerEvents: \"none\" }} />\n    {children}\n    <button type=\"button\" aria-pressed={paused} onClick={() => { manualPause.current = !manualPause.current; sync.current(); }}\n      style={{ position: \"absolute\", right: 20, bottom: 20, zIndex: 4, padding: \"10px 14px\", border: \"1px solid #ffffff60\", background: \"#101010\", color: \"#fff\", borderRadius: 999, cursor: \"pointer\" }}>\n      {paused ? \"Resume motion\" : \"Pause motion\"}\n    </button>\n  </section>;\n}\n"
        },
        {
          "path": "registry/striktly/LICENSE",
          "type": "registry:file",
          "target": "@components/striktly/LICENSE",
          "content": "MIT License\n\nCopyright (c) 2026 striktly contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
        }
      ],
      "docs": "Read https://striktly.com/docs/registry/ . Public scaffolding only; renderer downloads retain the normal membership check. Without loadShader this displays the supplied poster/video."
    },
    {
      "name": "prism-hero",
      "type": "registry:block",
      "title": "Prism landing page",
      "description": "A spacious landing page with a moving Prism background, clear typography, and a place for your next idea. Includes a recorded preview; connect the matching member-downloaded kit for live WebGL interaction.",
      "author": "Striktly <https://striktly.com>",
      "categories": [
        "backgrounds",
        "landing",
        "webgl"
      ],
      "registryDependencies": [
        "https://striktly.com/r/shader-surface.json"
      ],
      "files": [
        {
          "path": "registry/striktly/prism-hero.tsx",
          "type": "registry:component",
          "target": "@components/striktly/prism-hero.tsx",
          "content": "\"use client\";\nimport { ShaderSurface, type ShaderSurfaceProps } from \"./shader-surface\";\n\nconst config = {\n  \"effect\": \"prism\",\n  \"fadeIn\": 0,\n  \"fadeDelay\": 0,\n  \"overlayOpacity\": 0.5,\n  \"overlayBlend\": \"normal\",\n  \"overlayFit\": \"cover\",\n  \"glow\": 0.15,\n  \"glowRadius\": 12,\n  \"colors\": [\n    \"#1b0b3b\",\n    \"#ed72d6\",\n    \"#628fff\"\n  ],\n  \"speed\": 0.4,\n  \"scale\": 1,\n  \"cameraZoom\": 1,\n  \"cameraRotation\": 0,\n  \"cameraX\": 0,\n  \"cameraY\": 0,\n  \"softness\": 0.5,\n  \"blur\": 0,\n  \"noise\": 0.015,\n  \"intensity\": 1,\n  \"interaction\": 1,\n  \"pingEvery\": 2.4,\n  \"quality\": 0.5,\n  \"adaptive\": true,\n  \"maxPixels\": 700000,\n  \"maxFPS\": 30,\n  \"seed\": 0,\n  \"globalPointer\": false,\n  \"paused\": false\n};\ntype Props = Omit<ShaderSurfaceProps, \"config\" | \"poster\" | \"video\" | \"prismStage\"> & { config?: ShaderSurfaceProps[\"config\"] };\n\n/** Pass loadShader={() => import(\"./your-downloaded-kit/shader-background.js\")} through a stable module-level function. */\nexport function PrismHero(props: Props) {\n  return <ShaderSurface {...props} config={{ ...config, ...props.config }} poster=\"https://striktly.com/shader-content/c95289cdaea2230a65b802c2a20a8e959bc7f32114546534d14a23440d831f21/posters/prism.webp\" video=\"https://striktly.com/shader-content/c95289cdaea2230a65b802c2a20a8e959bc7f32114546534d14a23440d831f21/videos/prism.mp4\" prismStage={true} style={{ minHeight: 600, display: \"grid\", alignItems: \"center\", color: \"#fff\", ...props.style }}>\n    <div style={{ padding: \"clamp(30px, 7vw, 100px)\", maxWidth: 1000, position: \"relative\", zIndex: 3 }}>\n      <p style={{ font: \"12px ui-monospace, monospace\", letterSpacing: \".12em\" }}>A LITTLE ATMOSPHERE GOES A LONG WAY</p>\n      <h1 style={{ font: \"650 clamp(48px, 8vw, 108px)/.98 system-ui, sans-serif\", letterSpacing: \"-.065em\", margin: \"30px 0\" }}>Make room for a different feeling.</h1>\n      <p style={{ maxWidth: 440, font: \"16px/1.7 system-ui, sans-serif\" }}>A spacious landing page with a moving Prism background, clear typography, and a place for your next idea.</p>\n      <a href=\"https://striktly.com/prism/\" style={{ display: \"inline-block\", marginTop: 20, padding: \"15px 22px\", borderRadius: 99, background: \"#baff33\", color: \"#101510\", textDecoration: \"none\", font: \"600 13px system-ui, sans-serif\" }}>Customize Prism ↗</a>\n      {props.children}\n    </div>\n  </ShaderSurface>;\n}\n"
        }
      ],
      "docs": "Import PrismHero from your components/striktly/prism-hero file. Pass a stable loadShader function importing your Prism kit. See https://striktly.com/docs/registry/ and https://striktly.com/docs/landing-pages/ . No account keys are needed in the project.",
      "meta": {
        "shaderId": "prism",
        "documentation": "https://striktly.com/docs/landing-pages/",
        "starter": "https://striktly.com/starters/prism-landing.zip",
        "runtimeIncluded": false
      }
    },
    {
      "name": "chrome-portfolio",
      "type": "registry:block",
      "title": "Liquid Chrome portfolio",
      "description": "A portfolio starter that pairs a fluid metallic introduction with a quiet, readable grid for your projects. Includes a recorded preview; connect the matching member-downloaded kit for live WebGL interaction.",
      "author": "Striktly <https://striktly.com>",
      "categories": [
        "backgrounds",
        "portfolio",
        "webgl"
      ],
      "registryDependencies": [
        "https://striktly.com/r/shader-surface.json"
      ],
      "files": [
        {
          "path": "registry/striktly/chrome-portfolio.tsx",
          "type": "registry:component",
          "target": "@components/striktly/chrome-portfolio.tsx",
          "content": "\"use client\";\nimport { ShaderSurface, type ShaderSurfaceProps } from \"./shader-surface\";\n\nconst config = {\n  \"effect\": \"liquidchrome\",\n  \"fadeIn\": 0,\n  \"fadeDelay\": 0,\n  \"overlayOpacity\": 0.5,\n  \"overlayBlend\": \"normal\",\n  \"overlayFit\": \"cover\",\n  \"iridescence\": 0.12,\n  \"edgeGlow\": 0.8,\n  \"glow\": 0,\n  \"glowRadius\": 12,\n  \"flowAngle\": -24,\n  \"colors\": [\n    \"#080c13\",\n    \"#7b8a9e\",\n    \"#f4f7fa\"\n  ],\n  \"speed\": 0.42,\n  \"contrast\": 1.15,\n  \"scale\": 1,\n  \"zoom\": 1,\n  \"cameraZoom\": 1,\n  \"cameraRotation\": 0,\n  \"cameraX\": 0,\n  \"cameraY\": 0,\n  \"softness\": 0.2,\n  \"blur\": 0,\n  \"noise\": 0.003,\n  \"intensity\": 1,\n  \"interaction\": 0.8,\n  \"amplitude\": 1.1,\n  \"turbulence\": 0.6,\n  \"gridRadius\": 160,\n  \"pingEvery\": 2.4,\n  \"quality\": 1,\n  \"adaptive\": true,\n  \"maxPixels\": 700000,\n  \"maxFPS\": 30,\n  \"seed\": 6,\n  \"globalPointer\": false,\n  \"paused\": false\n};\ntype Props = Omit<ShaderSurfaceProps, \"config\" | \"poster\" | \"video\" | \"prismStage\"> & { config?: ShaderSurfaceProps[\"config\"] };\n\n/** Pass loadShader={() => import(\"./your-downloaded-kit/shader-background.js\")} through a stable module-level function. */\nexport function ChromePortfolio(props: Props) {\n  return <ShaderSurface {...props} config={{ ...config, ...props.config }} poster=\"https://striktly.com/shader-content/c95289cdaea2230a65b802c2a20a8e959bc7f32114546534d14a23440d831f21/posters/liquidchrome.webp\" video=\"https://striktly.com/shader-content/c95289cdaea2230a65b802c2a20a8e959bc7f32114546534d14a23440d831f21/videos/liquidchrome.mp4\" prismStage={false} style={{ minHeight: 600, display: \"grid\", alignItems: \"center\", color: \"#fff\", ...props.style }}>\n    <div style={{ padding: \"clamp(30px, 7vw, 100px)\", maxWidth: 1000, position: \"relative\", zIndex: 3 }}>\n      <p style={{ font: \"12px ui-monospace, monospace\", letterSpacing: \".12em\" }}>INDEPENDENT IDEAS / DISTINCTIVE SURFACES</p>\n      <h1 style={{ font: \"650 clamp(48px, 8vw, 108px)/.98 system-ui, sans-serif\", letterSpacing: \"-.065em\", margin: \"30px 0\" }}>Work with a little reflection.</h1>\n      <p style={{ maxWidth: 440, font: \"16px/1.7 system-ui, sans-serif\" }}>A portfolio starter that pairs a fluid metallic introduction with a quiet, readable grid for your projects.</p>\n      <a href=\"https://striktly.com/shaders/liquid-chrome/\" style={{ display: \"inline-block\", marginTop: 20, padding: \"15px 22px\", borderRadius: 99, background: \"#baff33\", color: \"#101510\", textDecoration: \"none\", font: \"600 13px system-ui, sans-serif\" }}>Customize Liquid Chrome ↗</a>\n      {props.children}\n    </div>\n  </ShaderSurface>;\n}\n"
        }
      ],
      "docs": "Import ChromePortfolio from your components/striktly/chrome-portfolio file. Pass a stable loadShader function importing your Liquid Chrome kit. See https://striktly.com/docs/registry/ and https://striktly.com/docs/portfolios/ . No account keys are needed in the project.",
      "meta": {
        "shaderId": "liquidchrome",
        "documentation": "https://striktly.com/docs/portfolios/",
        "starter": "https://striktly.com/starters/chrome-portfolio.zip",
        "runtimeIncluded": false
      }
    },
    {
      "name": "kinetic-product",
      "type": "registry:block",
      "title": "Kinetic product page",
      "description": "An interactive product-page starter with a mouse-reactive Kinetic grid and room to explain what your product does. Includes a recorded preview; connect the matching member-downloaded kit for live WebGL interaction.",
      "author": "Striktly <https://striktly.com>",
      "categories": [
        "backgrounds",
        "product",
        "webgl"
      ],
      "registryDependencies": [
        "https://striktly.com/r/shader-surface.json"
      ],
      "files": [
        {
          "path": "registry/striktly/kinetic-product.tsx",
          "type": "registry:component",
          "target": "@components/striktly/kinetic-product.tsx",
          "content": "\"use client\";\nimport { ShaderSurface, type ShaderSurfaceProps } from \"./shader-surface\";\n\nconst config = {\n  \"effect\": \"kinetic\",\n  \"fadeIn\": 0,\n  \"fadeDelay\": 0,\n  \"overlayOpacity\": 0.5,\n  \"overlayBlend\": \"normal\",\n  \"overlayFit\": \"cover\",\n  \"glow\": 0.15,\n  \"glowRadius\": 12,\n  \"colors\": [\n    \"#101316\",\n    \"#39434d\",\n    \"#d7edf1\"\n  ],\n  \"speed\": 0.28,\n  \"scale\": 1,\n  \"cameraZoom\": 1,\n  \"cameraRotation\": 0,\n  \"cameraX\": 0,\n  \"cameraY\": 0,\n  \"blur\": 0,\n  \"noise\": 0.003,\n  \"intensity\": 1,\n  \"interaction\": 1,\n  \"gridSpacing\": 36,\n  \"dotSize\": 0.5,\n  \"gridRadius\": 160,\n  \"lineWidth\": 0.3,\n  \"pulseStrength\": 1,\n  \"pulseSpeed\": 220,\n  \"ringWidth\": 44,\n  \"pingEvery\": 2.4,\n  \"quality\": 1,\n  \"adaptive\": true,\n  \"maxPixels\": 700000,\n  \"maxFPS\": 30,\n  \"seed\": 0,\n  \"globalPointer\": false,\n  \"paused\": false\n};\ntype Props = Omit<ShaderSurfaceProps, \"config\" | \"poster\" | \"video\" | \"prismStage\"> & { config?: ShaderSurfaceProps[\"config\"] };\n\n/** Pass loadShader={() => import(\"./your-downloaded-kit/shader-background.js\")} through a stable module-level function. */\nexport function KineticProduct(props: Props) {\n  return <ShaderSurface {...props} config={{ ...config, ...props.config }} poster=\"https://striktly.com/shader-content/c95289cdaea2230a65b802c2a20a8e959bc7f32114546534d14a23440d831f21/posters/kinetic.webp\" video=\"https://striktly.com/shader-content/c95289cdaea2230a65b802c2a20a8e959bc7f32114546534d14a23440d831f21/videos/kinetic.mp4\" prismStage={false} style={{ minHeight: 600, display: \"grid\", alignItems: \"center\", color: \"#fff\", ...props.style }}>\n    <div style={{ padding: \"clamp(30px, 7vw, 100px)\", maxWidth: 1000, position: \"relative\", zIndex: 3 }}>\n      <p style={{ font: \"12px ui-monospace, monospace\", letterSpacing: \".12em\" }}>GOOD TOOLS SHOULD FEEL ALIVE</p>\n      <h1 style={{ font: \"650 clamp(48px, 8vw, 108px)/.98 system-ui, sans-serif\", letterSpacing: \"-.065em\", margin: \"30px 0\" }}>Less friction. More forward.</h1>\n      <p style={{ maxWidth: 440, font: \"16px/1.7 system-ui, sans-serif\" }}>An interactive product-page starter with a mouse-reactive Kinetic grid and room to explain what your product does.</p>\n      <a href=\"https://striktly.com/kinetic/\" style={{ display: \"inline-block\", marginTop: 20, padding: \"15px 22px\", borderRadius: 99, background: \"#baff33\", color: \"#101510\", textDecoration: \"none\", font: \"600 13px system-ui, sans-serif\" }}>Customize Kinetic ↗</a>\n      {props.children}\n    </div>\n  </ShaderSurface>;\n}\n"
        }
      ],
      "docs": "Import KineticProduct from your components/striktly/kinetic-product file. Pass a stable loadShader function importing your Kinetic kit. See https://striktly.com/docs/registry/ and https://striktly.com/docs/product-pages/ . No account keys are needed in the project.",
      "meta": {
        "shaderId": "kinetic",
        "documentation": "https://striktly.com/docs/product-pages/",
        "starter": "https://striktly.com/starters/kinetic-product.zip",
        "runtimeIncluded": false
      }
    }
  ]
}
