{"id":29543253,"url":"https://github.com/prem-acharya/pattern-lock","last_synced_at":"2026-01-20T16:26:23.154Z","repository":{"id":302264015,"uuid":"1011799164","full_name":"prem-acharya/pattern-lock","owner":"prem-acharya","description":"Pattern Lock is a UI component that allows users to draw a pattern by connecting dots, similar to the pattern lock feature found on many mobile devices.","archived":false,"fork":false,"pushed_at":"2025-12-25T17:58:58.000Z","size":638,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-12-27T04:46:03.312Z","etag":null,"topics":["component","nextjs","pattern-lock","react","reactjs","shadcn-ui","tailwindcss","ui-components"],"latest_commit_sha":null,"homepage":"https://shadcn-pattern-lock.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/prem-acharya.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-01T11:01:57.000Z","updated_at":"2025-12-25T17:59:02.000Z","dependencies_parsed_at":"2025-07-01T12:41:21.328Z","dependency_job_id":"ab2aa07c-460d-4a00-968b-30ac40580053","html_url":"https://github.com/prem-acharya/pattern-lock","commit_stats":null,"previous_names":["prem-acharya/pattern-lock"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/prem-acharya/pattern-lock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prem-acharya%2Fpattern-lock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prem-acharya%2Fpattern-lock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prem-acharya%2Fpattern-lock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prem-acharya%2Fpattern-lock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/prem-acharya","download_url":"https://codeload.github.com/prem-acharya/pattern-lock/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prem-acharya%2Fpattern-lock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28607028,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["component","nextjs","pattern-lock","react","reactjs","shadcn-ui","tailwindcss","ui-components"],"created_at":"2025-07-17T13:42:32.143Z","updated_at":"2026-01-20T16:26:23.135Z","avatar_url":"https://github.com/prem-acharya.png","language":"TypeScript","readme":"---\ntitle: Documentation\ndescription: Pattern Lock is a UI component that allows users to draw a pattern by connecting dots, similar to the pattern lock feature found on many mobile devices.\n---\n\n### Features\n\n- Click and drag to connect the dots and create a pattern.\n- Press the **Reset** button to clear the pattern.\n- The output below the lock shows the pattern as a sequence of numbers.\n- Fully responsive and works in both light and dark themes.\n\n### Usage\n\n```tsx filename=\"usage.tsx\"\nimport { PatternLock } from \"@/components/pattern-lock\";\n\n\u003cPatternLock\n  pattern={pattern}\n  onPatternChange={setPattern}\n  dotSize={24}\n  lineWidth={4}\n/\u003e;\n```\n\n### Props\n\n| Prop                | Type                          | Default | Description                             |\n| ------------------- | ----------------------------- | ------- | --------------------------------------- |\n| `pattern`           | `number[]`                    | `[]`    | Current pattern as array of dot indices |\n| `onPatternChange`   | `(pattern: number[]) =\u003e void` | -       | Callback when pattern changes           |\n| `onPatternComplete` | `(pattern: number[]) =\u003e void` | -       | Callback when drawing is complete       |\n| `dotSize`           | `number`                      | `20`    | Size of the dots in pixels              |\n| `lineWidth`         | `number`                      | `4`     | Width of the connecting lines           |\n| `className`         | `string`                      | -       | Additional CSS classes                  |\n| `disabled`          | `boolean`                     | `false` | Whether the component is disabled       |\n\n### Installation\n\n```bash filename=\"terminal\"\nnpm install next-themes lucide-react\n```\n\n### Component\n\n```tsx filename=\"pattern-lock.tsx\"\n\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useTheme } from \"next-themes\";\n\ninterface PatternLockProps {\n  pattern?: number[];\n  onPatternComplete?: (pattern: number[]) =\u003e void;\n  onPatternChange?: (pattern: number[]) =\u003e void;\n  dotSize?: number;\n  lineWidth?: number;\n  className?: string;\n  disabled?: boolean;\n}\n\ninterface Point {\n  x: number;\n  y: number;\n}\n\nfunction isPointInDot(\n  point: Point | null,\n  dot: Point,\n  radius: number\n): boolean {\n  if (!point) return false;\n  const distance = Math.sqrt((point.x - dot.x) ** 2 + (point.y - dot.y) ** 2);\n  return distance \u003c= radius;\n}\n\nfunction getPointFromEvent(\n  event: React.MouseEvent | React.TouchEvent,\n  canvasRef: React.RefObject\u003cHTMLCanvasElement | null\u003e\n): Point {\n  const canvas = canvasRef.current;\n  if (!canvas) return { x: 0, y: 0 };\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width / rect.width;\n  const scaleY = canvas.height / rect.height;\n  if (\"touches\" in event) {\n    const touch = event.touches[0] || event.changedTouches[0];\n    return {\n      x: (touch.clientX - rect.left) * scaleX,\n      y: (touch.clientY - rect.top) * scaleY,\n    };\n  } else {\n    return {\n      x: (event.clientX - rect.left) * scaleX,\n      y: (event.clientY - rect.top) * scaleY,\n    };\n  }\n}\n\nfunction handleKeyDown(\n  event: React.KeyboardEvent,\n  onPatternChange?: (pattern: number[]) =\u003e void\n) {\n  if (event.key === \"Escape\" || event.key === \"Delete\") {\n    onPatternChange?.([]);\n  }\n}\n\nexport function PatternLock({\n  pattern = [],\n  onPatternComplete,\n  onPatternChange,\n  dotSize = 20,\n  lineWidth = 4,\n  className,\n  disabled = false,\n}: PatternLockProps) {\n  const canvasRef = React.useRef\u003cHTMLCanvasElement | null\u003e(null);\n  const [isDrawing, setIsDrawing] = React.useState(false);\n  const [currentPoint, setCurrentPoint] = React.useState\u003cPoint | null\u003e(null);\n  const [size, setSize] = React.useState(300);\n  const { resolvedTheme } = useTheme();\n  const [isHoveringDot, setIsHoveringDot] = React.useState(false);\n  const [mounted, setMounted] = React.useState(false);\n\n  const safeTheme = mounted ? resolvedTheme : \"light\";\n\n  React.useEffect(() =\u003e {\n    setMounted(true);\n    function updateSize() {\n      if (typeof window !== \"undefined\" \u0026\u0026 window.innerWidth \u003c 640) {\n        setSize(250);\n      } else {\n        setSize(300);\n      }\n    }\n    updateSize();\n    if (typeof window !== \"undefined\") {\n      window.addEventListener(\"resize\", updateSize);\n      return () =\u003e window.removeEventListener(\"resize\", updateSize);\n    }\n  }, []);\n\n  const dots = React.useMemo(() =\u003e {\n    const dotsArray: Point[] = [];\n    const spacing = size / 4;\n    for (let row = 0; row \u003c 3; row++) {\n      for (let col = 0; col \u003c 3; col++) {\n        dotsArray.push({\n          x: spacing + col * spacing,\n          y: spacing + row * spacing,\n        });\n      }\n    }\n    return dotsArray;\n  }, [size]);\n\n  const drawCanvas = React.useCallback(() =\u003e {\n    if (!mounted) return;\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    ctx.clearRect(0, 0, size, size);\n    if (pattern.length \u003e 1) {\n      let lineColor = \"#000\";\n      if (safeTheme === \"dark\") {\n        lineColor = \"#fff\";\n      }\n      ctx.strokeStyle = lineColor;\n      ctx.lineWidth = lineWidth;\n      ctx.lineCap = \"round\";\n      ctx.lineJoin = \"round\";\n      ctx.beginPath();\n      ctx.moveTo(dots[pattern[0]].x, dots[pattern[0]].y);\n      for (let i = 1; i \u003c pattern.length; i++) {\n        ctx.lineTo(dots[pattern[i]].x, dots[pattern[i]].y);\n      }\n      if (isDrawing \u0026\u0026 currentPoint) {\n        ctx.lineTo(currentPoint.x, currentPoint.y);\n      }\n      ctx.stroke();\n    }\n    dots.forEach((dot, index) =\u003e {\n      const isSelected = pattern.includes(index);\n      const isActive =\n        isSelected || (isDrawing \u0026\u0026 isPointInDot(currentPoint, dot, dotSize));\n      ctx.beginPath();\n      ctx.arc(dot.x, dot.y, dotSize / 2, 0, 2 * Math.PI);\n      let dotColor = \"#000\";\n      if (safeTheme === \"dark\") {\n        dotColor = \"#fff\";\n      }\n      if (isActive) {\n        ctx.fillStyle = \"hsl(var(--primary))\";\n        ctx.fill();\n        ctx.strokeStyle = \"hsl(var(--primary-foreground))\";\n        ctx.lineWidth = 2;\n        ctx.stroke();\n      } else {\n        ctx.fillStyle = dotColor;\n        ctx.fill();\n        ctx.strokeStyle = \"hsl(var(--border))\";\n        ctx.lineWidth = 2;\n        ctx.stroke();\n      }\n    });\n  }, [\n    mounted,\n    pattern,\n    isDrawing,\n    currentPoint,\n    dots,\n    size,\n    dotSize,\n    lineWidth,\n    safeTheme,\n  ]);\n\n  const handleMove = (event: React.MouseEvent | React.TouchEvent) =\u003e {\n    if (!isDrawing || disabled) return;\n    event.preventDefault();\n    const point = getPointFromEvent(event, canvasRef);\n    setCurrentPoint(point);\n    const dotIndex = dots.findIndex((dot) =\u003e isPointInDot(point, dot, dotSize));\n    if (dotIndex !== -1 \u0026\u0026 !pattern.includes(dotIndex)) {\n      const newPattern = [...pattern, dotIndex];\n      onPatternChange?.(newPattern);\n    }\n  };\n\n  const handleMouseMove = (event: React.MouseEvent | React.TouchEvent) =\u003e {\n    if (!isDrawing \u0026\u0026 !disabled \u0026\u0026 \"clientX\" in event) {\n      const point = getPointFromEvent(event, canvasRef);\n      const overDot = dots.some((dot) =\u003e isPointInDot(point, dot, dotSize));\n      setIsHoveringDot(overDot);\n    }\n    handleMove(event);\n  };\n\n  React.useEffect(() =\u003e {\n    drawCanvas();\n  }, [drawCanvas]);\n\n  const handleStart = (event: React.MouseEvent | React.TouchEvent) =\u003e {\n    if (disabled) return;\n    event.preventDefault();\n    const point = getPointFromEvent(event, canvasRef);\n    setIsDrawing(true);\n    setCurrentPoint(point);\n    const dotIndex = dots.findIndex((dot) =\u003e isPointInDot(point, dot, dotSize));\n    if (dotIndex !== -1) {\n      const newPattern = [dotIndex];\n      onPatternChange?.(newPattern);\n    }\n  };\n\n  const handleEnd = (event: React.MouseEvent | React.TouchEvent) =\u003e {\n    if (!isDrawing || disabled) return;\n    event.preventDefault();\n    setIsDrawing(false);\n    setCurrentPoint(null);\n    if (pattern.length \u003e 0) {\n      onPatternComplete?.(pattern);\n    }\n  };\n\n  if (!mounted) {\n    return (\n      \u003cdiv\n        className={cn(\"relative inline-block\", className)}\n        style={{ width: size, height: size }}\n      \u003e\n        \u003cdiv\n          className=\"border border-border rounded-lg bg-background flex items-center justify-center\"\n          style={{ width: size, height: size }}\n        \u003e\n          \u003cspan className=\"text-muted-foreground\"\u003eLoading...\u003c/span\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    );\n  }\n\n  return (\n    \u003cdiv\n      className={cn(\"relative inline-block\", className)}\n      role=\"application\"\n      aria-label=\"Pattern lock interface\"\n      tabIndex={0}\n      onKeyDown={(e) =\u003e handleKeyDown(e, onPatternChange)}\n    \u003e\n      \u003ccanvas\n        ref={canvasRef}\n        width={size}\n        height={size}\n        className={cn(\n          \"border border-border rounded-lg bg-background touch-none\",\n          disabled \u0026\u0026 \"opacity-50 cursor-not-allowed\",\n          isHoveringDot ? \"cursor-pointer\" : \"cursor-default\"\n        )}\n        onMouseDown={handleStart}\n        onMouseMove={handleMouseMove}\n        onMouseUp={handleEnd}\n        onMouseLeave={(e) =\u003e {\n          setIsHoveringDot(false);\n          handleEnd(e);\n        }}\n        onTouchStart={handleStart}\n        onTouchMove={handleMove}\n        onTouchEnd={handleEnd}\n        aria-label=\"Pattern lock grid - draw a pattern by connecting dots\"\n      /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprem-acharya%2Fpattern-lock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprem-acharya%2Fpattern-lock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprem-acharya%2Fpattern-lock/lists"}