skiper63

PreviousNext

Apple squicircle effect to any element with the filter property

Docs
skiper-uiui

Preview

Loading preview…
/skiper63.tsx
"use client";

import { motion } from "framer-motion";
import { GripHorizontal, RefreshCcw } from "lucide-react";
import React, { useState } from "react";

import { cn } from "@/lib/utils";

// to use the filter just add this to your layout.tsx
// <SkiperSquiCircleFilterLayout/>
// {children}

// on element you need to add squicircle just add the filter id SkiperSquiCircleFilter
//<div style={{filter: "url(#SquiCircleFilter)"}}></div>

// thats it you can use the filter now no extra rerenders no complications just pure css filter

export const SquiCircleFilterStatic = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className="absolute bottom-0 left-0"
      version="1.1"
    >
      <defs>
        <filter id="SkiperSquiCircleFilterLayout">
          <feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
          <feColorMatrix
            in="blur"
            mode="matrix"
            values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -7"
            result="goo"
          />
          <feBlend in="SourceGraphic" in2="goo" />
        </filter>
      </defs>
    </svg>
  );
};

// ------------------------------------------------------------
// use this to toggle the values no need to use this anywhere else
// else bcz it will just add more reRenders and you probably wont need that
// ------------------------------------------------------------

const Skiper63 = () => {
  const [toggle, setToggle] = useState(true);
  const [height, setHeight] = useState(200);
  const [width, setWidth] = useState(300);
  const [blurValue, setBlurValue] = useState(10);
  const [colorMatrixValue, setColorMatrixValue] = useState(20);
  const [alphaValue, setAlphaValue] = useState(-7);

  return (
    <div className="flex h-full w-full flex-col items-center justify-center">
      <div className="mb-20 grid content-start justify-items-center gap-6 text-center">
        <span className="after:to-foreground relative max-w-[12ch] text-xs uppercase leading-tight opacity-40 after:absolute after:left-1/2 after:top-full after:h-16 after:w-px after:bg-gradient-to-b after:from-transparent after:content-['']">
          squicircle with svg filter
        </span>
      </div>
      <SquiCircleFilter
        blurValue={blurValue}
        colorMatrixValue={colorMatrixValue}
        alphaValue={alphaValue}
      />
      <Options
        toggle={toggle}
        setToggle={setToggle}
        height={height}
        setHeight={setHeight}
        width={width}
        setWidth={setWidth}
        blurValue={blurValue}
        setBlurValue={setBlurValue}
        colorMatrixValue={colorMatrixValue}
        setColorMatrixValue={setColorMatrixValue}
        alphaValue={alphaValue}
        setAlphaValue={setAlphaValue}
      />
      <div
        className="bg-foreground rounded-2xl"
        style={{
          height: `${height}px`,
          width: `${width}px`,
          filter: toggle ? "url(#SquiCircleFilter)" : "none",
        }}
      ></div>
    </div>
  );
};

export { Skiper63 };

const SquiCircleFilter = ({
  blurValue = 10,
  colorMatrixValue = 20,
  alphaValue = -7,
}: {
  blurValue?: number;
  colorMatrixValue?: number;
  alphaValue?: number;
}) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className="absolute bottom-0 left-0"
      version="1.1"
    >
      <defs>
        <filter id="SquiCircleFilter">
          <feGaussianBlur
            in="SourceGraphic"
            stdDeviation={blurValue}
            result="blur"
          />
          <feColorMatrix
            in="blur"
            mode="matrix"
            values={`1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 ${colorMatrixValue} ${alphaValue}`}
            result="goo"
          />
          <feBlend in="SourceGraphic" in2="goo" />
        </filter>
      </defs>
    </svg>
  );
};

const Options = ({
  toggle,
  setToggle,
  height,
  setHeight,
  width,
  setWidth,
  blurValue,
  setBlurValue,
  colorMatrixValue,
  setColorMatrixValue,
  alphaValue,
  setAlphaValue,
}: {
  toggle: boolean;
  setToggle: (value: boolean) => void;
  height: number;
  setHeight: (value: number) => void;
  width: number;
  setWidth: (value: number) => void;
  blurValue: number;
  setBlurValue: (value: number) => void;
  colorMatrixValue: number;
  setColorMatrixValue: (value: number) => void;
  alphaValue: number;
  setAlphaValue: (value: number) => void;
}) => {
  const [isDragging, setIsDragging] = useState(false);

  const resetValues = () => {
    setToggle(false);
    setHeight(200);
    setWidth(300);
    setBlurValue(10);
    setColorMatrixValue(20);
    setAlphaValue(-7);
  };

  return (
    <motion.div
      className="top-30 border-foreground/10 bg-muted2 absolute right-1/2 flex w-[300px] translate-x-1/2 flex-col gap-3 rounded-3xl border p-3 backdrop-blur-sm lg:right-4 lg:translate-x-0"
      drag={isDragging}
      dragMomentum={false}
    >
      <div className="flex items-center justify-between">
        <span
          onPointerDown={() => setIsDragging(true)}
          onPointerUp={() => setIsDragging(false)}
          className="size-4 cursor-grab active:cursor-grabbing"
        >
          <GripHorizontal className="size-4 opacity-50" />
        </span>

        <p
          onClick={resetValues}
          className="hover:bg-foreground/10 group flex cursor-pointer items-center justify-center gap-2 rounded-lg px-2 py-1 text-sm opacity-50"
        >
          Reset
          <span className="group-active:-rotate-360 rotate-0 cursor-pointer transition-all duration-300 group-hover:rotate-90">
            <RefreshCcw className="size-4 opacity-50" />
          </span>
        </p>
      </div>

      <div className="flex w-full flex-col gap-3">
        {/* Toggle Control */}
        <div className="grid grid-cols-3 items-center gap-2 py-1">
          <p className="text-sm opacity-50">Filter :</p>
          <button
            onClick={() => setToggle(true)}
            className={cn(
              "bg-muted3 flex items-center justify-center rounded-lg py-1 text-left text-xs opacity-25 transition-colors",
              toggle && "opacity-100",
            )}
          >
            ON
          </button>
          <button
            onClick={() => setToggle(false)}
            className={cn(
              "bg-muted3 flex items-center justify-center rounded-lg py-1 text-left text-xs opacity-25 transition-colors",
              !toggle && "opacity-100",
            )}
          >
            OFF
          </button>
        </div>

        {/* Height Control */}
        <div className="grid w-full grid-cols-3 items-center py-1">
          <p className="text-sm opacity-50">Height :</p>
          <div className="flex w-full items-center justify-between gap-2">
            <input
              type="range"
              min={50}
              max={500}
              value={height}
              onChange={(e) => setHeight(Number(e.target.value))}
              className="bg-muted [&::-webkit-slider-runnable-track]:to-background [&::-webkit-slider-thumb]:bg-muted-foreground h-1.5 w-[150px] cursor-pointer appearance-none overflow-clip rounded-lg [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-moz-range-track]:bg-gradient-to-r [&::-moz-range-track]:from-blue-500 [&::-moz-range-track]:to-[#4F4F4E] [&::-moz-range-track]:bg-[length:var(--range-progress)_100%] [&::-moz-range-track]:bg-no-repeat [&::-webkit-slider-runnable-track]:bg-gradient-to-r [&::-webkit-slider-runnable-track]:from-blue-500 [&::-webkit-slider-runnable-track]:bg-[length:var(--range-progress)_100%] [&::-webkit-slider-runnable-track]:bg-no-repeat [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full"
              style={
                {
                  "--range-progress": `${((height - 50) / (500 - 50)) * 100}%`,
                } as React.CSSProperties
              }
            />
            <span className="w-8 text-right text-xs opacity-50">
              {height}px
            </span>
          </div>
        </div>

        {/* Width Control */}
        <div className="grid w-full grid-cols-3 items-center py-1">
          <p className="text-sm opacity-50">Width :</p>
          <div className="flex items-center justify-between gap-2">
            <input
              type="range"
              min={100}
              max={600}
              value={width}
              onChange={(e) => setWidth(Number(e.target.value))}
              className="bg-muted [&::-webkit-slider-runnable-track]:to-background [&::-webkit-slider-thumb]:bg-muted-foreground h-1.5 w-[150px] cursor-pointer appearance-none overflow-clip rounded-lg [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-moz-range-track]:bg-gradient-to-r [&::-moz-range-track]:from-blue-500 [&::-moz-range-track]:to-[#4F4F4E] [&::-moz-range-track]:bg-[length:var(--range-progress)_100%] [&::-moz-range-track]:bg-no-repeat [&::-webkit-slider-runnable-track]:bg-gradient-to-r [&::-webkit-slider-runnable-track]:from-blue-500 [&::-webkit-slider-runnable-track]:bg-[length:var(--range-progress)_100%] [&::-webkit-slider-runnable-track]:bg-no-repeat [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full"
              style={
                {
                  "--range-progress": `${((width - 100) / (600 - 100)) * 100}%`,
                } as React.CSSProperties
              }
            />
            <span className="w-8 text-right text-xs opacity-50">{width}px</span>
          </div>
        </div>

        {/* Blur Control */}
        <div className="grid w-full grid-cols-3 items-center py-1">
          <p className="text-sm opacity-50">Blur :</p>
          <div className="flex items-center justify-between gap-2">
            <input
              type="range"
              min={0}
              max={50}
              value={blurValue}
              onChange={(e) => setBlurValue(Number(e.target.value))}
              className="bg-muted [&::-webkit-slider-runnable-track]:to-background [&::-webkit-slider-thumb]:bg-muted-foreground h-1.5 w-[150px] cursor-pointer appearance-none overflow-clip rounded-lg [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-moz-range-track]:bg-gradient-to-r [&::-moz-range-track]:from-blue-500 [&::-moz-range-track]:to-[#4F4F4E] [&::-moz-range-track]:bg-[length:var(--range-progress)_100%] [&::-moz-range-track]:bg-no-repeat [&::-webkit-slider-runnable-track]:bg-gradient-to-r [&::-webkit-slider-runnable-track]:from-blue-500 [&::-webkit-slider-runnable-track]:bg-[length:var(--range-progress)_100%] [&::-webkit-slider-runnable-track]:bg-no-repeat [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full"
              style={
                {
                  "--range-progress": `${(blurValue / 50) * 100}%`,
                } as React.CSSProperties
              }
            />
            <span className="w-8 text-right text-xs opacity-50">
              {blurValue}
            </span>
          </div>
        </div>

        {/* Color Matrix Control */}
        <div className="grid w-full grid-cols-3 items-center py-1">
          <p className="text-sm opacity-50">Matrix :</p>
          <div className="flex items-center justify-between gap-2">
            <input
              type="range"
              min={1}
              max={50}
              value={colorMatrixValue}
              onChange={(e) => setColorMatrixValue(Number(e.target.value))}
              className="bg-muted [&::-webkit-slider-runnable-track]:to-background [&::-webkit-slider-thumb]:bg-muted-foreground h-1.5 w-[150px] cursor-pointer appearance-none overflow-clip rounded-lg [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-moz-range-track]:bg-gradient-to-r [&::-moz-range-track]:from-blue-500 [&::-moz-range-track]:to-[#4F4F4E] [&::-moz-range-track]:bg-[length:var(--range-progress)_100%] [&::-moz-range-track]:bg-no-repeat [&::-webkit-slider-runnable-track]:bg-gradient-to-r [&::-webkit-slider-runnable-track]:from-blue-500 [&::-webkit-slider-runnable-track]:bg-[length:var(--range-progress)_100%] [&::-webkit-slider-runnable-track]:bg-no-repeat [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full"
              style={
                {
                  "--range-progress": `${((colorMatrixValue - 1) / (50 - 1)) * 100}%`,
                } as React.CSSProperties
              }
            />
            <span className="w-8 text-right text-xs opacity-50">
              {colorMatrixValue}
            </span>
          </div>
        </div>

        {/* Alpha Control */}
        <div className="grid w-full grid-cols-3 items-center py-1">
          <p className="text-sm opacity-50">Alpha :</p>
          <div className="flex items-center justify-between gap-2">
            <input
              type="range"
              min={-20}
              max={0}
              value={alphaValue}
              onChange={(e) => setAlphaValue(Number(e.target.value))}
              className="bg-muted [&::-webkit-slider-runnable-track]:to-background [&::-webkit-slider-thumb]:bg-muted-foreground h-1.5 w-[150px] cursor-pointer appearance-none overflow-clip rounded-lg [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-moz-range-track]:bg-gradient-to-r [&::-moz-range-track]:from-blue-500 [&::-moz-range-track]:to-[#4F4F4E] [&::-moz-range-track]:bg-[length:var(--range-progress)_100%] [&::-moz-range-track]:bg-no-repeat [&::-webkit-slider-runnable-track]:bg-gradient-to-r [&::-webkit-slider-runnable-track]:from-blue-500 [&::-webkit-slider-runnable-track]:bg-[length:var(--range-progress)_100%] [&::-webkit-slider-runnable-track]:bg-no-repeat [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full"
              style={
                {
                  "--range-progress": `${((alphaValue + 20) / 20) * 100}%`,
                } as React.CSSProperties
              }
            />
            <span className="w-8 text-right text-xs opacity-50">
              {alphaValue}
            </span>
          </div>
        </div>
      </div>
    </motion.div>
  );
};

const LOGO_SPRING = {
  type: "spring",
  stiffness: 300,
  damping: 30,
};

Installation

npx shadcn@latest add @skiper-ui/skiper63

Usage

import { Skiper63 } from "@/components/ui/skiper63"
<Skiper63 />